mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16bd3ca575 | ||
|
|
927184e009 | ||
|
|
d4d9081936 | ||
|
|
a0bcfba73d | ||
|
|
09f4828006 | ||
|
|
cde64e255b | ||
|
|
7b134cef81 | ||
|
|
fde7502df8 | ||
|
|
955b28c520 | ||
|
|
5cfc37e26f | ||
|
|
42cb6b3a9d | ||
|
|
ad245e9f4c | ||
|
|
08260e6a55 | ||
|
|
aea12e9fed | ||
|
|
caef729a8d | ||
|
|
12030822ea | ||
|
|
97cf62d333 | ||
|
|
8240c92247 | ||
|
|
62a194c1b0 | ||
|
|
36e954c440 | ||
|
|
a674c183d5 | ||
|
|
4bab1025d7 | ||
|
|
7d21186158 | ||
|
|
eb5f89dae3 | ||
|
|
70292cb9c0 | ||
|
|
950f45833a |
@@ -30,10 +30,13 @@ jobs:
|
||||
uses: ./.github/workflows/frontend-build.yml
|
||||
secrets: inherit
|
||||
|
||||
# lint-server:
|
||||
# name: Server
|
||||
# uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
|
||||
# secrets: inherit
|
||||
lint-server:
|
||||
name: Server
|
||||
uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
lint-frontend:
|
||||
name: Frontend
|
||||
|
||||
@@ -28,6 +28,16 @@ You can see a live demo of CloudBeaver server here: https://demo.cloudbeaver.io
|
||||
|
||||
## Changelog
|
||||
|
||||
### 26.1.1 2026-06-22
|
||||
|
||||
### Changes since 26.1.0:
|
||||
|
||||
- General: Added a "Project Info" tab containing the project description to the project context menu.
|
||||
- Security:
|
||||
- Fixed a cross-site scripting vulnerability caused by unsanitized user input in the web interface.
|
||||
- Resolved a security flaw related to user-controlled file paths by adding strict validation to prevent unauthorized resource access.
|
||||
- Fixed the critical vulnerability (CVE-2026-9277) in the shell-quote library (updated to version 1.8.4).
|
||||
|
||||
### 26.1.0 2026-06-01
|
||||
|
||||
### Changes since 26.0.0:
|
||||
|
||||
+2
@@ -367,6 +367,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
|
||||
dataSourceRegistry.initializeDataSources();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataSourceParseResults loadDataSources(
|
||||
@NotNull List<DBPDataSourceConfigurationStorage> storages,
|
||||
@@ -382,6 +383,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
|
||||
dataSourceRegistry.saveDataSources();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSourceConfigurationManager getConfigurationManager() {
|
||||
return dataSourceRegistry.getConfigurationManager();
|
||||
|
||||
@@ -65,6 +65,7 @@ public class WebConnectionConfig {
|
||||
private Boolean defaultAutoCommit;
|
||||
private String defaultCatalogName;
|
||||
private String defaultSchemaName;
|
||||
private String connectionType;
|
||||
@NotNull
|
||||
private Map<String, String> defaultUserPreferences = new LinkedHashMap<>();
|
||||
|
||||
@@ -120,6 +121,7 @@ public class WebConnectionConfig {
|
||||
for (Map<String, Object> nhc : JSONUtils.getObjectList(params, "networkHandlersConfig")) {
|
||||
networkHandlersConfig.add(new WebNetworkHandlerConfigInput(nhc));
|
||||
}
|
||||
connectionType = JSONUtils.getString(params, "connectionType");
|
||||
}
|
||||
|
||||
@Property
|
||||
@@ -381,4 +383,13 @@ public class WebConnectionConfig {
|
||||
public void setDefaultUserPreferences(@NotNull Map<String, String> defaultUserPreferences) {
|
||||
this.defaultUserPreferences = defaultUserPreferences;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getConnectionType() {
|
||||
return connectionType;
|
||||
}
|
||||
|
||||
public void setConnectionType(@Nullable String connectionType) {
|
||||
this.connectionType = connectionType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,6 +586,11 @@ public class WebConnectionInfo {
|
||||
return tools;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getConnectionType() {
|
||||
return dataSourceContainer.getConnectionConfiguration().getConnectionType().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates param that checks whether credentials were saved only in session.
|
||||
*/
|
||||
|
||||
+2
-13
@@ -89,12 +89,6 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
return new SharedFileLockManager(getApplicationInstanceId());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Path getDefaultWorkingFolder() {
|
||||
return getServerConfigurationController().getWorkspacePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHeadlessMode() {
|
||||
return true;
|
||||
@@ -218,6 +212,8 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
@Override
|
||||
public Object start(IApplicationContext context) {
|
||||
initializeApplicationServices();
|
||||
setWorkspacePath(getServerConfigurationController().getWorkspacePath());
|
||||
|
||||
try {
|
||||
startServer();
|
||||
} catch (Exception e) {
|
||||
@@ -254,13 +250,6 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
return BaseWorkspaceImpl.readWorkspaceIdProperty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Path getWorkspaceDirectory() {
|
||||
return getServerConfigurationController().getWorkspacePath();
|
||||
}
|
||||
|
||||
|
||||
public String getApplicationId() {
|
||||
try {
|
||||
return getApplicationInstanceId();
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -53,9 +53,6 @@ public interface ServletApplication extends DBPApplication {
|
||||
@NotNull
|
||||
Path getDataDirectory(boolean create);
|
||||
|
||||
@NotNull
|
||||
Path getWorkspaceDirectory();
|
||||
|
||||
@NotNull
|
||||
Path getHomeDirectory();
|
||||
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -67,8 +67,7 @@ public abstract class BaseLocalResourceController implements RMController {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties)
|
||||
throws DBException {
|
||||
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) throws DBException {
|
||||
RMProject project = makeProjectFromId(projectId, true);
|
||||
if (project == null) {
|
||||
return null;
|
||||
@@ -110,8 +109,7 @@ public abstract class BaseLocalResourceController implements RMController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds)
|
||||
throws DBException {
|
||||
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException {
|
||||
DBPProject projectMetadata = getWebProject(projectId, false);
|
||||
return doFileReadOperation(
|
||||
projectId,
|
||||
|
||||
+44
-4
@@ -40,6 +40,7 @@ import org.jkiss.dbeaver.model.fs.lock.LockTarget;
|
||||
import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl;
|
||||
import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNLocalFolder;
|
||||
import org.jkiss.dbeaver.model.net.DBWNetworkProfile;
|
||||
import org.jkiss.dbeaver.model.rm.*;
|
||||
import org.jkiss.dbeaver.model.security.SMAdminController;
|
||||
import org.jkiss.dbeaver.model.sql.DBQuotaException;
|
||||
@@ -48,6 +49,7 @@ import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
|
||||
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
|
||||
import org.jkiss.dbeaver.registry.DataSourceParseResults;
|
||||
import org.jkiss.dbeaver.registry.ResourceTypeDescriptor;
|
||||
@@ -407,8 +409,13 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
registry::createDataSource
|
||||
)
|
||||
);
|
||||
Map<String, DBWNetworkProfile> oldNetworkProfiles = registry.getNetworkProfiles().getProfiles().stream()
|
||||
.collect(Collectors.toMap(
|
||||
DBWNetworkProfile::getProfileId,
|
||||
p -> p
|
||||
));
|
||||
DataSourceParseResults parseResults = super.updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds);
|
||||
sendDataSourcesConfigUpdatedEvent(registry, oldDataSources, parseResults);
|
||||
sendConfigUpdatedEvent(registry, oldDataSources, oldNetworkProfiles, parseResults);
|
||||
return parseResults != null;
|
||||
}
|
||||
|
||||
@@ -501,14 +508,48 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
return DBNLocalFolder.makeLocalFolderItemPath(projectId, folderPath);
|
||||
}
|
||||
|
||||
private void sendDataSourcesConfigUpdatedEvent(
|
||||
private void sendConfigUpdatedEvent(
|
||||
@NotNull DBPDataSourceRegistry registry,
|
||||
@NotNull Map<String, DataSourceDescriptor> oldDataSources,
|
||||
@NotNull Map<String, DBWNetworkProfile> oldNetworkProfiles,
|
||||
@Nullable DataSourceParseResults parseResults
|
||||
) {
|
||||
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null || oldDataSources.isEmpty()) {
|
||||
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null) {
|
||||
return;
|
||||
}
|
||||
boolean profilesChanged = !parseResults.removedProfiles.isEmpty();
|
||||
|
||||
// We don't need to check for updated profiles if there are already added or removed profiles
|
||||
if (!profilesChanged) {
|
||||
for (DBWNetworkProfile updatedProfile : parseResults.updatedProfiles) {
|
||||
if (oldNetworkProfiles.containsKey(updatedProfile.getProfileId())) {
|
||||
DBWNetworkProfile oldProfile = oldNetworkProfiles.get(updatedProfile.getProfileId());
|
||||
if (!oldProfile.equalConfigurations(updatedProfile)) {
|
||||
profilesChanged = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// profile was added
|
||||
profilesChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profilesChanged) {
|
||||
ServletAppUtils.getServletApplication().getEventController().addEvent(
|
||||
WSNetworkProfileEvent.update(
|
||||
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
|
||||
credentialsProvider.getActiveUserCredentials().getUserId(),
|
||||
registry.getProject().getId()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (oldDataSources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> updatedConfigurationDataSourceIds = new ArrayList<>();
|
||||
List<String> updatedNameDataSourceIds = new ArrayList<>();
|
||||
List<String> updatedInternalConfigurationDataSourceIds = new ArrayList<>();
|
||||
@@ -612,7 +653,6 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+10
@@ -123,6 +123,16 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
}
|
||||
}
|
||||
|
||||
public void migrateEventHandlersTo(@NotNull BaseWebSession target) {
|
||||
synchronized (sessionEventHandlers) {
|
||||
for (CBWebSessionEventHandler handler : sessionEventHandlers) {
|
||||
handler.migrateToSession(target);
|
||||
target.addEventHandler(handler);
|
||||
}
|
||||
sessionEventHandlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
return userContext.refresh(smAuthInfo);
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -68,6 +68,13 @@ public class WebSessionEventsFilter {
|
||||
}
|
||||
}
|
||||
|
||||
public void migrateTo(@NotNull WebSessionEventsFilter target) {
|
||||
synchronized (target.subscribedProjectIds) {
|
||||
target.subscribedEventTopics.addAll(this.subscribedEventTopics);
|
||||
target.subscribedProjectIds.addAll(this.subscribedProjectIds);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEventAllowed(WSEvent event) {
|
||||
if (event.isForceProcessed()) {
|
||||
return true;
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public class ServerGlobalWorkspace extends BaseWorkspaceImpl {
|
||||
@NotNull DBPPlatform platform,
|
||||
@NotNull ServletApplication application
|
||||
) {
|
||||
super(platform, application.getWorkspaceDirectory());
|
||||
super(platform, application.getWorkspacePath());
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
|
||||
@@ -303,6 +303,10 @@ public class ServletAppUtils {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Origin header: " + origin);
|
||||
}
|
||||
if ("null".equals(origin)) {
|
||||
//strange identity provider bug
|
||||
origin = null;
|
||||
}
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
origin = request.getHeader(HEADER_X_ORIGIN);
|
||||
if (log.isTraceEnabled()) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jkiss.dbeaver.model.access.DBAAuthCredentials;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials;
|
||||
@@ -40,6 +41,7 @@ import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
|
||||
import org.jkiss.dbeaver.model.net.ssh.SSHConstants;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceDisconnectEvent;
|
||||
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
@@ -320,6 +322,13 @@ public class WebDataSourceUtils {
|
||||
if (config.getConfigurationType() != null) {
|
||||
dsConfig.setConfigurationType(config.getConfigurationType());
|
||||
}
|
||||
if (config.getConnectionType() != null) {
|
||||
DBPConnectionType connectionType = DataSourceProviderRegistry.getInstance().getConnectionType(config.getConnectionType(), null);
|
||||
if (connectionType != null) {
|
||||
dsConfig.setConnectionType(connectionType);
|
||||
}
|
||||
}
|
||||
|
||||
if (CommonUtils.isEmpty(config.getUrl())) {
|
||||
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
*/
|
||||
package io.cloudbeaver.websocket;
|
||||
|
||||
import io.cloudbeaver.model.session.BaseWebSession;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
|
||||
|
||||
@@ -24,4 +26,6 @@ public interface CBWebSessionEventHandler {
|
||||
|
||||
void close();
|
||||
|
||||
void migrateToSession(@NotNull BaseWebSession newSession);
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -14,11 +14,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.admin;
|
||||
package io.cloudbeaver.model.config;
|
||||
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
@@ -57,7 +57,7 @@ public class AdminServerConfig {
|
||||
@Nullable
|
||||
private String bindSessionToIp;
|
||||
|
||||
public AdminServerConfig(Map<String, Object> params) {
|
||||
public AdminServerConfig(@NotNull Map<String, Object> params) {
|
||||
this.serverName = JSONUtils.getString(params, "serverName");
|
||||
this.adminName = JSONUtils.getString(params, "adminName");
|
||||
this.adminPassword = JSONUtils.getString(params, "adminPassword");
|
||||
+3
-3
@@ -228,11 +228,11 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
Location instanceLoc = Platform.getInstanceLocation();
|
||||
try {
|
||||
if (!instanceLoc.isSet()) { // always false?
|
||||
URL wsLocationURL = getWorkspaceDirectory().toUri().toURL();
|
||||
URL wsLocationURL = getWorkspacePath().toUri().toURL();
|
||||
instanceLoc.set(wsLocationURL, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting workspace location to " + getWorkspaceDirectory().toAbsolutePath(), e);
|
||||
log.error("Error setting workspace location to " + getWorkspacePath().toAbsolutePath(), e);
|
||||
return;
|
||||
}
|
||||
this.systemInformationCollector = createSystemInformationCollector();
|
||||
@@ -443,7 +443,7 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
|
||||
@NotNull
|
||||
public Path getDataDirectory(boolean create) {
|
||||
Path dataDir = getWorkspaceDirectory().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
|
||||
Path dataDir = getWorkspacePath().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
|
||||
if (create && !Files.exists(dataDir)) {
|
||||
try {
|
||||
Files.createDirectories(dataDir);
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -20,10 +20,7 @@ import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.cloudbeaver.model.app.BaseServerConfigurationController;
|
||||
import io.cloudbeaver.model.app.BaseServletApplication;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.config.PasswordPolicyConfiguration;
|
||||
import io.cloudbeaver.model.config.SMControllerConfiguration;
|
||||
import io.cloudbeaver.model.config.*;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -508,7 +505,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
@NotNull
|
||||
protected Map<String, Object> collectServerConfigProperties(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
Map<String, Object> originServerConfig
|
||||
@NotNull Map<String, Object> originServerConfig
|
||||
) {
|
||||
var serverConfigProperties = new LinkedHashMap<String, Object>();
|
||||
if (!CommonUtils.isEmpty(serverConfig.getServerName())) {
|
||||
@@ -633,6 +630,9 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public abstract CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper();
|
||||
|
||||
public CBAppConfig getAppConfiguration() {
|
||||
return appConfiguration;
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -19,6 +19,7 @@ package io.cloudbeaver.server;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.InstanceCreator;
|
||||
import io.cloudbeaver.model.config.AdminServerConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.config.WebDatabaseConfig;
|
||||
import org.jkiss.code.NotNull;
|
||||
@@ -41,6 +42,11 @@ public class CBServerConfigurationControllerEmbedded<T extends CBServerConfig> e
|
||||
super(serverConfig, homeDirectory);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper() {
|
||||
return new CBServerConfigurationMapper<>();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected Map<String, Object> collectServerConfigProperties(
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server;
|
||||
|
||||
import io.cloudbeaver.DBWFeatureSet;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.config.AdminServerConfig;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CBServerConfigurationMapper<C extends CBServerConfig, I extends AdminServerConfig> {
|
||||
private static final Log log = Log.getLog(CBServerConfigurationMapper.class);
|
||||
|
||||
@NotNull
|
||||
public CBServerConfigurations loadServerConfigurationsFromParams(@NotNull Map<String, Object> params) {
|
||||
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
|
||||
C serverConfig = createServerConfiguration();
|
||||
serverConfig.setServerName(CBApplication.getInstance().getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
|
||||
String adminName = null;
|
||||
String adminPassword = null;
|
||||
|
||||
if (!params.isEmpty()) { // FE can send an empty configuration
|
||||
I config = createInputConfiguration(params);
|
||||
adminName = config.getAdminName();
|
||||
adminPassword = config.getAdminPassword();
|
||||
|
||||
populateConfigurations(config, serverConfig, appConfig);
|
||||
}
|
||||
|
||||
return new CBServerConfigurations(
|
||||
serverConfig,
|
||||
appConfig,
|
||||
adminName,
|
||||
adminPassword
|
||||
);
|
||||
}
|
||||
|
||||
protected void populateConfigurations(@NotNull I input, @NotNull C serverConfig, @NotNull CBAppConfig appConfig) {
|
||||
appConfig.setAnonymousAccessEnabled(input.isAnonymousAccessEnabled());
|
||||
appConfig.setSupportsCustomConnections(input.isCustomConnectionsEnabled());
|
||||
appConfig.setPublicCredentialsSaveEnabled(input.isPublicCredentialsSaveEnabled());
|
||||
appConfig.setAdminCredentialsSaveEnabled(input.isAdminCredentialsSaveEnabled());
|
||||
updateDisabledFeaturesConfig(appConfig, input.getEnabledFeatures());
|
||||
// custom logic for enabling embedded drivers
|
||||
updateDisabledDriversConfig(appConfig, input.getDisabledDrivers());
|
||||
appConfig.setResourceManagerEnabled(input.isResourceManagerEnabled());
|
||||
appConfig.setSecretManagerEnabled(input.isSecretManagerEnabled());
|
||||
|
||||
if (CommonUtils.isEmpty(input.getEnabledAuthProviders())) {
|
||||
// All of them
|
||||
appConfig.setEnabledAuthProviders(new String[0]);
|
||||
} else {
|
||||
appConfig.setEnabledAuthProviders(input.getEnabledAuthProviders().toArray(new String[0]));
|
||||
}
|
||||
|
||||
appConfig.setDefaultNavigatorSettings(
|
||||
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
|
||||
|
||||
serverConfig.setServerName(input.getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(input.getSessionExpireTime());
|
||||
if (input.getForceHttps() != null) {
|
||||
serverConfig.setForceHttps(input.getForceHttps());
|
||||
}
|
||||
if (input.getSupportedHosts() != null) {
|
||||
serverConfig.setSupportedHosts(input.getSupportedHosts());
|
||||
}
|
||||
if (input.getBindSessionToIp() != null) {
|
||||
serverConfig.setBindSessionToIp(input.getBindSessionToIp());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
|
||||
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
|
||||
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
|
||||
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
|
||||
.map(DBWFeatureSet::getId)
|
||||
.filter(id -> !enabledIds.contains(id))
|
||||
.toArray(String[]::new);
|
||||
appConfig.setDisabledFeatures(disabledFeatures);
|
||||
}
|
||||
|
||||
// we disable embedded drivers by default and enable it in enabled drivers list
|
||||
// that's why we need so complicated logic for disabling drivers
|
||||
|
||||
private void updateDisabledDriversConfig(@NotNull CBAppConfig appConfig, @NotNull String[] disabledDriversConfig) {
|
||||
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
|
||||
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
|
||||
|
||||
// remove all disabled embedded drivers from enabled drivers list
|
||||
enabledIds.removeAll(disabledIds);
|
||||
|
||||
// enable embedded driver if it is not in disabled drivers list
|
||||
for (String driverId : appConfig.getDisabledDrivers()) {
|
||||
if (disabledIds.contains(driverId)) {
|
||||
// driver is also disabled
|
||||
continue;
|
||||
}
|
||||
// driver is removed from disabled list
|
||||
// we need to enable if it is embedded
|
||||
try {
|
||||
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
|
||||
if (driver.isEmbedded()) {
|
||||
enabledIds.add(driverId);
|
||||
}
|
||||
} catch (DBWebException e) {
|
||||
log.error("Failed to find driver by id", e);
|
||||
}
|
||||
}
|
||||
appConfig.setDisabledDrivers(disabledDriversConfig);
|
||||
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected C createServerConfiguration() {
|
||||
return (C) new CBServerConfig();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected I createInputConfiguration(@NotNull Map<String, Object> params) {
|
||||
return (I) new AdminServerConfig(params);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server;
|
||||
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
|
||||
public record CBServerConfigurations(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
@NotNull CBAppConfig appConfig,
|
||||
@Nullable String adminName,
|
||||
@Nullable String adminPassword
|
||||
) {
|
||||
}
|
||||
+40
-4
@@ -202,6 +202,43 @@ public class CBSessionManager implements WebAppSessionManager {
|
||||
return httpSession.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the current HTTP session, creates a new one, and binds a new {@link WebSession} to it.
|
||||
*/
|
||||
@NotNull
|
||||
public WebSession rotateSession(
|
||||
@NotNull HttpServletRequest request,
|
||||
@NotNull WebSession oldWebSession
|
||||
) throws DBWebException {
|
||||
HttpSession oldHttpSession = request.getSession(false);
|
||||
if (oldHttpSession != null) {
|
||||
oldHttpSession.invalidate();
|
||||
}
|
||||
String newSessionId = request.getSession(true).getId();
|
||||
|
||||
String locale = oldWebSession.getLocale();
|
||||
String remoteAddr = oldWebSession.getLastRemoteAddr();
|
||||
String remoteUserAgent = oldWebSession.getLastRemoteUserAgent();
|
||||
var requestInfo = new WebHttpRequestInfo(newSessionId, locale, remoteAddr, remoteUserAgent);
|
||||
WebSession newWebSession;
|
||||
try {
|
||||
newWebSession = createWebSessionImpl(requestInfo);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException(e);
|
||||
}
|
||||
oldWebSession.migrateEventHandlersTo(newWebSession);
|
||||
oldWebSession.getEventsFilter().migrateTo(newWebSession.getEventsFilter());
|
||||
String oldSessionId = oldWebSession.getSessionId();
|
||||
synchronized (sessionMap) {
|
||||
sessionMap.remove(oldSessionId);
|
||||
sessionMap.put(newSessionId, newWebSession);
|
||||
}
|
||||
oldWebSession.close(false, false);
|
||||
|
||||
log.debug("Session rotated '" + oldSessionId + "' -> '" + newSessionId + "'");
|
||||
return newWebSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns not expired session from cache, or restore it.
|
||||
*
|
||||
@@ -288,13 +325,12 @@ public class CBSessionManager implements WebAppSessionManager {
|
||||
@Nullable
|
||||
public WebSession findWebSession(@NotNull HttpServletRequest request) {
|
||||
String sessionId = getSessionId(request);
|
||||
WebSession webSession;
|
||||
synchronized (sessionMap) {
|
||||
var session = sessionMap.get(sessionId);
|
||||
if (session instanceof WebSession) {
|
||||
return (WebSession) session;
|
||||
}
|
||||
return null;
|
||||
webSession = (session instanceof WebSession) ? (WebSession) session : null;
|
||||
}
|
||||
return webSession;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSDataSourceUpdatedEventHandlerImpl">
|
||||
<topic id="cb_datasource"/>
|
||||
</eventHandler>
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSNetworkProfileEventHandler">
|
||||
<topic id="cb_profiles"/>
|
||||
</eventHandler>
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSFolderUpdatedEventHandlerImpl">
|
||||
<topic id="cb_datasource_folder"/>
|
||||
</eventHandler>
|
||||
|
||||
@@ -570,6 +570,8 @@ type ConnectionInfo {
|
||||
|
||||
"List of tools that can be used with this connection. Returns empty list if no tools are available"
|
||||
tools: [String!]! @since(version: "24.1.3")
|
||||
|
||||
connectionType: ID! @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
type ConnectionFolderInfo {
|
||||
@@ -633,6 +635,37 @@ type LogEntry {
|
||||
stackTrace: String
|
||||
}
|
||||
|
||||
type ConnectionType @since(version: "26.1.2") {
|
||||
"Unique identifier of the connection type"
|
||||
id: ID!
|
||||
"Human-readable name of the connection type"
|
||||
name: String!
|
||||
"Description of the connection type"
|
||||
description: String
|
||||
"Color of the connection type (for light theme)"
|
||||
colorLight: String!
|
||||
"Alternative color of the connection type (for dark theme)"
|
||||
colorDark: String
|
||||
"Is autocommit enabled for this connection type"
|
||||
autocommit: Boolean!
|
||||
"Confirm execution of queries for this connection type"
|
||||
confirmExecute: Boolean!
|
||||
"Confirm data change operations (INSERT, UPDATE, DELETE) for this connection type"
|
||||
confirmDataChange: Boolean!
|
||||
"Smart commit mode is enabled for this connection type"
|
||||
smartCommit: Boolean!
|
||||
"Recover smart commit mode is enabled for this connection type"
|
||||
smartCommitRecover: Boolean!
|
||||
"Auto-close transactions is enabled for this connection type"
|
||||
autoCloseTransactions: Boolean!
|
||||
"Auto-close idle transactions period in seconds for this connection type"
|
||||
closeIdleTransactionPeriod: Int!
|
||||
"Auto-close connections is enabled for this connection type"
|
||||
autoCloseConnections: Boolean!
|
||||
"Auto-close idle connections period in seconds for this connection type"
|
||||
closeIdleConnectionPeriod: Int!
|
||||
}
|
||||
|
||||
####################################################
|
||||
# Input types
|
||||
####################################################
|
||||
@@ -745,6 +778,8 @@ input ConnectionConfig {
|
||||
defaultSchemaName: String @since(version: "25.0.5") @deprecated(reason: "25.2.1 use expertPropertyValues instead")
|
||||
"Default user settings for connection"
|
||||
defaultUserPreferences: Object @since(version: "26.0.2")
|
||||
|
||||
connectionType: ID @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
####################################################
|
||||
@@ -790,6 +825,8 @@ extend type Query {
|
||||
|
||||
"Returns connection driver properties based on configuration"
|
||||
listConnectionDriverProperties( projectId: ID!, config: ConnectionConfig! ): [ObjectPropertyInfo!]!
|
||||
|
||||
listConnectionTypes: [ConnectionType!]! @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
||||
@@ -68,19 +68,39 @@ input SQLDataFilter {
|
||||
limit: Int
|
||||
|
||||
constraints: [ SQLDataFilterConstraint ]
|
||||
"When true constraints are combined with OR instead of AND"
|
||||
anyConstraint: Boolean
|
||||
where: String
|
||||
orderBy: String
|
||||
}
|
||||
|
||||
type SQLResultReference @since(version: "26.1.1") {
|
||||
"Pair (source-row column index, target-entity column name) used to build a navigation filter"
|
||||
type SQLReferenceColumnMapping @since(version: "26.1.2") {
|
||||
"Position in the result-set bindings"
|
||||
sourceColumnIndex: Int!
|
||||
"Column name in the source result set"
|
||||
sourceColumnName: String!
|
||||
"Ordinal position of the column in the target entity definition"
|
||||
targetColumnIndex: Int!
|
||||
"Column name in the target entity"
|
||||
targetColumnName: String!
|
||||
}
|
||||
|
||||
type SQLResultAssociation @since(version: "26.1.2") {
|
||||
"True for reverse references (incoming FKs)"
|
||||
reference: Boolean!
|
||||
"Name of the association (foreign key)"
|
||||
associationName: String!
|
||||
"Fully qualified name of the entity"
|
||||
"Name of the catalog the target entity belongs to"
|
||||
targetCatalogName: String
|
||||
"Name of the schema the target entity belongs to"
|
||||
targetSchemaName: String
|
||||
"Plain name of the target entity"
|
||||
targetEntityName: String
|
||||
"Navigator node URI of the target entity, suitable for opening it directly in the UI. Null if the entity is not present in the navigator tree"
|
||||
nodePath: String
|
||||
"Indices of result-set columns that participate as the source-side attributes of this reference"
|
||||
columnIndexList: [Int!]!
|
||||
targetNodePath: String
|
||||
"List of column mappings between the source result set and the target entity"
|
||||
columnMapping: [SQLReferenceColumnMapping!]!
|
||||
}
|
||||
|
||||
type SQLResultColumn {
|
||||
@@ -133,11 +153,6 @@ type SQLResultSet {
|
||||
"Returns list of rows in the result set. Each row contains data and metadata"
|
||||
rowsWithMetaData: [SQLResultRowMetaData] @since(version: "23.3.5")
|
||||
|
||||
"Forward foreign keys navigable from this result set"
|
||||
associations: [SQLResultReference!]! @since(version: "26.1.1")
|
||||
"Reverse references (incoming FKs from other entities) navigable from this result set"
|
||||
references: [SQLResultReference!]! @since(version: "26.1.1")
|
||||
|
||||
# True means that resultset was generated by single entity query
|
||||
# New rows can be added, old rows can be deleted
|
||||
singleEntity: Boolean!
|
||||
@@ -328,6 +343,15 @@ type TransactionLogInfos {
|
||||
|
||||
extend type Query {
|
||||
|
||||
"Foreign-key navigation metadata for the given result set. Set isReference to true for reverse references, false for forward associations, or omit it to get both"
|
||||
sqlResultAssociations(
|
||||
projectId: ID,
|
||||
connectionId: ID!,
|
||||
contextId: ID!,
|
||||
resultsId: ID!,
|
||||
isReference: Boolean
|
||||
): [SQLResultAssociation!]! @since(version: "26.1.2")
|
||||
|
||||
"Returns SQL dialect info for the specified connection"
|
||||
sqlDialectInfo( projectId: ID, connectionId: ID! ): SQLDialectInfo
|
||||
|
||||
@@ -467,19 +491,6 @@ extend type Mutation {
|
||||
dataFormat: ResultDataFormat
|
||||
): AsyncTaskInfo!
|
||||
|
||||
"Creates async task for navigating a foreign-key reference exposed in SQLResultSet.references. Pass associationName, isReference and any one of the chosen reference's columnIndices (used to locate the source entity)."
|
||||
asyncSqlNavigateForeignKey(
|
||||
projectId: ID,
|
||||
connectionId: ID!,
|
||||
contextId: ID!,
|
||||
resultsId: ID!,
|
||||
row: SQLResultRow!,
|
||||
columnIndex: Int!,
|
||||
associationName: String!,
|
||||
isReference: Boolean!,
|
||||
dataFormat: ResultDataFormat
|
||||
): AsyncTaskInfo! @since(version: "26.1.1")
|
||||
|
||||
"Returns transaction log info for the specified project, connection and context"
|
||||
getTransactionLogInfo(
|
||||
projectId: ID!,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server.events;
|
||||
|
||||
import io.cloudbeaver.WebSessionProjectImpl;
|
||||
import io.cloudbeaver.model.session.BaseWebSession;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
|
||||
public class WSNetworkProfileEventHandler extends WSDefaultEventHandler<WSNetworkProfileEvent> {
|
||||
private static final Log log = Log.getLog(WSNetworkProfileEventHandler.class);
|
||||
|
||||
@Override
|
||||
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
|
||||
if (event.getProjectId() != null) {
|
||||
if (activeUserSession instanceof WebSession webSession) {
|
||||
WebSessionProjectImpl project = webSession.getProjectById(event.getProjectId());
|
||||
if (project == null) {
|
||||
log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId());
|
||||
return;
|
||||
}
|
||||
project.getDataSourceRegistry().getNetworkProfiles().reloadProfiles();
|
||||
} else {
|
||||
super.updateSessionData(activeUserSession, event);
|
||||
}
|
||||
} else {
|
||||
DBWorkbench.getPlatform().getNetworkProfiles().reloadProfiles();
|
||||
super.updateSessionData(activeUserSession, event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
|
||||
if (event.getProjectId() != null && !activeUserSession.isProjectAccessible(event.getProjectId())) {
|
||||
return false;
|
||||
}
|
||||
return super.isAcceptableInSession(activeUserSession, event);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -33,12 +33,16 @@ public class CBClientEventProcessor {
|
||||
|
||||
private static final Log log = Log.getLog(CBClientEventProcessor.class);
|
||||
|
||||
final BaseWebSession webSession;
|
||||
volatile BaseWebSession webSession;
|
||||
|
||||
public CBClientEventProcessor(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
void setWebSession(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
public void process(@Nullable String message) {
|
||||
if (CommonUtils.isEmpty(message)) {
|
||||
return;
|
||||
|
||||
+7
-1
@@ -40,7 +40,7 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
|
||||
|
||||
private static final int QUEUE_CAPACITY = 1000;
|
||||
|
||||
private final BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
private final BlockingQueue<WSEvent> queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
|
||||
private final CBClientEventProcessor processor;
|
||||
private volatile long lastPoll;
|
||||
@@ -88,6 +88,12 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateToSession(@NotNull BaseWebSession newSession) {
|
||||
this.webSession = newSession;
|
||||
this.processor.setWebSession(newSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleWebSessionEvent(@NotNull WSEvent event) {
|
||||
if (!queue.offer(event)) {
|
||||
|
||||
+25
-3
@@ -32,7 +32,11 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
|
||||
private static final Log log = Log.getLog(CBEventsWebSocket.class);
|
||||
|
||||
@Nullable
|
||||
private BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
@Nullable
|
||||
private FromUserEventHandler eventProcessor;
|
||||
@Nullable
|
||||
private WebSocketPingPongCallback pingPongCallback;
|
||||
|
||||
@Override
|
||||
public void onOpen(Session session, EndpointConfig config) {
|
||||
@@ -48,13 +52,31 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
|
||||
log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session");
|
||||
|
||||
session.setMaxIdleTimeout(Duration.ofMinutes(5).toMillis());
|
||||
session.addMessageHandler(String.class, new FromUserEventHandler(webSession));
|
||||
session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession));
|
||||
this.eventProcessor = new FromUserEventHandler(webSession);
|
||||
session.addMessageHandler(String.class, eventProcessor);
|
||||
this.pingPongCallback = new WebSocketPingPongCallback(webSession);
|
||||
session.addMessageHandler(PongMessage.class, pingPongCallback);
|
||||
|
||||
CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateToSession(@NotNull BaseWebSession newSession) {
|
||||
BaseWebSession oldSession = this.webSession;
|
||||
this.webSession = newSession;
|
||||
if (eventProcessor != null) {
|
||||
eventProcessor.setWebSession(newSession);
|
||||
}
|
||||
if (pingPongCallback != null) {
|
||||
pingPongCallback.setWebSession(newSession);
|
||||
}
|
||||
if (oldSession != null) {
|
||||
CBJettyWebSocketManager.migrateWebSocket(oldSession.getSessionId(), newSession.getSessionId(), this);
|
||||
}
|
||||
log.debug("EventWebSocket migrated to the " + newSession.getSessionId() + " session");
|
||||
}
|
||||
|
||||
private static class FromUserEventHandler extends CBClientEventProcessor implements MessageHandler.Whole<String> {
|
||||
|
||||
private FromUserEventHandler(@NotNull BaseWebSession webSession) {
|
||||
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -39,6 +39,25 @@ public class CBJettyWebSocketManager {
|
||||
socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-keys a live web socket from the old session id to the new one after a session rotation,
|
||||
* so that keep-alive pings keep being delivered to it.
|
||||
*/
|
||||
public static void migrateWebSocket(
|
||||
@NotNull String oldSessionId,
|
||||
@NotNull String newSessionId,
|
||||
@NotNull CBEventsWebSocket webSocket
|
||||
) {
|
||||
List<CBEventsWebSocket> oldSockets = socketBySessionId.get(oldSessionId);
|
||||
if (oldSockets != null) {
|
||||
oldSockets.remove(webSocket);
|
||||
if (oldSockets.isEmpty()) {
|
||||
socketBySessionId.remove(oldSessionId);
|
||||
}
|
||||
}
|
||||
registerWebSocket(newSessionId, webSocket);
|
||||
}
|
||||
|
||||
public static void sendPing() {
|
||||
//remove expired sessions
|
||||
WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager();
|
||||
|
||||
+5
-1
@@ -24,12 +24,16 @@ import org.jkiss.code.NotNull;
|
||||
|
||||
public class WebSocketPingPongCallback implements MessageHandler.Whole<PongMessage> {
|
||||
@NotNull
|
||||
private final BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
|
||||
public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
void setWebSession(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(PongMessage message) {
|
||||
if (webSession instanceof WebHeadlessSession) {
|
||||
|
||||
+13
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -71,7 +71,7 @@ public abstract class WebServiceServletBase extends HttpServlet {
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
errorMessage = e.getMessage();
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + errorMessage);
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + flattenForResponse(errorMessage));
|
||||
} finally {
|
||||
if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) {
|
||||
apiCallInterceptor.onApiCallEvent(
|
||||
@@ -83,6 +83,17 @@ public abstract class WebServiceServletBase extends HttpServlet {
|
||||
|
||||
protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException;
|
||||
|
||||
/**
|
||||
* Replaces control characters (line breaks, tabs, etc.) with spaces so the message can be safely
|
||||
* embedded into the JSON error body produced by the servlet container's error handler.
|
||||
*/
|
||||
private String flattenForResponse(String message) {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
return message.replaceAll("\\p{Cntrl}+", " ").trim();
|
||||
}
|
||||
|
||||
protected Map<String, Object> getVariables(HttpServletRequest request) {
|
||||
return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE);
|
||||
}
|
||||
|
||||
+4
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.rm.RMConstants;
|
||||
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
|
||||
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
|
||||
@@ -115,6 +116,9 @@ public interface DBWServiceCore extends DBWService {
|
||||
@NotNull Map<String, Object> connectionConfig
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id);
|
||||
|
||||
@WebAction
|
||||
WebConnectionInfo getConnectionState(WebSession webSession, @Nullable String projectId, String connectionId) throws DBWebException;
|
||||
|
||||
|
||||
+5
@@ -87,6 +87,11 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
|
||||
getArgumentVal(env, "projectId"),
|
||||
getArgumentVal(env, "config")
|
||||
)
|
||||
).dataFetcher(
|
||||
"listConnectionTypes", env -> getService(env).getConnectionTypes(
|
||||
getWebSession(env),
|
||||
getArgument(env, "id")
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
|
||||
+24
-8
@@ -45,6 +45,7 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.SMObjectType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.exec.DBCConnectException;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNDataSource;
|
||||
@@ -310,17 +311,32 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
|
||||
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
|
||||
webSession, projectId, configInput.getConnectionId());
|
||||
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
|
||||
DBPConnectionConfiguration connectionConfiguration = new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
|
||||
return WebServiceUtils.getDriverProperties(
|
||||
webSession,
|
||||
testDataSource.getDriver(),
|
||||
testDataSource,
|
||||
connectionConfiguration
|
||||
);
|
||||
try {
|
||||
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
|
||||
DBPConnectionConfiguration connectionConfiguration
|
||||
= new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
|
||||
return WebServiceUtils.getDriverProperties(
|
||||
webSession,
|
||||
testDataSource.getDriver(),
|
||||
testDataSource,
|
||||
connectionConfiguration
|
||||
);
|
||||
} catch (DBWebException e) {
|
||||
log.error("Error getting driver properties", e);
|
||||
return new WebPropertyInfo[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id) {
|
||||
return DataSourceProviderRegistry.getInstance().getConnectionTypes().stream()
|
||||
.map(ct -> id == null || id.equals(ct.getId()) ? ct : null)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebConnectionInfo initConnection(
|
||||
|
||||
+3
-6
@@ -127,15 +127,12 @@ public interface DBWServiceSQL extends DBWService {
|
||||
@Nullable WebDataFormat dataFormat) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
WebAsyncTaskInfo asyncNavigateForeignKey(
|
||||
List<WebSQLQueryResultAssociation> getSqlResultAssociations(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
@NotNull String resultsId,
|
||||
@NotNull WebSQLResultsRow row,
|
||||
int columnIndex,
|
||||
@NotNull String associationName,
|
||||
boolean isReference,
|
||||
@Nullable WebDataFormat dataFormat) throws DBException;
|
||||
@Nullable Boolean isReference
|
||||
) throws DBException;
|
||||
|
||||
/**
|
||||
* Reads dynamic trace from provided database results.
|
||||
|
||||
+42
-3
@@ -41,6 +41,7 @@ public class WebSQLDataFilter {
|
||||
private int offset;
|
||||
private int limit;
|
||||
private String where;
|
||||
private boolean anyConstraint;
|
||||
private final List<WebSQLDataFilterConstraint> constraints = new ArrayList<>();
|
||||
|
||||
public WebSQLDataFilter() {
|
||||
@@ -57,6 +58,7 @@ public class WebSQLDataFilter {
|
||||
this.limit = MAX_ROWS_NUMBER;
|
||||
}
|
||||
this.where = CommonUtils.toString(filterProps.get("where"), null);
|
||||
this.anyConstraint = CommonUtils.toBoolean(filterProps.get("anyConstraint"));
|
||||
Object constraints = filterProps.get("constraints");
|
||||
if (constraints instanceof Collection) {
|
||||
for (Object constrItem : (Collection<?>) constraints) {
|
||||
@@ -92,10 +94,18 @@ public class WebSQLDataFilter {
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* When true constraints are combined with OR instead of AND
|
||||
*/
|
||||
public boolean isAnyConstraint() {
|
||||
return anyConstraint;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLDataFilter from(@NotNull DBDDataFilter filter) {
|
||||
var webFilter = new WebSQLDataFilter();
|
||||
webFilter.where = filter.getWhere();
|
||||
webFilter.anyConstraint = filter.isAnyConstraint();
|
||||
for (DBDAttributeConstraint constraint : filter.getConstraints()) {
|
||||
webFilter.constraints.add(WebSQLDataFilterConstraint.from(constraint));
|
||||
}
|
||||
@@ -106,6 +116,7 @@ public class WebSQLDataFilter {
|
||||
public DBDDataFilter makeDataFilter(@Nullable WebSQLResultsInfo resultInfo) throws DBException {
|
||||
DBDDataFilter dataFilter = new DBDDataFilter();
|
||||
dataFilter.setWhere(where);
|
||||
dataFilter.setAnyConstraint(anyConstraint);
|
||||
if (CommonUtils.isEmpty(constraints)) {
|
||||
return dataFilter;
|
||||
}
|
||||
@@ -139,14 +150,42 @@ public class WebSQLDataFilter {
|
||||
|
||||
private void fillEmptyConstrains(@NotNull List<DBDAttributeConstraint> emptyConstraints) throws DBException {
|
||||
for (WebSQLDataFilterConstraint webConstr : constraints) {
|
||||
if (webConstr.getAttributePosition() >= emptyConstraints.size()) {
|
||||
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", webConstr.getAttributePosition()));
|
||||
DBDAttributeConstraint dbConstr = findConstraintToFill(emptyConstraints, webConstr);
|
||||
if (dbConstr.hasCondition()) {
|
||||
// The attribute already has a condition from a previous constraint
|
||||
dbConstr = new DBDAttributeConstraint(dbConstr);
|
||||
dbConstr.reset();
|
||||
emptyConstraints.add(dbConstr);
|
||||
}
|
||||
DBDAttributeConstraint dbConstr = emptyConstraints.get(webConstr.getAttributePosition());
|
||||
fillEmptyConstraint(dbConstr, webConstr);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBDAttributeConstraint findConstraintToFill(
|
||||
@NotNull List<DBDAttributeConstraint> emptyConstraints,
|
||||
@NotNull WebSQLDataFilterConstraint webConstr
|
||||
) throws DBException {
|
||||
Integer attributePosition = webConstr.getAttributePosition();
|
||||
if (attributePosition != null) {
|
||||
if (attributePosition < 0 || attributePosition >= emptyConstraints.size()) {
|
||||
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", attributePosition));
|
||||
}
|
||||
return emptyConstraints.get(attributePosition);
|
||||
}
|
||||
// try by attribute name
|
||||
String attributeName = webConstr.getAttributeName();
|
||||
if (!CommonUtils.isEmpty(attributeName)) {
|
||||
for (DBDAttributeConstraint dbConstr : emptyConstraints) {
|
||||
if (CommonUtils.equalObjects(dbConstr.getAttributeName(), attributeName)) {
|
||||
return dbConstr;
|
||||
}
|
||||
}
|
||||
throw new DBException(MessageFormat.format("Constraint attribute ''{0}'' not found in result set", attributeName));
|
||||
}
|
||||
throw new DBException("Constraint must specify either attributePosition or attributeName");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBDAttributeConstraint fillEmptyConstraint(@NotNull DBDAttributeConstraint dbConstr,
|
||||
@NotNull WebSQLDataFilterConstraint webConstr) {
|
||||
|
||||
+18
-5
@@ -479,7 +479,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
sendTransactionalEvent(contextInfo);
|
||||
}
|
||||
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
|
||||
updatedResultSet.setResultsInfo(resultsInfo);
|
||||
updatedResultSet.setColumns(resultsInfo.getAttributes());
|
||||
|
||||
@@ -651,7 +651,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
DBDAttributeBinding[] allAttributes = resultsInfo.getAttributes();
|
||||
DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]);
|
||||
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
|
||||
updatedResultSet.setResultsInfo(resultsInfo);
|
||||
updatedResultSet.setColumns(resultsInfo.getAttributes());
|
||||
|
||||
@@ -1131,7 +1131,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
@NotNull WebSQLExecuteInfo executeInfo,
|
||||
@NotNull DBDDataFilter filter
|
||||
) throws DBException {
|
||||
if (!filter.getConstraints().isEmpty() || !CommonUtils.isEmpty(filter.getWhere())) {
|
||||
if (filter.getConstraintsCount() > 0 || !CommonUtils.isEmpty(filter.getWhere())) {
|
||||
StringBuilder where = new StringBuilder();
|
||||
SQLUtils.appendConditionString(
|
||||
filter,
|
||||
@@ -1268,10 +1268,23 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
throws DBException {
|
||||
if (cellRow instanceof Map<?, ?> variables) {
|
||||
if (variables.get(FILE_ID) != null) {
|
||||
Path path = WebAppUtils.getWebPlatform()
|
||||
String fileId = variables.get(FILE_ID).toString();
|
||||
try {
|
||||
// file id must be UUID
|
||||
UUID.fromString(fileId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DBException("File ID is invalid");
|
||||
}
|
||||
|
||||
Path uploadFolder = WebAppUtils.getWebPlatform()
|
||||
.getTempFolder(webSession.getProgressMonitor(), TEMP_FILE_FOLDER)
|
||||
.resolve(webSession.getSessionId())
|
||||
.resolve(variables.get(FILE_ID).toString());
|
||||
.normalize();
|
||||
Path path = uploadFolder.resolve(fileId).normalize();
|
||||
// ensure the resolved path stays within the session upload directory
|
||||
if (!path.startsWith(uploadFolder)) {
|
||||
throw new DBException("File ID is invalid");
|
||||
}
|
||||
|
||||
try {
|
||||
var file = Files.newInputStream(path);
|
||||
|
||||
+1
-2
@@ -54,7 +54,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
|
||||
private final WebDataFormat dataFormat;
|
||||
@Nullable
|
||||
private final DBDDataFilter dataFilter;
|
||||
private final WebSQLQueryResultSet webResultSet;
|
||||
private final WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet();
|
||||
|
||||
private DBDAttributeBinding[] bindings;
|
||||
private DBCTrace trace;
|
||||
@@ -71,7 +71,6 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
|
||||
this.dataContainer = dataContainer;
|
||||
this.dataFormat = dataFormat;
|
||||
this.dataFilter = dataFilter;
|
||||
this.webResultSet = new WebSQLQueryResultSet(contextInfo.getProcessor().getWebSession());
|
||||
rowLimit = ServletAppUtils.getServletApplication()
|
||||
.getAppConfiguration()
|
||||
.getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT);
|
||||
|
||||
+37
-13
@@ -21,19 +21,20 @@ import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBPEvaluationContext;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNNode;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntity;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntityAssociation;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntityConstraint;
|
||||
import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog;
|
||||
import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WebSQLQueryResultReference {
|
||||
public class WebSQLQueryResultAssociation {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultReference.class);
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultAssociation.class);
|
||||
|
||||
@Nullable
|
||||
private final WebSession session;
|
||||
@@ -41,18 +42,23 @@ public class WebSQLQueryResultReference {
|
||||
private final DBSEntityAssociation association;
|
||||
private final boolean reverse;
|
||||
@NotNull
|
||||
private final List<Integer> columnIndexList;
|
||||
private final List<WebSQLReferenceColumnMapping> columnMapping;
|
||||
|
||||
public WebSQLQueryResultReference(
|
||||
public WebSQLQueryResultAssociation(
|
||||
@Nullable WebSession session,
|
||||
@NotNull DBSEntityAssociation association,
|
||||
boolean reverse,
|
||||
@NotNull List<Integer> columnIndexList
|
||||
@NotNull List<WebSQLReferenceColumnMapping> columnMapping
|
||||
) {
|
||||
this.session = session;
|
||||
this.association = association;
|
||||
this.reverse = reverse;
|
||||
this.columnIndexList = columnIndexList;
|
||||
this.columnMapping = columnMapping;
|
||||
}
|
||||
|
||||
@Property
|
||||
public boolean isReference() {
|
||||
return reverse;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -63,18 +69,36 @@ public class WebSQLQueryResultReference {
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetEntityName() {
|
||||
public String getTargetCatalogName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
if (targetEntity == null) {
|
||||
return null;
|
||||
}
|
||||
return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI);
|
||||
DBSCatalog catalog = DBUtils.getParentOfType(DBSCatalog.class, targetEntity);
|
||||
return catalog == null ? null : catalog.getName();
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getNodePath() {
|
||||
public String getTargetSchemaName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
if (targetEntity == null) {
|
||||
return null;
|
||||
}
|
||||
DBSSchema schema = DBUtils.getParentOfType(DBSSchema.class, targetEntity);
|
||||
return schema == null ? null : schema.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetEntityName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
return targetEntity == null ? null : targetEntity.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetNodePath() {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -94,8 +118,8 @@ public class WebSQLQueryResultReference {
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<Integer> getColumnIndexList() {
|
||||
return columnIndexList;
|
||||
public List<WebSQLReferenceColumnMapping> getColumnMapping() {
|
||||
return columnMapping;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
+1
-24
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -36,10 +35,6 @@ public class WebSQLQueryResultSet {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultSet.class);
|
||||
|
||||
@Nullable
|
||||
private final WebSession session;
|
||||
@Nullable
|
||||
private DBDAttributeBinding[] referencesBindings;
|
||||
private WebSQLQueryResultColumn[] columns;
|
||||
private List<WebSQLQueryResultSetRow> rows = Collections.emptyList();
|
||||
private boolean hasMoreData;
|
||||
@@ -55,8 +50,7 @@ public class WebSQLQueryResultSet {
|
||||
private boolean readOnly;
|
||||
private String readOnlyStatus;
|
||||
|
||||
public WebSQLQueryResultSet(@Nullable WebSession session) {
|
||||
this.session = session;
|
||||
public WebSQLQueryResultSet() {
|
||||
}
|
||||
|
||||
@Property
|
||||
@@ -79,23 +73,6 @@ public class WebSQLQueryResultSet {
|
||||
columns[i] = new WebSQLQueryResultColumn(bindings[i]);
|
||||
}
|
||||
this.columns = columns;
|
||||
this.referencesBindings = bindings;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<WebSQLQueryResultReference> getAssociations() {
|
||||
return session != null && referencesBindings != null
|
||||
? WebSQLUtils.collectAssociations(session, referencesBindings)
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<WebSQLQueryResultReference> getReferences() {
|
||||
return session != null && referencesBindings != null
|
||||
? WebSQLUtils.collectReferences(session, referencesBindings)
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
@Property
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
|
||||
public class WebSQLReferenceColumnMapping {
|
||||
|
||||
private final int sourceColumnIndex;
|
||||
@NotNull
|
||||
private final String sourceColumnName;
|
||||
private final int targetColumnIndex;
|
||||
@NotNull
|
||||
private final String targetColumnName;
|
||||
|
||||
public WebSQLReferenceColumnMapping(
|
||||
int sourceColumnIndex,
|
||||
@NotNull String sourceColumnName,
|
||||
int targetColumnIndex,
|
||||
@NotNull String targetColumnName
|
||||
) {
|
||||
this.sourceColumnIndex = sourceColumnIndex;
|
||||
this.sourceColumnName = sourceColumnName;
|
||||
this.targetColumnIndex = targetColumnIndex;
|
||||
this.targetColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
@Property
|
||||
public int getSourceColumnIndex() {
|
||||
return sourceColumnIndex;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getSourceColumnName() {
|
||||
return sourceColumnName;
|
||||
}
|
||||
|
||||
@Property
|
||||
public int getTargetColumnIndex() {
|
||||
return targetColumnIndex;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getTargetColumnName() {
|
||||
return targetColumnName;
|
||||
}
|
||||
}
|
||||
+42
-31
@@ -331,23 +331,23 @@ public class WebSQLUtils {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<WebSQLQueryResultReference> collectAssociations(
|
||||
public static List<WebSQLQueryResultAssociation> collectAssociations(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings
|
||||
) {
|
||||
return collectWebSQLQueryResultReference(session, bindings, false);
|
||||
return collectWebSQLQueryResultAssociation(session, bindings, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<WebSQLQueryResultReference> collectReferences(
|
||||
public static List<WebSQLQueryResultAssociation> collectReferences(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings
|
||||
) {
|
||||
return collectWebSQLQueryResultReference(session, bindings, true);
|
||||
return collectWebSQLQueryResultAssociation(session, bindings, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<WebSQLQueryResultReference> collectWebSQLQueryResultReference(
|
||||
private static List<WebSQLQueryResultAssociation> collectWebSQLQueryResultAssociation(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings,
|
||||
boolean reverse
|
||||
@@ -369,7 +369,7 @@ public class WebSQLUtils {
|
||||
return idx == null ? null : bindings[idx];
|
||||
};
|
||||
|
||||
List<WebSQLQueryResultReference> result = new ArrayList<>();
|
||||
List<WebSQLQueryResultAssociation> result = new ArrayList<>();
|
||||
DBRProgressMonitor monitor = session.getProgressMonitor();
|
||||
for (DBSEntity entity : entities) {
|
||||
try {
|
||||
@@ -377,9 +377,9 @@ public class WebSQLUtils {
|
||||
? DBStructUtils.readReferences(monitor, entity, attrToBinding)
|
||||
: DBStructUtils.readAssociations(monitor, entity, attrToBinding);
|
||||
for (DBSEntityAssociation association : source) {
|
||||
List<Integer> columnIndex = collectOwnColumnIndex(monitor, association, reverse, attrToIndex);
|
||||
if (columnIndex != null) {
|
||||
result.add(new WebSQLQueryResultReference(session, association, reverse, columnIndex));
|
||||
List<WebSQLReferenceColumnMapping> mapping = collectColumnMapping(monitor, association, reverse, attrToIndex);
|
||||
if (mapping != null) {
|
||||
result.add(new WebSQLQueryResultAssociation(session, association, reverse, mapping));
|
||||
}
|
||||
}
|
||||
} catch (DBException e) {
|
||||
@@ -389,38 +389,49 @@ public class WebSQLUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs source-side attributes (in our result set) with target-side attributes
|
||||
*/
|
||||
@Nullable
|
||||
private static List<Integer> collectOwnColumnIndex(
|
||||
private static List<WebSQLReferenceColumnMapping> collectColumnMapping(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull DBSEntityAssociation association,
|
||||
boolean reverse,
|
||||
@NotNull Map<DBSEntityAttribute, Integer> attrToIndex
|
||||
) throws DBException {
|
||||
DBSEntityReferrer ownSide;
|
||||
if (reverse) {
|
||||
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
|
||||
if (!(refConstraint instanceof DBSEntityReferrer referrer)) {
|
||||
return null;
|
||||
}
|
||||
ownSide = referrer;
|
||||
} else {
|
||||
if (!(association instanceof DBSEntityReferrer associationRef)) {
|
||||
return null;
|
||||
}
|
||||
ownSide = associationRef;
|
||||
}
|
||||
List<? extends DBSEntityAttributeRef> attrs = ownSide.getAttributeReferences(monitor);
|
||||
if (attrs == null || attrs.isEmpty()) {
|
||||
if (!(association instanceof DBSEntityReferrer associationReferrer)) {
|
||||
return null;
|
||||
}
|
||||
List<Integer> indexList = new ArrayList<>(attrs.size());
|
||||
for (DBSEntityAttributeRef attrRef : attrs) {
|
||||
Integer idx = attrToIndex.get(attrRef.getAttribute());
|
||||
if (idx == null) {
|
||||
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
|
||||
if (!(refConstraint instanceof DBSEntityReferrer constraintReferrer)) {
|
||||
return null;
|
||||
}
|
||||
DBSEntityReferrer sourceSide = reverse ? constraintReferrer : associationReferrer;
|
||||
DBSEntityReferrer targetSide = reverse ? associationReferrer : constraintReferrer;
|
||||
|
||||
List<? extends DBSEntityAttributeRef> sourceAttrs = sourceSide.getAttributeReferences(monitor);
|
||||
List<? extends DBSEntityAttributeRef> targetAttrs = targetSide.getAttributeReferences(monitor);
|
||||
if (sourceAttrs == null || targetAttrs == null
|
||||
|| sourceAttrs.isEmpty() || sourceAttrs.size() != targetAttrs.size()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
List<WebSQLReferenceColumnMapping> mapping = new ArrayList<>(sourceAttrs.size());
|
||||
for (int i = 0; i < sourceAttrs.size(); i++) {
|
||||
DBSEntityAttribute sourceAttr = sourceAttrs.get(i).getAttribute();
|
||||
Integer sourceIdx = sourceAttr == null ? null : attrToIndex.get(sourceAttr);
|
||||
if (sourceIdx == null) {
|
||||
return null;
|
||||
}
|
||||
indexList.add(idx);
|
||||
DBSEntityAttribute targetAttr = targetAttrs.get(i).getAttribute();
|
||||
if (targetAttr == null) {
|
||||
return null;
|
||||
}
|
||||
// getOrdinalPosition() is 1-based. Normalize to 0-based to match sourceColumnIndex
|
||||
int targetIdx = targetAttr.getOrdinalPosition() - 1;
|
||||
mapping.add(new WebSQLReferenceColumnMapping(
|
||||
sourceIdx, sourceAttr.getName(), targetIdx, targetAttr.getName()));
|
||||
}
|
||||
return indexList;
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -54,6 +54,14 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
.dataFetcher("sqlDialectInfo", env ->
|
||||
getService(env).getDialectInfo(getWebConnection(env))
|
||||
)
|
||||
.dataFetcher("sqlResultAssociations", env ->
|
||||
getService(env).getSqlResultAssociations(
|
||||
getWebSession(env),
|
||||
getSQLContext(env),
|
||||
getArgumentVal(env, "resultsId"),
|
||||
getArgument(env, "isReference")
|
||||
)
|
||||
)
|
||||
.dataFetcher("sqlListContexts", env ->
|
||||
getService(env).listContexts(getWebSession(env),
|
||||
getProjectReference(env),
|
||||
@@ -232,17 +240,6 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
getDataFilter(env),
|
||||
getDataFormat(env)
|
||||
))
|
||||
.dataFetcher("asyncSqlNavigateForeignKey", env ->
|
||||
getService(env).asyncNavigateForeignKey(
|
||||
getWebSession(env),
|
||||
getSQLContext(env),
|
||||
getArgumentVal(env, "resultsId"),
|
||||
new WebSQLResultsRow(getArgument(env, "row")),
|
||||
getArgumentVal(env, "columnIndex"),
|
||||
getArgumentVal(env, "associationName"),
|
||||
CommonUtils.toBoolean(env.getArgument("isReference")),
|
||||
getDataFormat(env)
|
||||
))
|
||||
.dataFetcher("asyncSqlExecuteResults", env ->
|
||||
getService(env).asyncGetQueryResults(
|
||||
getWebSession(env), getArgumentVal(env, "taskId")
|
||||
|
||||
+16
-63
@@ -36,8 +36,6 @@ import org.jkiss.dbeaver.model.DBPDataSource;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.data.DBDAttributeBinding;
|
||||
import org.jkiss.dbeaver.model.data.DBDReferenceNavigation;
|
||||
import org.jkiss.dbeaver.model.data.DBDReferenceUtils;
|
||||
import org.jkiss.dbeaver.model.exec.DBCException;
|
||||
import org.jkiss.dbeaver.model.exec.DBCLogicalOperator;
|
||||
import org.jkiss.dbeaver.model.exec.DBExecUtils;
|
||||
@@ -61,7 +59,9 @@ import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLCompletionProposalComparator;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionAnalyzer;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionContext;
|
||||
import org.jkiss.dbeaver.model.struct.*;
|
||||
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
|
||||
import org.jkiss.dbeaver.model.struct.DBSObject;
|
||||
import org.jkiss.dbeaver.model.struct.DBSWrapper;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -603,72 +603,25 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("Read data from container " + nodePath, runnable);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WebAsyncTaskInfo asyncNavigateForeignKey(
|
||||
public List<WebSQLQueryResultAssociation> getSqlResultAssociations(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
@NotNull String resultsId,
|
||||
@NotNull WebSQLResultsRow row,
|
||||
int columnIndex,
|
||||
@NotNull String associationName,
|
||||
boolean isReference,
|
||||
@Nullable WebDataFormat dataFormat
|
||||
) {
|
||||
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<>() {
|
||||
@Override
|
||||
public void run(DBRProgressMonitor monitor) throws InvocationTargetException {
|
||||
try {
|
||||
monitor.beginTask("Navigate foreign key", 1);
|
||||
WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId);
|
||||
DBSEntity sourceEntity = resolveSourceEntity(resultsInfo.getAttributes(), columnIndex);
|
||||
DBSEntityAssociation association = isReference
|
||||
? DBStructUtils.findReverseAssociationByName(monitor, sourceEntity, associationName)
|
||||
: DBStructUtils.findForwardAssociationByName(monitor, sourceEntity, associationName);
|
||||
|
||||
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(
|
||||
resultsId,
|
||||
contextInfo,
|
||||
List.of(row)
|
||||
);
|
||||
DBDReferenceNavigation navigation = isReference
|
||||
? DBDReferenceUtils.resolveReferenceNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows())
|
||||
: DBDReferenceUtils.resolveAssociationNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows());
|
||||
if (!(navigation.getTargetEntity() instanceof DBSDataContainer targetDataContainer)) {
|
||||
throw new DBWebException("Referenced entity '" + navigation.getTargetEntity().getName() + "' is not a data container");
|
||||
}
|
||||
WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer(
|
||||
contextInfo,
|
||||
monitor,
|
||||
targetDataContainer,
|
||||
null,
|
||||
WebSQLDataFilter.from(navigation.getTargetFilter()),
|
||||
dataFormat
|
||||
);
|
||||
this.result = executeResults.getStatusMessage();
|
||||
this.extendedResults = executeResults;
|
||||
} catch (Throwable e) {
|
||||
throw new InvocationTargetException(e);
|
||||
} finally {
|
||||
monitor.done();
|
||||
}
|
||||
}
|
||||
};
|
||||
return webSession.createAndRunAsyncTask("Navigate foreign key from results " + resultsId, runnable);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBSEntity resolveSourceEntity(
|
||||
@NotNull DBDAttributeBinding[] attributes,
|
||||
int columnIndex
|
||||
@Nullable Boolean isReference
|
||||
) throws DBException {
|
||||
if (columnIndex < 0 || columnIndex >= attributes.length) {
|
||||
throw new DBWebException("Column index '" + columnIndex + "' is out of range");
|
||||
DBDAttributeBinding[] attributes = contextInfo.getResults(resultsId).getAttributes();
|
||||
if (isReference == null) {
|
||||
// Both forward associations and reverse references
|
||||
List<WebSQLQueryResultAssociation> associations =
|
||||
new ArrayList<>(WebSQLUtils.collectAssociations(webSession, attributes));
|
||||
associations.addAll(WebSQLUtils.collectReferences(webSession, attributes));
|
||||
return associations;
|
||||
}
|
||||
DBSEntityAttribute entityAttribute = attributes[columnIndex].getEntityAttribute();
|
||||
if (entityAttribute == null) {
|
||||
throw new DBException("Column [" + attributes[columnIndex].getName() + "] is not bound to any entity");
|
||||
}
|
||||
return entityAttribute.getParentObject();
|
||||
return isReference
|
||||
? WebSQLUtils.collectReferences(webSession, attributes)
|
||||
: WebSQLUtils.collectAssociations(webSession, attributes);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+11
-96
@@ -19,20 +19,15 @@ package io.cloudbeaver.service.admin.impl;
|
||||
import io.cloudbeaver.*;
|
||||
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
|
||||
import io.cloudbeaver.model.WebPropertyInfo;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebUser;
|
||||
import io.cloudbeaver.registry.*;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import io.cloudbeaver.server.WebAppUtils;
|
||||
import io.cloudbeaver.server.*;
|
||||
import io.cloudbeaver.service.DBWServiceServerConfigurator;
|
||||
import io.cloudbeaver.service.admin.*;
|
||||
import io.cloudbeaver.service.security.SMUtils;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -44,7 +39,6 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthConfiguration;
|
||||
import org.jkiss.dbeaver.model.auth.SMObjectType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
|
||||
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
|
||||
import org.jkiss.dbeaver.model.rm.RMProjectType;
|
||||
@@ -596,50 +590,12 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
@Override
|
||||
public boolean configureServer(@NotNull WebSession webSession, @NotNull Map<String, Object> params) throws DBWebException {
|
||||
try {
|
||||
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
|
||||
CBServerConfig serverConfig = new CBServerConfig();
|
||||
serverConfig.setServerName(CBApplication.getInstance().getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
|
||||
String adminName = null;
|
||||
String adminPassword = null;
|
||||
|
||||
if (!params.isEmpty()) { // FE can send an empty configuration
|
||||
var config = new AdminServerConfig(params);
|
||||
appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled());
|
||||
appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled());
|
||||
appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled());
|
||||
appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled());
|
||||
updateDisabledFeaturesConfig(appConfig, config.getEnabledFeatures());
|
||||
// custom logic for enabling embedded drivers
|
||||
updateDisabledDriversConfig(appConfig, config.getDisabledDrivers());
|
||||
appConfig.setResourceManagerEnabled(config.isResourceManagerEnabled());
|
||||
appConfig.setSecretManagerEnabled(config.isSecretManagerEnabled());
|
||||
|
||||
if (CommonUtils.isEmpty(config.getEnabledAuthProviders())) {
|
||||
// All of them
|
||||
appConfig.setEnabledAuthProviders(new String[0]);
|
||||
} else {
|
||||
appConfig.setEnabledAuthProviders(config.getEnabledAuthProviders().toArray(new String[0]));
|
||||
}
|
||||
|
||||
appConfig.setDefaultNavigatorSettings(
|
||||
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
|
||||
|
||||
adminName = config.getAdminName();
|
||||
adminPassword = config.getAdminPassword();
|
||||
serverConfig.setServerName(config.getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(config.getSessionExpireTime());
|
||||
if (config.getForceHttps() != null) {
|
||||
serverConfig.setForceHttps(config.getForceHttps());
|
||||
}
|
||||
if (config.getSupportedHosts() != null) {
|
||||
serverConfig.setSupportedHosts(config.getSupportedHosts());
|
||||
}
|
||||
if (config.getBindSessionToIp() != null) {
|
||||
serverConfig.setBindSessionToIp(config.getBindSessionToIp());
|
||||
}
|
||||
}
|
||||
|
||||
CBServerConfigurationMapper<?, ?> mapper = CBApplication.getInstance()
|
||||
.getServerConfigurationController().createServerConfigurationInputMapper();
|
||||
CBServerConfigurations configurations = mapper.loadServerConfigurationsFromParams(params);
|
||||
String adminName = configurations.adminName();
|
||||
String adminPassword = configurations.adminPassword();
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
// Grant admin permissions to the current user
|
||||
WebUser curUser = webSession.getUser();
|
||||
@@ -651,7 +607,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
// Try to get admin name from existing authentications (first one)
|
||||
if (!authInfoList.isEmpty()) {
|
||||
adminName = authInfoList.get(0).getUserId();
|
||||
adminName = authInfoList.getFirst().getUserId();
|
||||
}
|
||||
}
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
@@ -666,7 +622,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
// Patch configuration by services
|
||||
for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) {
|
||||
try {
|
||||
wsc.configureServer(CBApplication.getInstance(), webSession, serverConfig, appConfig);
|
||||
wsc.configureServer(CBApplication.getInstance(), webSession, configurations.serverConfig(), configurations.appConfig());
|
||||
} catch (Exception e) {
|
||||
log.warn("Error configuring server by web service " + wsc.getClass().getName(), e);
|
||||
}
|
||||
@@ -678,8 +634,8 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
adminName,
|
||||
adminPassword,
|
||||
authInfos,
|
||||
serverConfig,
|
||||
appConfig,
|
||||
configurations.serverConfig(),
|
||||
configurations.appConfig(),
|
||||
webSession
|
||||
);
|
||||
|
||||
@@ -687,8 +643,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
if (configurationMode) {
|
||||
// In config mode we always refresh because admin user doesn't exist yet
|
||||
webSession.resetUserState();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Just reload session state
|
||||
webSession.refreshUserData();
|
||||
}
|
||||
@@ -700,46 +655,6 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
|
||||
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
|
||||
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
|
||||
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
|
||||
.map(DBWFeatureSet::getId)
|
||||
.filter(id -> !enabledIds.contains(id))
|
||||
.toArray(String[]::new);
|
||||
appConfig.setDisabledFeatures(disabledFeatures);
|
||||
}
|
||||
|
||||
// we disable embedded drivers by default and enable it in enabled drivers list
|
||||
// that's why we need so complicated logic for disabling drivers
|
||||
private void updateDisabledDriversConfig(CBAppConfig appConfig, String[] disabledDriversConfig) {
|
||||
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
|
||||
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
|
||||
|
||||
// remove all disabled embedded drivers from enabled drivers list
|
||||
enabledIds.removeAll(disabledIds);
|
||||
|
||||
// enable embedded driver if it is not in disabled drivers list
|
||||
for (String driverId : appConfig.getDisabledDrivers()) {
|
||||
if (disabledIds.contains(driverId)) {
|
||||
// driver is also disabled
|
||||
continue;
|
||||
}
|
||||
// driver is removed from disabled list
|
||||
// we need to enable if it is embedded
|
||||
try {
|
||||
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
|
||||
if (driver.isEmbedded()) {
|
||||
enabledIds.add(driverId);
|
||||
}
|
||||
} catch (DBWebException e) {
|
||||
log.error("Failed to find driver by id", e);
|
||||
}
|
||||
}
|
||||
appConfig.setDisabledDrivers(disabledDriversConfig);
|
||||
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setDefaultNavigatorSettings(@NotNull WebSession webSession, @NotNull DBNBrowseSettings settings) throws DBWebException {
|
||||
CBApplication.getInstance().getAppConfiguration().setDefaultNavigatorSettings(settings);
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -36,6 +36,7 @@ public interface DBWServiceAuth extends DBWService {
|
||||
|
||||
@WebAction(authRequired = false)
|
||||
WebAuthStatus authLogin(
|
||||
@NotNull HttpServletRequest httpRequest,
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String providerId,
|
||||
@Nullable String providerConfigurationId,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -37,6 +37,7 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
|
||||
public void bindWiring(DBWBindingContext model) {
|
||||
model.getQueryType()
|
||||
.dataFetcher("authLogin", env -> getService(env).authLogin(
|
||||
GraphQLEndpoint.getServletRequestOrThrow(env),
|
||||
getWebSession(env, false),
|
||||
getArgumentVal(env, "provider"),
|
||||
getArgument(env, "configuration"),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
|
||||
+21
-4
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 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.
|
||||
@@ -67,7 +67,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
|
||||
@Override
|
||||
public WebAuthStatus authLogin(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull HttpServletRequest httpRequest,
|
||||
@NotNull WebSession inputWebSession,
|
||||
@NotNull String providerId,
|
||||
@Nullable String providerConfigurationId,
|
||||
@Nullable Map<String, Object> authParameters,
|
||||
@@ -75,6 +76,13 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
boolean forceSessionsLogout
|
||||
) throws DBWebException {
|
||||
try {
|
||||
WebSession webSession = inputWebSession;
|
||||
if (inputWebSession.getUser() == null) {
|
||||
// Rotate anonymous web sessions during login attempts to prevent session fixation attacks.
|
||||
webSession = CBApplication.getInstance().getSessionManager()
|
||||
.rotateSession(httpRequest, inputWebSession);
|
||||
}
|
||||
|
||||
var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout);
|
||||
//TODO deprecated, use asyncAuthLogin for federated auth, exits for backward compatibility
|
||||
linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration()
|
||||
@@ -85,7 +93,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
} else {
|
||||
//run it sync
|
||||
var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser);
|
||||
return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession());
|
||||
List<WebAuthInfo> authInfos = authProcessor.authenticateSession();
|
||||
return new WebAuthStatus(smAuthInfo.getAuthStatus(), authInfos);
|
||||
}
|
||||
} catch (SMTooManySessionsException e) {
|
||||
throw new DBWebException("User authentication failed", e.getErrorType(), e);
|
||||
@@ -95,14 +104,22 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public WebAsyncAuthStatus federatedLogin(
|
||||
@NotNull HttpServletRequest httpRequest,
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSession inputWebSession,
|
||||
@NotNull String providerId,
|
||||
@Nullable String providerConfigurationId,
|
||||
boolean linkWithActiveUser,
|
||||
boolean forceSessionsLogout
|
||||
) throws DBWebException {
|
||||
WebSession webSession = inputWebSession;
|
||||
if (inputWebSession.getUser() == null) {
|
||||
// Rotate anonymous web sessions during login attempts to prevent session fixation attacks.
|
||||
webSession = CBApplication.getInstance().getSessionManager()
|
||||
.rotateSession(httpRequest, inputWebSession);
|
||||
}
|
||||
|
||||
WebAuthProviderDescriptor providerDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(providerId);
|
||||
if (providerDescriptor == null) {
|
||||
throw new DBWebException("Provider '" + providerId + "' not found");
|
||||
|
||||
+11
-4
@@ -128,12 +128,19 @@ public class WebFSServlet extends WebServiceServletBase {
|
||||
if (baseName == null || baseName.toString().isBlank()) {
|
||||
throw new DBException("Invalid file name");
|
||||
}
|
||||
Path normalizedParent = parent.normalize();
|
||||
Path resolved = normalizedParent.resolve(baseName.toString()).normalize();
|
||||
if (!resolved.startsWith(normalizedParent) || resolved.equals(normalizedParent)) {
|
||||
if (submittedFileName.isBlank()
|
||||
|| ".".equals(submittedFileName)
|
||||
|| "..".equals(submittedFileName)
|
||||
|| submittedFileName.indexOf('/') >= 0
|
||||
|| submittedFileName.indexOf('\\') >= 0
|
||||
) {
|
||||
throw new DBException("Invalid file name");
|
||||
}
|
||||
try {
|
||||
return parent.normalize().resolve(submittedFileName).normalize();
|
||||
} catch (InvalidPathException e) {
|
||||
throw new DBException("Invalid file name");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -502,7 +502,7 @@ public class CBDatabase extends InternalDB<WebDatabaseConfig> {
|
||||
}
|
||||
|
||||
public static boolean isDefaultH2Configuration(WebDatabaseConfig databaseConfiguration) {
|
||||
var workspace = ServletAppUtils.getServletApplication().getWorkspaceDirectory();
|
||||
var workspace = ServletAppUtils.getServletApplication().getWorkspacePath();
|
||||
var v1Path = workspace.resolve(".data").resolve(V1_DB_NAME);
|
||||
var v2Path = workspace.resolve(".data").resolve(V2_DB_NAME);
|
||||
var v1DefaultUrl = "jdbc:h2:" + v1Path;
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class ConnectionsTest extends CloudbeaverMockTest {
|
||||
System.out.println("APP:: " + GeneralUtils.getProductTitle());
|
||||
//CBPlatform.setApplication(testApp);
|
||||
|
||||
Path defaultWorkingFolder = DBWorkbench.getPlatform().getApplication().getDefaultWorkingFolder();
|
||||
Path defaultWorkingFolder = DBWorkbench.getPlatform().getApplication().getWorkspacePath();
|
||||
System.out.println("DBeaver application: " + defaultWorkingFolder);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
+115
-71
@@ -21,13 +21,17 @@ import io.cloudbeaver.service.sql.WebSQLProcessor;
|
||||
import io.cloudbeaver.service.sql.WebServiceBindingSQL;
|
||||
import io.cloudbeaver.test.platform.CloudbeaverDBTest;
|
||||
import io.cloudbeaver.test.platform.util.GraphQLTestConstant;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.jkiss.dbeaver.model.exec.DBCLogicalOperator;
|
||||
import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNModel;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNProject;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -35,7 +39,7 @@ import java.util.Map;
|
||||
public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
|
||||
@BeforeEach
|
||||
public void prepareTables() throws SQLException {
|
||||
public void prepareTables() throws Exception {
|
||||
try (JDBCStatement stmt = databaseSession.createStatement()) {
|
||||
Assertions.assertFalse(stmt.execute("DROP TABLE IF EXISTS FK_NAV_ORDER"));
|
||||
Assertions.assertFalse(stmt.execute("DROP TABLE IF EXISTS FK_NAV_CUSTOMER"));
|
||||
@@ -52,11 +56,15 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_ORDER (ID, CUSTOMER_ID) VALUES (10, 1)"));
|
||||
Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_ORDER (ID, CUSTOMER_ID) VALUES (11, 2)"));
|
||||
}
|
||||
DBNModel navigatorModel = webSession.getNavigatorModelOrThrow();
|
||||
DBNProject projectNode = navigatorModel.getRoot().getProjectNode(globalProject);
|
||||
Assertions.assertNotNull(projectNode, "Project navigator node not found");
|
||||
projectNode.getDatabases().getChildren(monitor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadReferencedRowByForeignKeyCell() throws Exception {
|
||||
// Given: an order result set exposing a forward FK from CUSTOMER_ID to FK_NAV_CUSTOMER.
|
||||
// Given
|
||||
WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo);
|
||||
WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId());
|
||||
String taskId = clientWrapper.asyncSqlExecute(
|
||||
@@ -68,42 +76,23 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
clientWrapper.waitTaskCompleted(taskId);
|
||||
|
||||
Map<String, Object> orderResultSet = clientWrapper.readTaskResultSet(taskId);
|
||||
List<Map<String, Object>> associations = JSONUtils.getObjectList(orderResultSet, "associations");
|
||||
Assertions.assertFalse(associations.isEmpty(), "Forward FK is missing on FK_NAV_ORDER result set");
|
||||
Map<String, Object> forwardRef = associations.getFirst();
|
||||
Map<String, Object> forwardRef = fetchFirstReference(sqlProcessorContext, orderResultSet, false);
|
||||
Assertions.assertNotNull(forwardRef, "Forward FK is missing on FK_NAV_ORDER result set");
|
||||
Assertions.assertEquals("FK_NAV_ORDER_CUSTOMER", JSONUtils.getString(forwardRef, "associationName"));
|
||||
Assertions.assertTrue(
|
||||
JSONUtils.getString(forwardRef, "targetEntityName").contains("FK_NAV_CUSTOMER"),
|
||||
"Forward target should be FK_NAV_CUSTOMER"
|
||||
);
|
||||
Assertions.assertEquals("FK_NAV_CUSTOMER", JSONUtils.getString(forwardRef, "targetEntityName"));
|
||||
Assertions.assertEquals("PUBLIC", JSONUtils.getString(forwardRef, "targetSchemaName"));
|
||||
Assertions.assertNotNull(JSONUtils.getString(forwardRef, "targetNodePath"));
|
||||
|
||||
List<Map<String, Object>> orderRows = JSONUtils.getObjectList(orderResultSet, "rowsWithMetaData");
|
||||
Assertions.assertEquals(2, orderRows.size());
|
||||
Map<String, Object> orderRow = orderRows.getFirst();
|
||||
|
||||
// When: navigating from the FK cell value to the referenced customer row.
|
||||
int columnIndex = asIntegerList(forwardRef.get("columnIndexList")).getFirst();
|
||||
Map<String, Object> navigateVars = new HashMap<>();
|
||||
navigateVars.put("projectId", globalProject.getId());
|
||||
navigateVars.put("connectionId", databaseContainer.getId());
|
||||
navigateVars.put("contextId", sqlProcessorContext.getId());
|
||||
navigateVars.put("resultsId", String.valueOf(orderResultSet.get("id")));
|
||||
navigateVars.put("row", orderRow);
|
||||
navigateVars.put("columnIndex", columnIndex);
|
||||
navigateVars.put("associationName", JSONUtils.getString(forwardRef, "associationName"));
|
||||
navigateVars.put("isReference", false);
|
||||
navigateVars.put("dataFormat", null);
|
||||
// When
|
||||
Map<String, Object> customerResultSet = navigateByReference(sqlProcessorContext, forwardRef, orderRow);
|
||||
|
||||
Map<String, Object> navigateResponse = client.sendQuery(GraphQLTestConstant.GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY, navigateVars);
|
||||
Assertions.assertNotNull(navigateResponse);
|
||||
String navigateTaskId = JSONUtils.getString(navigateResponse, "id");
|
||||
Assertions.assertNotNull(navigateTaskId);
|
||||
clientWrapper.waitTaskCompleted(navigateTaskId);
|
||||
|
||||
// Then: the navigation result contains only the referenced customer.
|
||||
Map<String, Object> customerResultSet = clientWrapper.readTaskResultSet(navigateTaskId);
|
||||
// Then
|
||||
List<Map<String, Object>> customerRows = JSONUtils.getObjectList(customerResultSet, "rowsWithMetaData");
|
||||
Assertions.assertEquals(1, customerRows.size());
|
||||
|
||||
List<?> customerData = (List<?>) customerRows.getFirst().get("data");
|
||||
Assertions.assertEquals("1", String.valueOf(customerData.get(0)));
|
||||
Assertions.assertEquals("Alice", customerData.get(1));
|
||||
@@ -111,7 +100,7 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
|
||||
@Test
|
||||
public void shouldExposeReverseReferenceOnReferencedColumn() throws Exception {
|
||||
// Given: a customer result set; FK_NAV_ORDER.CUSTOMER_ID references FK_NAV_CUSTOMER.ID.
|
||||
// Given
|
||||
WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo);
|
||||
WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId());
|
||||
String taskId = clientWrapper.asyncSqlExecute(
|
||||
@@ -124,30 +113,28 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
|
||||
Map<String, Object> customerResultSet = clientWrapper.readTaskResultSet(taskId);
|
||||
|
||||
// Then: result set carries a reverse reference describing the FK from FK_NAV_ORDER,
|
||||
// attached to the PK column (ID at index 0) via columnIndexList.
|
||||
List<Map<String, Object>> references = JSONUtils.getObjectList(customerResultSet, "references");
|
||||
// Then
|
||||
List<Map<String, Object>> references = fetchReferences(sqlProcessorContext, customerResultSet, true);
|
||||
Assertions.assertFalse(references.isEmpty(), "Reverse reference is missing on FK_NAV_CUSTOMER result set");
|
||||
Map<String, Object> reverseRef = references.getFirst();
|
||||
Assertions.assertEquals("FK_NAV_ORDER_CUSTOMER", JSONUtils.getString(reverseRef, "associationName"));
|
||||
String reverseTarget = JSONUtils.getString(reverseRef, "targetEntityName");
|
||||
Assertions.assertNotNull(reverseTarget);
|
||||
Assertions.assertTrue(
|
||||
reverseTarget.contains("FK_NAV_ORDER"),
|
||||
"Reverse reference target should point to the referencing entity FK_NAV_ORDER, got: " + reverseTarget
|
||||
);
|
||||
// Reverse ref targets the PK column ID at index 0
|
||||
Assertions.assertEquals(List.of(0), asIntegerList(reverseRef.get("columnIndexList")));
|
||||
Assertions.assertEquals("FK_NAV_ORDER", JSONUtils.getString(reverseRef, "targetEntityName"));
|
||||
Assertions.assertEquals("PUBLIC", JSONUtils.getString(reverseRef, "targetSchemaName"));
|
||||
|
||||
List<Map<String, Object>> mapping = JSONUtils.getObjectList(reverseRef, "columnMapping");
|
||||
Assertions.assertEquals(1, mapping.size());
|
||||
Assertions.assertEquals(0, ((Number) mapping.getFirst().get("sourceColumnIndex")).intValue());
|
||||
Assertions.assertEquals("CUSTOMER_ID", mapping.getFirst().get("targetColumnName"));
|
||||
|
||||
Assertions.assertTrue(
|
||||
JSONUtils.getObjectList(customerResultSet, "associations").isEmpty(),
|
||||
fetchReferences(sqlProcessorContext, customerResultSet, false).isEmpty(),
|
||||
"FK_NAV_CUSTOMER result set should not expose any forward associations"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNavigateReverseReferenceFromParentRow() throws Exception {
|
||||
// Given: a customer result set positioned on Alice (ID=1).
|
||||
// Given
|
||||
WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo);
|
||||
WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId());
|
||||
String taskId = clientWrapper.asyncSqlExecute(
|
||||
@@ -159,35 +146,17 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
clientWrapper.waitTaskCompleted(taskId);
|
||||
|
||||
Map<String, Object> customerResultSet = clientWrapper.readTaskResultSet(taskId);
|
||||
List<Map<String, Object>> references = JSONUtils.getObjectList(customerResultSet, "references");
|
||||
Assertions.assertFalse(references.isEmpty(), "Reverse reference is missing");
|
||||
Map<String, Object> reverseRef = references.getFirst();
|
||||
Map<String, Object> reverseRef = fetchFirstReference(sqlProcessorContext, customerResultSet, true);
|
||||
Assertions.assertNotNull(reverseRef, "Reverse reference is missing");
|
||||
|
||||
List<Map<String, Object>> customerRows = JSONUtils.getObjectList(customerResultSet, "rowsWithMetaData");
|
||||
Assertions.assertEquals(2, customerRows.size());
|
||||
Map<String, Object> aliceRow = customerRows.getFirst();
|
||||
|
||||
// When: navigating using any one of columnIndexList to identify the source entity.
|
||||
int columnIndex = asIntegerList(reverseRef.get("columnIndexList")).getFirst();
|
||||
Map<String, Object> navigateVars = new HashMap<>();
|
||||
navigateVars.put("projectId", globalProject.getId());
|
||||
navigateVars.put("connectionId", databaseContainer.getId());
|
||||
navigateVars.put("contextId", sqlProcessorContext.getId());
|
||||
navigateVars.put("resultsId", String.valueOf(customerResultSet.get("id")));
|
||||
navigateVars.put("row", aliceRow);
|
||||
navigateVars.put("columnIndex", columnIndex);
|
||||
navigateVars.put("associationName", JSONUtils.getString(reverseRef, "associationName"));
|
||||
navigateVars.put("isReference", true);
|
||||
navigateVars.put("dataFormat", null);
|
||||
// When
|
||||
Map<String, Object> orderResultSet = navigateByReference(sqlProcessorContext, reverseRef, aliceRow);
|
||||
|
||||
Map<String, Object> navigateResponse = client.sendQuery(GraphQLTestConstant.GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY, navigateVars);
|
||||
Assertions.assertNotNull(navigateResponse);
|
||||
String navigateTaskId = JSONUtils.getString(navigateResponse, "id");
|
||||
Assertions.assertNotNull(navigateTaskId);
|
||||
clientWrapper.waitTaskCompleted(navigateTaskId);
|
||||
|
||||
// Then: navigation returns Alice's single order (ID=10, CUSTOMER_ID=1).
|
||||
Map<String, Object> orderResultSet = clientWrapper.readTaskResultSet(navigateTaskId);
|
||||
// Then
|
||||
List<Map<String, Object>> orderRows = JSONUtils.getObjectList(orderResultSet, "rowsWithMetaData");
|
||||
Assertions.assertEquals(1, orderRows.size());
|
||||
List<?> orderData = (List<?>) orderRows.getFirst().get("data");
|
||||
@@ -195,8 +164,83 @@ public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest {
|
||||
Assertions.assertEquals("1", String.valueOf(orderData.get(1)));
|
||||
}
|
||||
|
||||
private static List<Integer> asIntegerList(Object value) {
|
||||
List<?> raw = (List<?>) value;
|
||||
return raw.stream().map(n -> ((Number) n).intValue()).toList();
|
||||
private List<Map<String, Object>> fetchReferences(
|
||||
WebSQLContextInfo sqlProcessorContext,
|
||||
Map<String, Object> resultSet,
|
||||
boolean isReference
|
||||
) throws Exception {
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
vars.put("projectId", globalProject.getId());
|
||||
vars.put("connectionId", databaseContainer.getId());
|
||||
vars.put("contextId", sqlProcessorContext.getId());
|
||||
vars.put("resultsId", String.valueOf(resultSet.get("id")));
|
||||
vars.put("isReference", isReference);
|
||||
List<Map<String, Object>> references = client.sendQuery(GraphQLTestConstant.GQL_SQL_RESULT_ASSOCIATIONS, vars);
|
||||
return references == null ? List.of() : references;
|
||||
}
|
||||
|
||||
private Map<String, Object> fetchFirstReference(
|
||||
WebSQLContextInfo sqlProcessorContext,
|
||||
Map<String, Object> resultSet,
|
||||
boolean isReference
|
||||
) throws Exception {
|
||||
List<Map<String, Object>> refs = fetchReferences(sqlProcessorContext, resultSet, isReference);
|
||||
return refs.isEmpty() ? null : refs.getFirst();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, Object> navigateByReference(
|
||||
@NotNull WebSQLContextInfo sqlProcessorContext,
|
||||
@NotNull Map<String, Object> reference,
|
||||
@NotNull Map<String, Object> sourceRow
|
||||
) throws Exception {
|
||||
String containerNodePath = JSONUtils.getString(reference, "targetNodePath");
|
||||
|
||||
String openTaskId = clientWrapper.asyncReadDataFromContainer(
|
||||
globalProject, sqlProcessorContext, databaseContainer.getId(), containerNodePath, Map.of());
|
||||
clientWrapper.waitTaskCompleted(openTaskId);
|
||||
Map<String, Object> openedResultSet = clientWrapper.readTaskResultSet(openTaskId);
|
||||
String resultId = String.valueOf(openedResultSet.get("id"));
|
||||
|
||||
List<?> rowData = (List<?>) sourceRow.get("data");
|
||||
List<Map<String, Object>> mapping = JSONUtils.getObjectList(reference, "columnMapping");
|
||||
List<Map<String, Object>> constraints = new ArrayList<>();
|
||||
for (Map<String, Object> pair : mapping) {
|
||||
int sourceIdx = ((Number) pair.get("sourceColumnIndex")).intValue();
|
||||
String targetCol = (String) pair.get("targetColumnName");
|
||||
Map<String, Object> constraint = new HashMap<>();
|
||||
constraint.put("attributePosition", findColumnPosition(openedResultSet, targetCol));
|
||||
constraint.put("attributeName", targetCol);
|
||||
constraint.put("operator", DBCLogicalOperator.EQUALS.getId());
|
||||
constraint.put("value", rowData.get(sourceIdx));
|
||||
constraints.add(constraint);
|
||||
}
|
||||
Map<String, Object> filter = new HashMap<>();
|
||||
filter.put("constraints", constraints);
|
||||
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
vars.put("projectId", globalProject.getId());
|
||||
vars.put("connectionId", databaseContainer.getId());
|
||||
vars.put("contextId", sqlProcessorContext.getId());
|
||||
vars.put("containerNodePath", containerNodePath);
|
||||
vars.put("resultId", resultId);
|
||||
vars.put("filter", filter);
|
||||
|
||||
Map<String, Object> response = client.sendQuery(GraphQLTestConstant.GQL_ASYNC_READ_DATA_FROM_CONTAINER, vars);
|
||||
Assertions.assertNotNull(response);
|
||||
String navTaskId = JSONUtils.getString(response, "id");
|
||||
Assertions.assertNotNull(navTaskId);
|
||||
clientWrapper.waitTaskCompleted(navTaskId);
|
||||
return clientWrapper.readTaskResultSet(navTaskId);
|
||||
}
|
||||
|
||||
private static int findColumnPosition(Map<String, Object> resultSet, String columnName) {
|
||||
List<Map<String, Object>> columns = JSONUtils.getObjectList(resultSet, "columns");
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
if (columnName.equals(JSONUtils.getString(columns.get(i), "name"))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Column '" + columnName + "' not found in target result set");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-26
@@ -91,18 +91,6 @@ public class GraphQLTestConstant {
|
||||
argumentCount
|
||||
}
|
||||
}
|
||||
associations {
|
||||
associationName
|
||||
targetEntityName
|
||||
nodePath
|
||||
columnIndexList
|
||||
}
|
||||
references {
|
||||
associationName
|
||||
targetEntityName
|
||||
nodePath
|
||||
columnIndexList
|
||||
}
|
||||
rowsWithMetaData {
|
||||
data
|
||||
metaData
|
||||
@@ -113,30 +101,32 @@ public class GraphQLTestConstant {
|
||||
}
|
||||
""";
|
||||
|
||||
public static final String GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY = """
|
||||
mutation asyncSqlNavigateForeignKey(
|
||||
public static final String GQL_SQL_RESULT_ASSOCIATIONS = """
|
||||
query sqlResultAssociations(
|
||||
$projectId: ID,
|
||||
$connectionId: ID!,
|
||||
$contextId: ID!,
|
||||
$resultsId: ID!,
|
||||
$row: SQLResultRow!,
|
||||
$columnIndex: Int!,
|
||||
$associationName: String!,
|
||||
$isReference: Boolean!,
|
||||
$dataFormat: ResultDataFormat
|
||||
$isReference: Boolean!
|
||||
) {
|
||||
result: asyncSqlNavigateForeignKey(
|
||||
result: sqlResultAssociations(
|
||||
projectId: $projectId,
|
||||
connectionId: $connectionId,
|
||||
contextId: $contextId,
|
||||
resultsId: $resultsId,
|
||||
row: $row,
|
||||
columnIndex: $columnIndex,
|
||||
associationName: $associationName,
|
||||
isReference: $isReference,
|
||||
dataFormat: $dataFormat
|
||||
isReference: $isReference
|
||||
) {
|
||||
id
|
||||
reference
|
||||
associationName
|
||||
targetCatalogName
|
||||
targetSchemaName
|
||||
targetEntityName
|
||||
targetNodePath
|
||||
columnMapping {
|
||||
sourceColumnIndex
|
||||
sourceColumnName
|
||||
targetColumnName
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
@@ -217,6 +207,7 @@ public class GraphQLTestConstant {
|
||||
$connectionId: ID!,
|
||||
$contextId: ID!,
|
||||
$containerNodePath: ID!,
|
||||
$resultId: ID,
|
||||
$filter: SQLDataFilter
|
||||
) {
|
||||
result: asyncReadDataFromContainer(
|
||||
@@ -224,6 +215,7 @@ public class GraphQLTestConstant {
|
||||
connectionId: $connectionId,
|
||||
contextId: $contextId,
|
||||
containerNodePath: $containerNodePath,
|
||||
resultId: $resultId,
|
||||
filter: $filter
|
||||
) {
|
||||
id
|
||||
|
||||
@@ -29,11 +29,14 @@
|
||||
pointer-events: none;
|
||||
opacity: var(--dbv-kit-control-disabled-opacity);
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox__check {
|
||||
margin-top: calc(var(--dbv-kit-checkbox-height) * (calc(1 - var(--dbv-kit-checkbox-check-size)) / 2));
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox__check {
|
||||
--check-size: round(calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size)), 2px);
|
||||
margin-top: calc(var(--dbv-kit-checkbox-height) * (calc(1 - var(--dbv-kit-checkbox-check-size)) / 2));
|
||||
height: var(--check-size);
|
||||
line-height: var(--check-size);
|
||||
width: var(--check-size);
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
.dbv-kit-combobox__item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: var(--dbv-kit-combobox-font-weight);
|
||||
font-size: var(--dbv-kit-combobox-font-size);
|
||||
padding-inline: var(--dbv-kit-combobox-item-padding-inline);
|
||||
padding-block: var(--dbv-kit-combobox-item-padding-block);
|
||||
text-align: left;
|
||||
|
||||
@@ -45,14 +45,15 @@ export interface ComboboxPopoverProps extends AriaComboboxPopoverProps {
|
||||
/**
|
||||
* ComboboxPopover - Wrapper around AriaKit's popover with default styles
|
||||
*/
|
||||
export function ComboboxPopover({ children, className, ...props }: ComboboxPopoverProps): React.JSX.Element {
|
||||
export function ComboboxPopover({ children, className, portal: portalProp, ...props }: ComboboxPopoverProps): React.JSX.Element {
|
||||
const popoverContext = use(UiKitPopoverContext);
|
||||
const portal = (popoverContext.portal || portalProp) ?? false;
|
||||
|
||||
return (
|
||||
<AriaComboboxPopover
|
||||
gutter={8}
|
||||
className={clsx('dbv-kit-combobox__popover', className)}
|
||||
portal={(popoverContext.portal || props.portal) ?? false}
|
||||
portal={portal}
|
||||
sameWidth
|
||||
unmountOnHide
|
||||
{...props}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--dbv-kit-select-label-padding-inline);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
@@ -30,12 +31,21 @@
|
||||
opacity: var(--dbv-kit-control-disabled-opacity);
|
||||
background-color: var(--dbv-kit-select-disabled-background);
|
||||
}
|
||||
&:focus-visible {
|
||||
&:focus-visible,
|
||||
&[data-focus-visible] {
|
||||
outline-style: solid;
|
||||
outline-offset: var(--dbv-kit-control-outline-offset);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-select__value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dbv-kit-select__label {
|
||||
display: block;
|
||||
padding-inline: var(--dbv-kit-select-label-padding-inline);
|
||||
|
||||
@@ -38,14 +38,15 @@ export function Select({ className, ...props }: SelectProps) {
|
||||
return <AriaSelect className={clsx('dbv-kit-select', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function SelectPopover({ children, className, ...props }: SelectPopoverProps) {
|
||||
export function SelectPopover({ children, className, portal: portalProp, ...props }: SelectPopoverProps) {
|
||||
const popoverContext = use(UiKitPopoverContext);
|
||||
const portal = (popoverContext.portal || portalProp) ?? false;
|
||||
|
||||
return (
|
||||
<AriaSelectPopover
|
||||
className={clsx('dbv-kit-select__popover', className)}
|
||||
sameWidth={props.sameWidth ?? true}
|
||||
portal={(popoverContext.portal || props.portal) ?? false}
|
||||
portal={portal}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function SelectField<T, ItemType extends {} = SelectItem<T>>({
|
||||
{label && <SelectLabel className={clsx(required && 'dbv-kit-select__label--required')}>{label}</SelectLabel>}
|
||||
|
||||
<Select id={id} name={name} disabled={disabled} required={required}>
|
||||
{displayValue}
|
||||
<span className="dbv-kit-select__value">{displayValue}</span>
|
||||
{arrowIcon ?? <Select.Arrow className="dbv-kit-select__arrow-icon tw:text-sm!" />}
|
||||
</Select>
|
||||
{description && <span className="dbv-kit-select__description">{description}</span>}
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
"node-domexception": ">1.0.0",
|
||||
"shell-quote": ">1.8.3",
|
||||
"esbuild": ">=0.28.1",
|
||||
"form-data": ">=4.0.6"
|
||||
"form-data": ">=4.0.6",
|
||||
"undici": ">=6.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "dbeaver-test",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./module": "./lib/module.js"
|
||||
"./module": "./lib/module.js",
|
||||
"./*.css": "./src/*.css"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,7 @@ export const GroupBack: React.FC<React.HTMLAttributes<HTMLButtonElement>> = func
|
||||
const styles = useS(classes);
|
||||
|
||||
return (
|
||||
<Flex gap="xs" align="center">
|
||||
<Flex gap="xxs" align="center">
|
||||
<ActionIconButton className={s(styles, { button: true }, className)} name="angle" title={translate('ui_close')} {...rest} />
|
||||
{children}
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@layer components {
|
||||
.dbv-kit-disclosure__disclosure {
|
||||
--dbv-kit-disclosure-foreground: var(--theme-text-on-surface);
|
||||
--dbv-kit-disclosure-outline-color: var(--theme-primary);
|
||||
--dbv-kit-disclosure-outline-width: 1px;
|
||||
outline-offset: var(--dbv-kit-disclosure-outline-offset) !important;
|
||||
|
||||
&[aria-expanded='true'] .disclosure-icon {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&[data-s-gap='xxs'] {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&[data-s-gap='xs'] {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -12,7 +12,7 @@ import classes from './Flex.module.css';
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
overflow?: boolean;
|
||||
gap?: 'xs' | 'md' | 'lg';
|
||||
gap?: 'xxs' | 'xs' | 'md' | 'lg';
|
||||
wrap?: React.CSSProperties['flexWrap'];
|
||||
direction?: React.CSSProperties['flexDirection'];
|
||||
align?: React.CSSProperties['alignItems'];
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
--dbv-kit-checkbox-border-color: var(--theme-on-secondary);
|
||||
--dbv-kit-checkbox-border-color-checked: var(--theme-on-secondary);
|
||||
--dbv-kit-checkbox-icon-color: var(--theme-on-secondary);
|
||||
--dbv-kit-control-outline-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@@ -54,6 +53,9 @@
|
||||
--dbv-kit-checkbox-large-font-size: 16px;
|
||||
--dbv-kit-checkbox-xlarge-font-size: 18px;
|
||||
|
||||
--dbv-kit-control-outline-color: var(--theme-primary);
|
||||
--dbv-kit-control-outline-width: 1px;
|
||||
|
||||
& .checkbox-markup__label {
|
||||
color: var(--theme-on-surface);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
|
||||
--dbv-kit-combobox-disabled-background: var(--theme-input-background-disabled);
|
||||
|
||||
--dbv-kit-control-outline-offset: -2px;
|
||||
--dbv-kit-control-outline-offset: -1px;
|
||||
--dbv-kit-control-outline-width: 1px;
|
||||
--dbv-kit-control-outline-color: var(--theme-primary);
|
||||
|
||||
--dbv-kit-combobox-popover-background: var(--theme-input-background);
|
||||
--dbv-kit-combobox-popover-foreground: var(--theme-input-color);
|
||||
@@ -27,8 +29,17 @@
|
||||
padding-block: calc(var(--tw-spacing) * 2);
|
||||
gap: calc(var(--tw-spacing) * 2);
|
||||
letter-spacing: var(--tw-tracking-normal);
|
||||
font-family: var(--tw-font-roboto);
|
||||
font-size: var(--tw-text-caption);
|
||||
font-weight: var(--tw-font-weight-caption);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.combobox__item-value {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base.components {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -64,12 +64,12 @@ type ObjectProps<TValue, TKey extends keyof TState, TState> = ComboboxBaseProps<
|
||||
value?: never;
|
||||
};
|
||||
|
||||
export interface ComboboxType {
|
||||
export interface IComboboxType {
|
||||
<TKey, TValue>(props: ControlledProps<TKey, TValue>): React.JSX.Element;
|
||||
<TValue, TKey extends keyof TState, TState>(props: ObjectProps<TValue, TKey, TState>): React.JSX.Element;
|
||||
}
|
||||
|
||||
export const Combobox: ComboboxType = observer(function Combobox({
|
||||
export const Combobox: IComboboxType = observer(function Combobox({
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
name,
|
||||
@@ -255,7 +255,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
)}
|
||||
{icon && <div className="tw:absolute tw:left-3 tw:w-4 tw:h-4">{typeof icon === 'string' ? <IconOrImage icon={icon} /> : icon}</div>}
|
||||
{displayPopover && (
|
||||
<ComboboxPopover className="theme-text-on-surface theme-background-surface theme-typography--caption">
|
||||
<ComboboxPopover className="theme-text-on-surface theme-background-surface">
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map(({ itemKey, itemValue, itemTitle, itemIcon, itemDisabled }) => (
|
||||
<ComboboxItem
|
||||
@@ -274,7 +274,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
|
||||
{itemIcon && typeof itemIcon === 'string' ? <IconOrImage icon={itemIcon} /> : itemIcon}
|
||||
</div>
|
||||
)}
|
||||
<div className="tw:truncate">{itemValue}</div>
|
||||
<div className="combobox__item-value">{itemValue}</div>
|
||||
</ComboboxItem>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
|
||||
--dbv-kit-radio-inactive-border: color-mix(in srgb, var(--theme-on-secondary), white 20%);
|
||||
--dbv-kit-radio-hover-shadow-color: color-mix(in srgb, var(--theme-on-secondary), white 80%);
|
||||
|
||||
--dbv-kit-control-outline-color: var(--theme-primary);
|
||||
--dbv-kit-control-outline-width: 1px;
|
||||
}
|
||||
|
||||
.dbv-kit-radio__title {
|
||||
|
||||
@@ -34,7 +34,10 @@
|
||||
--dbv-kit-select-disabled-background: var(--theme-input-background-disabled);
|
||||
|
||||
--dbv-kit-select-border-radius: 3px;
|
||||
--dbv-kit-control-outline-offset: -2px;
|
||||
|
||||
--dbv-kit-control-outline-width: 1px;
|
||||
--dbv-kit-control-outline-color: var(--theme-primary);
|
||||
--dbv-kit-control-outline-offset: -1px;
|
||||
}
|
||||
|
||||
.dbv-kit-select__arrow-icon {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
import './module.js';
|
||||
|
||||
import './FormControls/Select.css';
|
||||
import './FormControls/Combobox.css';
|
||||
|
||||
export * from './AuthenticationProviderLoader.js';
|
||||
export * from './useAuthenticationAction.js';
|
||||
export * from './CommonDialog/CommonDialog/CommonDialogBody.js';
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type GetUserConnectionsQueryVariables,
|
||||
GraphQLService,
|
||||
type InitConnectionMutationVariables,
|
||||
type ObjectPropertyInfo,
|
||||
type TestConnectionMutation,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
@@ -470,6 +471,15 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
|
||||
return this.get(key)!;
|
||||
}
|
||||
|
||||
async getConnectionDriverProperties(projectId: string, config: ConnectionConfig): Promise<ObjectPropertyInfo[]> {
|
||||
const { properties } = await this.graphQLService.sdk.getConnectionDriverProperties({
|
||||
projectId,
|
||||
config,
|
||||
});
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
deleteConnection(key: IConnectionInfoParams): Promise<void>;
|
||||
deleteConnection(key: ResourceKeyList<IConnectionInfoParams>): Promise<void>;
|
||||
deleteConnection(key: ResourceKey<IConnectionInfoParams>): Promise<void>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -58,10 +58,8 @@ export * from './DatabaseConnection.js';
|
||||
export * from './DBDriverResource.js';
|
||||
export * from './CONNECTION_INFO_PARAM_SCHEMA.js';
|
||||
export * from './isJDBCConnection.js';
|
||||
export * from './NetworkHandlerResource.js';
|
||||
export * from './useConnectionInfo.js';
|
||||
export * from './useDBDriver.js';
|
||||
export * from './USER_NAME_PROPERTY_ID.js';
|
||||
export * from './parseConnectionKey.js';
|
||||
export * from './DBDriverExpertSettingsResource.js';
|
||||
export * from './CONNECTION_CONFIG_SCHEMA.js';
|
||||
|
||||
@@ -38,10 +38,6 @@ export default [
|
||||
['connections_connection_test_fail', 'Verbindungstest ist fehlgeschlagen'],
|
||||
['connections_network_handler_default_user', 'Benutzer'],
|
||||
['connections_network_handler_default_password', 'Passwort'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'Benutzer'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Passwort'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', 'Authentifizierungsmethode'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Erweiterte Einstellungen'],
|
||||
['connections_not_found', 'Es wurden keine Datenbankverbindungen gefunden'],
|
||||
|
||||
['core_connections_settings_disable', 'Disable'],
|
||||
|
||||
@@ -51,23 +51,9 @@ export default [
|
||||
['connections_connection_create_fail', 'Fail to create connection'],
|
||||
['connections_connection_save_fail', 'Fail to save connection'],
|
||||
['connections_connection_expert_settings', 'Expert settings'],
|
||||
['connections_network_handler_test', 'Test Tunnel'],
|
||||
['connections_network_handler_test_fail', 'Tunnel test failed'],
|
||||
['connections_network_handler_test_success', 'Tunnel test success'],
|
||||
['connections_network_handler_default_title', 'Network handler'],
|
||||
['connections_network_handler_default_user', 'User'],
|
||||
['connections_network_handler_default_password', 'Password'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'SSH Tunnel'],
|
||||
['connections_network_handler_ssh_tunnel_enable', 'Use SSH Tunnel'],
|
||||
['connections_network_handler_ssh_tunnel_host', 'Host'],
|
||||
['connections_network_handler_ssh_tunnel_port', 'Port'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'User'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Password'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', 'Authentication method'],
|
||||
['connections_network_handler_ssh_tunnel_private_key', 'Private Key'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Advanced Settings'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', 'Alive interval (ms)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', 'Connect timeout (ms)'],
|
||||
['connections_driver_search_placeholder', 'Type driver name...'],
|
||||
['connections_not_found', 'No database connections were found'],
|
||||
|
||||
|
||||
@@ -56,23 +56,9 @@ export default [
|
||||
['connections_connection_create_fail', 'Échec de la création de la connexion'],
|
||||
['connections_connection_save_fail', 'Échec de la sauvegarde de la connexion'],
|
||||
['connections_connection_expert_settings', 'Expert settings'],
|
||||
['connections_network_handler_test', 'Tester le tunnel'],
|
||||
['connections_network_handler_test_fail', 'Échec du test du tunnel'],
|
||||
['connections_network_handler_test_success', 'Test du tunnel réussi'],
|
||||
['connections_network_handler_default_title', 'Gestionnaire de réseau'],
|
||||
['connections_network_handler_default_user', 'Utilisateur'],
|
||||
['connections_network_handler_default_password', 'Mot de passe'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'Tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_enable', 'Utiliser le tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_host', 'Hôte'],
|
||||
['connections_network_handler_ssh_tunnel_port', 'Port'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'Utilisateur'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Mot de passe'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', "Méthode d'authentification"],
|
||||
['connections_network_handler_ssh_tunnel_private_key', 'Clé privée'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Paramètres avancés'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', 'Intervalle de maintien en vie (ms)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', 'Délai de connexion (ms)'],
|
||||
['connections_driver_search_placeholder', 'Saisir le nom du pilote...'],
|
||||
['connections_not_found', 'Aucune connexion de base de données trouvée'],
|
||||
|
||||
|
||||
@@ -49,23 +49,9 @@ export default [
|
||||
['connections_connection_create_fail', 'Errore di creazione connessione'],
|
||||
['connections_connection_save_fail', 'Errore di salvataggio connessione'],
|
||||
['connections_connection_expert_settings', 'Expert settings'],
|
||||
['connections_network_handler_test', 'Prova il Tunnel'],
|
||||
['connections_network_handler_test_fail', 'Prova del Tunnel fallita'],
|
||||
['connections_network_handler_test_success', 'Prova del Tunnel terminata con successo'],
|
||||
['connections_network_handler_default_title', 'Network Handler'],
|
||||
['connections_network_handler_default_user', 'Utente'],
|
||||
['connections_network_handler_default_password', 'Password'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'Tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_enable', 'Usa il Tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_host', 'Host'],
|
||||
['connections_network_handler_ssh_tunnel_port', 'Porta'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'Utente'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Password'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', 'Authentication method'],
|
||||
['connections_network_handler_ssh_tunnel_private_key', 'Private key'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Advanced Settings'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', 'Alive interval (ms)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', 'Connect timeout (ms)'],
|
||||
['connections_driver_search_placeholder', 'Digita il nome del driver...'],
|
||||
['connections_not_found', 'Nessuna connessione al database trovata'],
|
||||
|
||||
|
||||
@@ -60,23 +60,9 @@ export default [
|
||||
['connections_connection_create_fail', 'Не удалось создать подключение'],
|
||||
['connections_connection_save_fail', 'Не удалось сохранить подключение'],
|
||||
['connections_connection_expert_settings', 'Продвинутые настройки'],
|
||||
['connections_network_handler_test', 'Проверить подключение'],
|
||||
['connections_network_handler_test_fail', 'Не удалось установить соединение'],
|
||||
['connections_network_handler_test_success', 'Соединение установлено'],
|
||||
['connections_network_handler_default_title', 'Сетевой обработчик'],
|
||||
['connections_network_handler_default_user', 'Пользователь'],
|
||||
['connections_network_handler_default_password', 'Пароль'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'SSH Тунель'],
|
||||
['connections_network_handler_ssh_tunnel_enable', 'Использовать SSH Тунель'],
|
||||
['connections_network_handler_ssh_tunnel_host', 'Хост'],
|
||||
['connections_network_handler_ssh_tunnel_port', 'Порт'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'Пользователь'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Пароль'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', 'Метод аутентификации'],
|
||||
['connections_network_handler_ssh_tunnel_private_key', 'Приватный ключ'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Дополнительные настройки'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', 'Интервал keep-alive (мс)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', 'Тайм-аут подключения (мс)'],
|
||||
['connections_driver_search_placeholder', 'Введите название драйвера...'],
|
||||
['connections_not_found', 'Подключения к базам данных не найдены'],
|
||||
|
||||
|
||||
@@ -66,23 +66,9 @@ export default [
|
||||
['connections_connection_create_fail', 'Tạo kết nối thất bại'],
|
||||
['connections_connection_save_fail', 'Lưu kết nối thất bại'],
|
||||
['connections_connection_expert_settings', 'Cài đặt nâng cao'],
|
||||
['connections_network_handler_test', 'Kiểm tra Tunnel'],
|
||||
['connections_network_handler_test_fail', 'Kiểm tra tunnel thất bại'],
|
||||
['connections_network_handler_test_success', 'Kiểm tra tunnel thành công'],
|
||||
['connections_network_handler_default_title', 'Trình xử lý mạng'],
|
||||
['connections_network_handler_default_user', 'Người dùng'],
|
||||
['connections_network_handler_default_password', 'Mật khẩu'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'Tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_enable', 'Sử dụng Tunnel SSH'],
|
||||
['connections_network_handler_ssh_tunnel_host', 'Máy chủ'],
|
||||
['connections_network_handler_ssh_tunnel_port', 'Cổng'],
|
||||
['connections_network_handler_ssh_tunnel_user', 'Người dùng'],
|
||||
['connections_network_handler_ssh_tunnel_password', 'Mật khẩu'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', 'Phương thức xác thực'],
|
||||
['connections_network_handler_ssh_tunnel_private_key', 'Khóa riêng'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', 'Cài đặt nâng cao'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', 'Khoảng thời gian giữ kết nối (ms)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', 'Thời gian chờ kết nối (ms)'],
|
||||
['connections_driver_search_placeholder', 'Nhập tên trình điều khiển (driver)...'],
|
||||
['connections_not_found', 'Không tìm thấy kết nối cơ sở dữ liệu'],
|
||||
[
|
||||
|
||||
@@ -43,23 +43,9 @@ export default [
|
||||
['connections_connection_create_fail', '无法创建连接'],
|
||||
['connections_connection_save_fail', '无法保存连接'],
|
||||
['connections_connection_expert_settings', '专业设置'],
|
||||
['connections_network_handler_test', '测试隧道'],
|
||||
['connections_network_handler_test_fail', '隧道测试失败'],
|
||||
['connections_network_handler_test_success', '隧道测试成功'],
|
||||
['connections_network_handler_default_title', '网络处理程序'],
|
||||
['connections_network_handler_default_user', '用户'],
|
||||
['connections_network_handler_default_password', '密码'],
|
||||
['connections_network_handler_ssh_tunnel_title', 'SSH隧道'],
|
||||
['connections_network_handler_ssh_tunnel_enable', '使用SSH隧道'],
|
||||
['connections_network_handler_ssh_tunnel_host', '主机'],
|
||||
['connections_network_handler_ssh_tunnel_port', '端口'],
|
||||
['connections_network_handler_ssh_tunnel_user', '用户'],
|
||||
['connections_network_handler_ssh_tunnel_password', '密码'],
|
||||
['connections_network_handler_ssh_tunnel_auth_type', '认证方式'],
|
||||
['connections_network_handler_ssh_tunnel_private_key', '私钥'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings', '高级设置'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_alive_interval', '活动间隔 (ms)'],
|
||||
['connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout', '连接超时 (ms)'],
|
||||
['connections_driver_search_placeholder', '输入驱动名称...'],
|
||||
['connections_not_found', '未找到数据库连接'],
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di';
|
||||
import { NetworkHandlerResource } from './NetworkHandlerResource.js';
|
||||
import { NavNodeExtensionsService } from './NavTree/NavNodeExtensionsService.js';
|
||||
import { DBDriverResource } from './DBDriverResource.js';
|
||||
import { DatabaseAuthModelsResource } from './DatabaseAuthModelsResource.js';
|
||||
@@ -42,7 +41,6 @@ export default ModuleRegistry.add({
|
||||
.addSingleton(Bootstrap, ConnectionsLocaleService)
|
||||
.addSingleton(Dependency, proxy(ConnectionsSettingsService))
|
||||
.addSingleton(Dependency, proxy(ConnectionDialectResource))
|
||||
.addSingleton(Dependency, proxy(NetworkHandlerResource))
|
||||
.addSingleton(Dependency, proxy(DBDriverResource))
|
||||
.addSingleton(Dependency, proxy(DatabaseAuthModelsResource))
|
||||
.addSingleton(Dependency, proxy(ContainerResource))
|
||||
@@ -60,7 +58,6 @@ export default ModuleRegistry.add({
|
||||
.addSingleton(Dependency, proxy(ConnectionExecutionContextResource))
|
||||
.addSingleton(Dependency, proxy(DBDriverExpertSettingsResource))
|
||||
.addSingleton(ConnectionDialectResource)
|
||||
.addSingleton(NetworkHandlerResource)
|
||||
.addSingleton(NavNodeExtensionsService)
|
||||
.addSingleton(DBDriverResource)
|
||||
.addSingleton(DBDriverExpertSettingsResource)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { isNumber } from '@dbeaver/js-helpers';
|
||||
|
||||
import { ServerResourceQuotasResource } from './ServerResourceQuotasResource.js';
|
||||
|
||||
@@ -27,10 +29,6 @@ const DEFAULT_QUOTAS: IQuotas = {
|
||||
sqlBinaryPreviewMaxLength: 261120,
|
||||
};
|
||||
|
||||
function isNumber(value: any): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
@injectable(() => [ServerResourceQuotasResource])
|
||||
export class QuotasService {
|
||||
get quotas(): IQuotas {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
query getConnectionDriverProperties($projectId: ID!, $config: ConnectionConfig!) {
|
||||
properties: listConnectionDriverProperties(projectId: $projectId, config: $config) {
|
||||
...ObjectPropertyInfo
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,5 @@ fragment AdminUserInfo on AdminUserInfo {
|
||||
disabledBy
|
||||
disableDate
|
||||
disableReason
|
||||
lastLoginTime
|
||||
}
|
||||
|
||||
@@ -40,18 +40,6 @@ mutation getSqlExecuteTaskResults($taskId: ID!) {
|
||||
data
|
||||
metaData
|
||||
}
|
||||
associations {
|
||||
associationName
|
||||
targetEntityName
|
||||
nodePath
|
||||
columnIndexList
|
||||
}
|
||||
references {
|
||||
associationName
|
||||
targetEntityName
|
||||
nodePath
|
||||
columnIndexList
|
||||
}
|
||||
singleEntity
|
||||
hasMoreData
|
||||
hasRowIdentifier
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
query getSqlResultAssociations($projectId: ID!, $connectionId: ID!, $contextId: ID!, $resultsId: ID!, $isReference: Boolean) {
|
||||
associations: sqlResultAssociations(
|
||||
projectId: $projectId
|
||||
connectionId: $connectionId
|
||||
contextId: $contextId
|
||||
resultsId: $resultsId
|
||||
isReference: $isReference
|
||||
) {
|
||||
reference
|
||||
associationName
|
||||
targetEntityName
|
||||
targetNodePath
|
||||
columnMapping {
|
||||
sourceColumnIndex
|
||||
sourceColumnName
|
||||
targetColumnIndex
|
||||
targetColumnName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,6 @@ export abstract class FormPart<TPartState extends object, TFormState = any> impl
|
||||
protected validate(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
|
||||
|
||||
protected abstract loader(): Promise<void>;
|
||||
protected abstract saveChanges(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): Promise<void>;
|
||||
protected abstract saveChanges(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void>;
|
||||
dispose(): void | Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,18 @@
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"modifiers": ["exported"],
|
||||
"format": ["PascalCase"],
|
||||
"custom": {
|
||||
"regex": "^I[A-Z]",
|
||||
"match": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": ["error", "always"],
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
"error",
|
||||
|
||||
@@ -92,6 +92,18 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'interface',
|
||||
modifiers: ['exported'],
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z]',
|
||||
match: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@stylistic/semi': ['error', 'always'],
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,12 +26,13 @@ import type { TeamFormProps } from '../TeamsAdministrationFormService.js';
|
||||
import type { GrantedUsersFormPart } from './GrantedUsersFormPart.js';
|
||||
|
||||
const USER_ID_COLUMN: IGrantManagementTableColumn = { key: 'userId', label: 'administration_teams_team_granted_users_user_id' };
|
||||
const LAST_LOGIN_COLUMN: IGrantManagementTableColumn = { key: 'lastLogin', label: 'plugin_authentication_administration_user_last_login' };
|
||||
const TEAM_ROLE_COLUMN: IGrantManagementTableColumn = {
|
||||
key: 'teamRole',
|
||||
label: 'plugin_authentication_administration_team_user_team_role_supervisor',
|
||||
};
|
||||
|
||||
const COLUMNS: IGrantManagementTableColumn[] = [USER_ID_COLUMN];
|
||||
const COLUMNS: IGrantManagementTableColumn[] = [USER_ID_COLUMN, LAST_LOGIN_COLUMN];
|
||||
|
||||
export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = observer(function GrantedUsersTable({ tabId, formState }) {
|
||||
const translate = useTranslate();
|
||||
@@ -106,6 +107,13 @@ export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = obse
|
||||
);
|
||||
}
|
||||
|
||||
if (colKey === LAST_LOGIN_COLUMN.key) {
|
||||
const lastLoginFullTime = user.lastLoginTime ? new Date(user.lastLoginTime).toLocaleString() : '-';
|
||||
const lastLoginDate = user.lastLoginTime ? new Date(user.lastLoginTime).toLocaleDateString() : '-';
|
||||
|
||||
return <span title={lastLoginFullTime}>{lastLoginDate}</span>;
|
||||
}
|
||||
|
||||
if (colKey === TEAM_ROLE_COLUMN.key) {
|
||||
const granted = tabState.state.grantedUsers.find(grantedUser => grantedUser.userId === user.userId);
|
||||
|
||||
|
||||
+18
-2
@@ -1,13 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, FieldCheckbox, Group, GroupTitle, Placeholder, useAutoLoad, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Container,
|
||||
FieldCheckbox,
|
||||
Group,
|
||||
GroupTitle,
|
||||
InputField,
|
||||
Placeholder,
|
||||
useAutoLoad,
|
||||
useResource,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui';
|
||||
|
||||
@@ -36,6 +46,7 @@ export const UserFormInfo: TabContainerPanelComponent<UserFormProps> = observer(
|
||||
|
||||
const disabled = tabState.isLoading();
|
||||
const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled;
|
||||
const lastLoginTime = user.data?.lastLoginTime ? new Date(user.data.lastLoginTime).toLocaleString() : null;
|
||||
|
||||
return (
|
||||
<Container overflow>
|
||||
@@ -53,6 +64,11 @@ export const UserFormInfo: TabContainerPanelComponent<UserFormProps> = observer(
|
||||
state={tabState.state}
|
||||
disabled={disabled || userManagementDisabled}
|
||||
/>
|
||||
{lastLoginTime && (
|
||||
<InputField value={lastLoginTime ?? ''} readOnly>
|
||||
{translate('plugin_authentication_administration_user_last_login')}
|
||||
</InputField>
|
||||
)}
|
||||
<UserFormInfoTeams formState={formState} tabState={tabState} tabSelected={tab.selected} disabled={disabled} />
|
||||
</Group>
|
||||
<UserFormInfoMetaParameters formState={formState} tabState={tabState} tabSelected={tab.selected} disabled={disabled} />
|
||||
|
||||
+8
-1
@@ -34,8 +34,9 @@ const ROLE_COLUMN = { key: 'role', label: 'authentication_user_role' };
|
||||
const TEAM_COLUMN = { key: 'team', label: 'authentication_user_team' };
|
||||
const ENABLED_COLUMN = { key: 'enabled', label: 'authentication_user_enabled' };
|
||||
const AUTH_COLUMN = { key: 'auth', label: 'authentication_administration_user_auth_methods' };
|
||||
const LAST_LOGIN_COLUMN = { key: 'lastLogin', label: 'plugin_authentication_administration_user_last_login' };
|
||||
|
||||
const COLUMNS = [ID_COLUMN, TEAM_COLUMN, ENABLED_COLUMN, AUTH_COLUMN];
|
||||
const COLUMNS = [ID_COLUMN, TEAM_COLUMN, ENABLED_COLUMN, AUTH_COLUMN, LAST_LOGIN_COLUMN];
|
||||
|
||||
export const UsersTable = observer<Props>(function UsersTable({ users, isManageable, displayAuthRole, onLoadMore }) {
|
||||
const translate = useTranslate();
|
||||
@@ -131,6 +132,12 @@ export const UsersTable = observer<Props>(function UsersTable({ users, isManagea
|
||||
);
|
||||
}
|
||||
|
||||
if (column.key === LAST_LOGIN_COLUMN.key) {
|
||||
const lastLoginFullTime = row.lastLoginTime ? new Date(row.lastLoginTime).toLocaleString() : '-';
|
||||
const lastLoginDate = row.lastLoginTime ? new Date(row.lastLoginTime).toLocaleDateString() : '-';
|
||||
return <span title={lastLoginFullTime}>{lastLoginDate}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ export default [
|
||||
|
||||
['plugin_authentication_administration_team_form_edit_label', 'Team editing form'],
|
||||
['plugin_authentication_administration_user_form_edit_label', 'User editing form'],
|
||||
['plugin_authentication_administration_user_last_login', 'Last login'],
|
||||
|
||||
[
|
||||
'plugin_authentication_administration_user_username_validation_error',
|
||||
|
||||
@@ -128,6 +128,7 @@ export default [
|
||||
],
|
||||
['plugin_authentication_administration_team_form_edit_label', 'Team editing form'],
|
||||
['plugin_authentication_administration_user_form_edit_label', 'User editing form'],
|
||||
['plugin_authentication_administration_user_last_login', 'Dernière connexion'],
|
||||
|
||||
[
|
||||
'plugin_authentication_administration_user_username_validation_error',
|
||||
|
||||
@@ -86,6 +86,7 @@ export default [
|
||||
|
||||
['plugin_authentication_administration_team_form_edit_label', 'Team editing form'],
|
||||
['plugin_authentication_administration_user_form_edit_label', 'User editing form'],
|
||||
['plugin_authentication_administration_user_last_login', 'Ultimo accesso'],
|
||||
|
||||
[
|
||||
'plugin_authentication_administration_user_username_validation_error',
|
||||
|
||||
@@ -147,6 +147,7 @@ export default [
|
||||
|
||||
['plugin_authentication_administration_team_form_edit_label', 'Форма редактирования команды'],
|
||||
['plugin_authentication_administration_user_form_edit_label', 'Форма редактирования пользователя'],
|
||||
['plugin_authentication_administration_user_last_login', 'Последний вход'],
|
||||
|
||||
[
|
||||
'plugin_authentication_administration_user_username_validation_error',
|
||||
|
||||
@@ -137,6 +137,7 @@ export default [
|
||||
],
|
||||
['plugin_authentication_administration_team_form_edit_label', 'Biểu mẫu chỉnh sửa nhóm'],
|
||||
['plugin_authentication_administration_user_form_edit_label', 'Biểu mẫu chỉnh sửa người dùng'],
|
||||
['plugin_authentication_administration_user_last_login', 'Lần đăng nhập cuối'],
|
||||
|
||||
[
|
||||
'plugin_authentication_administration_user_username_validation_error',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user