Merge branch 'devel' into 5170-cb-6198-migrate-secrets-ui-public-from-backend-to-frontend-part

This commit is contained in:
Evgenia
2025-09-11 10:33:03 +02:00
committed by GitHub
53 changed files with 676 additions and 499 deletions
@@ -35,6 +35,7 @@ import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.secret.DBSSecretController;
import org.jkiss.dbeaver.model.struct.DBSObjectFilter;
import org.jkiss.dbeaver.registry.DataSourceConfigurationManager;
import org.jkiss.dbeaver.registry.DataSourceParseResults;
import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry;
import org.jkiss.dbeaver.registry.DataSourceRegistry;
@@ -377,7 +378,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
}
@Override
public boolean loadDataSources(
public DataSourceParseResults loadDataSources(
@NotNull List<DBPDataSourceConfigurationStorage> storages,
@NotNull DataSourceConfigurationManager manager,
@Nullable Collection<String> dataSourceIds, boolean refresh,
@@ -29,13 +29,16 @@ import org.jkiss.dbeaver.model.navigator.DBNModel;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.rm.RMUtils;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceRegistry;
import org.jkiss.dbeaver.runtime.jobs.DisconnectJob;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WebSessionProjectImpl extends WebProjectImpl {
private static final Log log = Log.getLog(WebSessionProjectImpl.class);
@@ -194,40 +197,31 @@ public class WebSessionProjectImpl extends WebProjectImpl {
/**
* updates data sources based on event in web session
*
* @param dataSourceIds list of updated connections
* @param eventId id of event
* @param event data source updated event
*/
public synchronized boolean updateProjectDataSources(@NotNull List<String> dataSourceIds, @NotNull String eventId) {
public synchronized boolean updateProjectDataSources(@NotNull WSDataSourceEvent event) {
var sendDataSourceUpdatedEvent = false;
DBPDataSourceRegistry registry = getDataSourceRegistry();
// save old connections
var oldDataSources = dataSourceIds.stream()
.map(registry::getDataSource)
.filter(Objects::nonNull)
.collect(Collectors.toMap(
DBPDataSourceContainer::getId,
registry::createDataSource)
);
if (WSDataSourceEvent.CREATED.equals(eventId) || WSDataSourceEvent.UPDATED.equals(eventId)) {
registry.refreshConfig(dataSourceIds);
if (WSDataSourceEvent.CREATED.equals(event.getId()) || WSDataSourceEvent.UPDATED.equals(event.getId())) {
registry.refreshConfig(event.getDataSourceIds());
}
for (String dsId : dataSourceIds) {
for (String dsId : event.getDataSourceIds()) {
DataSourceDescriptor ds = (DataSourceDescriptor) registry.getDataSource(dsId);
if (ds == null) {
continue;
}
switch (eventId) {
switch (event.getId()) {
case WSDataSourceEvent.CREATED -> {
addConnection(ds);
sendDataSourceUpdatedEvent = true;
}
case WSDataSourceEvent.UPDATED -> {
boolean connectionUpdated = !ds.equalSettings(oldDataSources.get(dsId));
if (connectionUpdated) {
sendDataSourceUpdatedEvent = true;
if (event.getProperty() == WSDataSourceProperty.CONFIGURATION) {
WebDataSourceUtils.disconnectDataSource(webSession, ds);
}
// if settings were changed we need to send event
if (event.getProperty() != WSDataSourceProperty.INTERNAL) {
sendDataSourceUpdatedEvent = true;
}
}
case WSDataSourceEvent.DELETED -> {
WebDataSourceUtils.disconnectDataSource(webSession, ds);
@@ -149,27 +149,37 @@ public abstract class BaseLocalResourceController implements RMController {
@NotNull String projectId,
@NotNull String configuration,
@Nullable List<String> dataSourceIds
) throws DBException {
return updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds) != null;
}
@Nullable
protected DataSourceParseResults updateProjectDataSourcesConfig(
@NotNull String projectId,
@NotNull String configuration,
@Nullable List<String> dataSourceIds
) throws DBException {
try (var lock = lockController.lock(projectId, "updateProjectDataSources")) {
DBPProject project = getWebProject(projectId, false);
return doFileWriteOperation(projectId, project.getMetadataFolder(false),
return doFileWriteOperation(
projectId, project.getMetadataFolder(false),
() -> {
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(
StandardCharsets.UTF_8));
DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer();
var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources(
final DataSourceParseResults parseResults = ((DataSourcePersistentRegistry) registry).loadDataSources(
List.of(storage),
manager,
dataSourceIds,
true,
false
dataSourceIds == null
);
registry.checkForErrors();
log.debug("Save data sources configuration in project '" + projectId + "'");
((DataSourcePersistentRegistry) registry).saveDataSources();
registry.checkForErrors();
return configChanged;
return parseResults;
}
);
}
@@ -217,6 +227,7 @@ public abstract class BaseLocalResourceController implements RMController {
var parent = result.getParent();
var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/"));
DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName);
((DataSourcePersistentRegistry) registry).saveDataSources();
registry.checkForErrors();
return null;
}
@@ -244,6 +255,7 @@ public abstract class BaseLocalResourceController implements RMController {
log.warn("Can not find folder by path [" + folderPath + "] for deletion");
}
}
((DataSourcePersistentRegistry) registry).saveDataSources();
registry.checkForErrors();
return null;
}
@@ -265,6 +277,7 @@ public abstract class BaseLocalResourceController implements RMController {
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
registry.moveFolder(oldPath, newPath);
registry.checkForErrors();
((DataSourcePersistentRegistry) registry).saveDataSources();
return null;
}
);
@@ -28,18 +28,26 @@ 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.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPWorkspace;
import org.jkiss.dbeaver.model.auth.SMCredentials;
import org.jkiss.dbeaver.model.auth.SMCredentialsProvider;
import org.jkiss.dbeaver.model.fs.lock.FileLockController;
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.rm.*;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.model.security.SMObjectType;
import org.jkiss.dbeaver.model.sql.DBQuotaException;
import org.jkiss.dbeaver.model.websocket.event.MessageType;
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.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceParseResults;
import org.jkiss.dbeaver.registry.ResourceTypeDescriptor;
import org.jkiss.dbeaver.registry.ResourceTypeRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
@@ -334,6 +342,239 @@ public class LocalResourceController extends BaseLocalResourceController {
return project;
}
@Override
public void createProjectDataSources(
@NotNull String projectId,
@NotNull String configuration,
@Nullable List<String> dataSourceIds
) throws DBException {
super.createProjectDataSources(projectId, configuration, dataSourceIds);
if (credentialsProvider.getActiveUserCredentials() != null && dataSourceIds != null) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
dataSourceIds,
WSDataSourceProperty.CONFIGURATION
)
);
}
}
@Override
public boolean updateProjectDataSources(
@NotNull String projectId,
@NotNull String configuration,
@Nullable List<String> dataSourceIds
) throws DBException {
DBPDataSourceRegistry registry = getWebProject(projectId, false).getDataSourceRegistry();
Map<String, DataSourceDescriptor> oldDataSources = registry.getDataSources().stream()
.filter(ds -> dataSourceIds == null || dataSourceIds.contains(ds.getId()))
.collect(Collectors.toMap(
DBPDataSourceContainer::getId,
registry::createDataSource
)
);
DataSourceParseResults parseResults = super.updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds);
sendDataSourcesConfigUpdatedEvent(registry, oldDataSources, parseResults);
return parseResults != null;
}
@Override
public void deleteProjectDataSources(@NotNull String projectId, @NotNull String[] dataSourceIds) throws DBException {
super.deleteProjectDataSources(projectId, dataSourceIds);
if (credentialsProvider.getActiveUserCredentials() != null && dataSourceIds.length > 0) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.delete(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
Arrays.asList(dataSourceIds),
WSDataSourceProperty.CONFIGURATION
)
);
}
}
@Override
public void createProjectDataSourceFolder(@NotNull String projectId, @NotNull String folderPath) throws DBException {
super.createProjectDataSourceFolder(projectId, folderPath);
if (credentialsProvider.getActiveUserCredentials() != null) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
List.of(createNodePathFromFolderPath(projectId, folderPath))
)
);
}
}
@Override
public void moveProjectDataSourceFolder(
@NotNull String projectId,
@NotNull String oldPath,
@NotNull String newPath
) throws DBException {
super.moveProjectDataSourceFolder(projectId, oldPath, newPath);
if (credentialsProvider.getActiveUserCredentials() != null) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.delete(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
List.of(createNodePathFromFolderPath(projectId, oldPath))
)
);
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
List.of(createNodePathFromFolderPath(projectId, newPath))
)
);
}
}
@Override
public void deleteProjectDataSourceFolders(@NotNull String projectId, @NotNull String[] folderPaths, boolean dropContents)
throws DBException {
super.deleteProjectDataSourceFolders(projectId, folderPaths, dropContents);
if (credentialsProvider.getActiveUserCredentials() != null) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
projectId,
Arrays.stream(folderPaths).map(
p -> createNodePathFromFolderPath(projectId, p)
).collect(Collectors.toList())
)
);
}
}
private String createNodePathFromFolderPath(String projectId, String folderPath) {
return DBNLocalFolder.makeLocalFolderItemPath(projectId, folderPath);
}
private void sendDataSourcesConfigUpdatedEvent(
@NotNull DBPDataSourceRegistry registry,
@NotNull Map<String, DataSourceDescriptor> oldDataSources,
@Nullable DataSourceParseResults parseResults
) {
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null || oldDataSources.isEmpty()) {
return;
}
List<String> updatedConfigurationDataSourceIds = new ArrayList<>();
List<String> updatedNameDataSourceIds = new ArrayList<>();
List<String> updatedInternalConfigurationDataSourceIds = new ArrayList<>();
for (Map.Entry<String, DataSourceDescriptor> entry : oldDataSources.entrySet()) {
String dsId = entry.getKey();
DataSourceDescriptor oldDs = entry.getValue();
DataSourceDescriptor newDs = (DataSourceDescriptor) registry.getDataSource(dsId);
if (newDs == null) {
continue;
}
if (!oldDs.equalConfiguration(newDs)) {
updatedConfigurationDataSourceIds.add(dsId);
} else if (!oldDs.isLooselyEqualTo(newDs)) {
updatedNameDataSourceIds.add(dsId);
} else if (!oldDs.equalInternalConfiguration(newDs)) {
updatedInternalConfigurationDataSourceIds.add(dsId);
}
}
if (!updatedConfigurationDataSourceIds.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.update(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
updatedConfigurationDataSourceIds,
WSDataSourceProperty.CONFIGURATION
)
);
}
if (!updatedNameDataSourceIds.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.update(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
updatedNameDataSourceIds,
WSDataSourceProperty.NAME
)
);
}
if (!updatedInternalConfigurationDataSourceIds.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.update(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
updatedInternalConfigurationDataSourceIds,
WSDataSourceProperty.INTERNAL
)
);
}
if (!parseResults.addedDataSources.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
updatedNameDataSourceIds,
WSDataSourceProperty.CONFIGURATION
)
);
}
if (!parseResults.removedDataSources.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDataSourceEvent.delete(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
updatedNameDataSourceIds,
WSDataSourceProperty.CONFIGURATION
)
);
}
if (!parseResults.addedFolders.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.create(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
parseResults.addedFolders.stream().map(
f -> createNodePathFromFolderPath(registry.getProject().getId(), f.getFolderPath())
).toList()
)
);
}
if (!parseResults.removedFolders.isEmpty()) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSDatasourceFolderEvent.delete(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId(),
parseResults.removedFolders.stream().map(
f -> createNodePathFromFolderPath(registry.getProject().getId(), f.getFolderPath())
).toList()
)
);
}
}
@NotNull
@Override
public RMResource[] listResources(
@@ -21,7 +21,6 @@ import io.cloudbeaver.model.WebConnectionFolderInfo;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.utils.CommonUtils;
public class WebConnectionFolderUtils {
@@ -45,9 +44,4 @@ public class WebConnectionFolderUtils {
throw new DBWebException("Folder name '" + folderName + "' contains illegal characters: /");
}
}
public static DBPDataSourceFolder createFolder(WebConnectionFolderInfo parentFolder, String newName, DBPDataSourceRegistry registry) throws DBWebException {
DBPDataSourceFolder folder = registry.addFolder(parentFolder == null ? null : parentFolder.getDataSourceFolder(), newName);
return folder;
}
}
@@ -41,30 +41,5 @@ public class WSUserEventHandler<EVENT extends WSAbstractEvent> implements WSEven
case WSUserDisabledEvent e -> sessionManager.closeUserSession(e);
default -> { }
}
switch (event.getId()) {
case WSUserCloseSessionsEvent.ID:
if (event instanceof WSUserCloseSessionsEvent closeSessionsEvent) {
if (closeSessionsEvent.getSessionIds().isEmpty()) {
sessionManager.closeAllSessions(closeSessionsEvent.getSessionId());
} else {
sessionManager.closeSessions(closeSessionsEvent.getSessionIds());
}
}
break;
case WSUserDeletedEvent.ID:
if (event instanceof WSUserDeletedEvent userDeletedEvent) {
sessionManager.closeUserSession(userDeletedEvent);
}
break;
case WSUserDisabledEvent.ID:
if (event instanceof WSUserDisabledEvent userDisabledEvent) {
sessionManager.closeUserSession(userDisabledEvent);
}
break;
default:
break;
}
}
}
@@ -318,9 +318,11 @@ public class WebServiceUtils extends WebCommonUtils {
return container.getName() + " [" + container.getId() + "]";
}
public static void updateConfigAndRefreshDatabases(WebSession session, String projectId) throws DBWebException {
public static void refreshDatabases(WebSession session, String projectId) throws DBWebException {
DBNProject projectNode = session.getNavigatorModelOrThrow().getRoot().getProjectNode(session.getProjectById(projectId));
DBNModel.updateConfigAndRefreshDatabases(projectNode.getDatabases());
if (projectNode != null) {
projectNode.getDatabases().refreshChildren();
}
}
public static boolean isGlobalProject(DBPProject project) {
@@ -39,10 +39,7 @@ public class WSDataSourceUpdatedEventHandlerImpl extends WSAbstractProjectEventH
log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId());
return;
}
sendEvent = project.updateProjectDataSources(
event.getDataSourceIds(),
event.getId()
);
sendEvent = project.updateProjectDataSources(event);
}
if (sendEvent) {
activeUserSession.addSessionEvent(event);
@@ -23,7 +23,6 @@ import io.cloudbeaver.model.WebNetworkHandlerConfigInput;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.utils.ServletAppUtils;
import io.cloudbeaver.utils.WebDataSourceUtils;
import io.cloudbeaver.utils.WebEventUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -37,8 +36,6 @@ import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.secret.DBSSecretController;
import org.jkiss.dbeaver.model.secret.DBSSecretValue;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob;
@@ -99,13 +96,6 @@ public class ConnectionControllerCE implements ConnectionController {
WebConnectionInfo connectionInfo = project.addConnection(newDataSource);
webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo(
newDataSource));
WebEventUtils.addDataSourceUpdatedEvent(
webSession.getProjectById(projectId),
webSession,
connectionInfo.getId(),
WSConstants.EventAction.CREATE,
WSDataSourceProperty.CONFIGURATION
);
log.info(String.format(
"New connection was created: [info=%s, user=%s]",
WebServiceUtils.getConnectionContainerInfo(newDataSource),
@@ -189,7 +179,6 @@ public class ConnectionControllerCE implements ConnectionController {
DBPDataSourceRegistry sessionRegistry
) throws DBWebException {
WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, config.getConnectionId());
boolean sendEvent = !((DataSourceDescriptor) dataSource).equalSettings(getOldDataSource(dataSource));
if (!isSharedCredentials(dataSource)) {
// secret controller is responsible for notification, password changes applied after checks
WebServiceUtils.saveAuthProperties(
@@ -200,24 +189,12 @@ public class ConnectionControllerCE implements ConnectionController {
config.isSharedCredentials()
);
}
WSDataSourceProperty property = getDatasourceEventProperty(getOldDataSource(dataSource), dataSource);
try {
sessionRegistry.updateDataSource(dataSource);
sessionRegistry.checkForErrors();
} catch (DBException e) {
throw new DBWebException("Failed to update connection", e);
}
if (sendEvent) {
WebEventUtils.addDataSourceUpdatedEvent(
webSession.getProjectById(projectId),
webSession,
connectionInfo.getId(),
WSConstants.EventAction.UPDATE,
property
);
}
log.info(String.format(
"Connection updated: [info=%s, userId=%s]",
WebServiceUtils.getConnectionContainerInfo(dataSource),
@@ -237,13 +214,6 @@ public class ConnectionControllerCE implements ConnectionController {
webSession.addInfoMessage("Delete connection - " +
WebServiceUtils.getConnectionContainerInfo(connectionInfo.getDataSourceContainer()));
closeAndDeleteConnection(webSession, projectId, connectionId, true);
WebEventUtils.addDataSourceUpdatedEvent(
webSession.getProjectById(projectId),
webSession,
connectionId,
WSConstants.EventAction.DELETE,
WSDataSourceProperty.CONFIGURATION
);
log.info(String.format(
"Connection deleted: [info=%s, userId=%s]",
@@ -360,23 +330,6 @@ public class ConnectionControllerCE implements ConnectionController {
return project;
}
private WSDataSourceProperty getDatasourceEventProperty(
DataSourceDescriptor oldDataSource,
DBPDataSourceContainer dataSource
) {
if (!oldDataSource.equalConfiguration((DataSourceDescriptor) dataSource)) {
return WSDataSourceProperty.CONFIGURATION;
}
var nameChanged = !CommonUtils.equalObjects(oldDataSource.getName(), dataSource.getName());
var descriptionChanged = !CommonUtils.equalObjects(oldDataSource.getDescription(), dataSource.getDescription());
if (nameChanged && descriptionChanged) {
return WSDataSourceProperty.CONFIGURATION;
}
return nameChanged ? WSDataSourceProperty.NAME : WSDataSourceProperty.CONFIGURATION;
}
@NotNull
private WebConnectionInfo closeAndDeleteConnection(
@NotNull WebSession webSession,
@@ -27,7 +27,10 @@ import io.cloudbeaver.server.WebAppUtils;
import io.cloudbeaver.server.WebApplication;
import io.cloudbeaver.service.core.DBWServiceCore;
import io.cloudbeaver.service.security.SMUtils;
import io.cloudbeaver.utils.*;
import io.cloudbeaver.utils.ServletAppUtils;
import io.cloudbeaver.utils.WebCommonUtils;
import io.cloudbeaver.utils.WebConnectionFolderUtils;
import io.cloudbeaver.utils.WebDataSourceUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jkiss.code.NotNull;
@@ -41,14 +44,16 @@ 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.DBPDriver;
import org.jkiss.dbeaver.model.navigator.*;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.model.navigator.DBNDataSource;
import org.jkiss.dbeaver.model.navigator.DBNModel;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
import org.jkiss.dbeaver.model.net.DBWNetworkHandler;
import org.jkiss.dbeaver.model.net.DBWTunnel;
import org.jkiss.dbeaver.model.net.ssh.SSHSession;
import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
@@ -56,7 +61,6 @@ import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
import org.jkiss.dbeaver.registry.settings.ProductSettingsRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.CommonUtils;
import java.util.*;
@@ -393,15 +397,7 @@ public class WebServiceCore implements DBWServiceCore {
dataSourceRegistry.addDataSource(newDataSource);
dataSourceRegistry.checkForErrors();
WebConnectionInfo connectionInfo = project.addConnection(newDataSource);
WebEventUtils.addDataSourceUpdatedEvent(
webSession.getProjectById(projectId),
webSession,
connectionInfo.getId(),
WSConstants.EventAction.CREATE,
WSDataSourceProperty.CONFIGURATION
);
return connectionInfo;
return project.addConnection(newDataSource);
} catch (DBException e) {
throw new DBWebException("Error copying connection", e);
}
@@ -536,25 +532,18 @@ public class WebServiceCore implements DBWServiceCore {
) throws DBWebException {
WebConnectionFolderUtils.validateConnectionFolder(folderName);
session.addInfoMessage("Create new folder");
WebConnectionFolderInfo parentNode = null;
WebConnectionFolderInfo parentFolder = null;
try {
if (parentPath != null) {
parentNode = WebConnectionFolderUtils.getFolderInfo(session, projectId, parentPath);
parentFolder = WebConnectionFolderUtils.getFolderInfo(session, projectId, parentPath);
}
WebProjectImpl project = getProjectById(session, projectId);
DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry();
DBPDataSourceFolder newFolder = WebConnectionFolderUtils.createFolder(parentNode,
folderName,
sessionRegistry);
WebConnectionFolderInfo folderInfo = new WebConnectionFolderInfo(session, newFolder);
WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId);
WebEventUtils.addNavigatorNodeUpdatedEvent(
session.getProjectById(projectId),
session,
DBNLocalFolder.makeLocalFolderItemPath(newFolder),
WSConstants.EventAction.CREATE
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
DBPDataSourceFolder newFolder = registry.addFolder(
parentFolder == null ? null : parentFolder.getDataSourceFolder(), folderName
);
return folderInfo;
WebServiceUtils.refreshDatabases(session, projectId);
return new WebConnectionFolderInfo(session, newFolder);
} catch (DBException e) {
throw new DBWebException(e.getMessage(), e);
}
@@ -565,26 +554,12 @@ public class WebServiceCore implements DBWServiceCore {
@NotNull WebSession session,
@Nullable String projectId,
@NotNull String folderPath,
@NotNull String newName
@NotNull String newPath
) throws DBWebException {
WebConnectionFolderUtils.validateConnectionFolder(newName);
WebConnectionFolderUtils.validateConnectionFolder(newPath);
WebConnectionFolderInfo folderInfo = WebConnectionFolderUtils.getFolderInfo(session, projectId, folderPath);
var oldFolderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder());
folderInfo.getDataSourceFolder().setName(newName);
var newFolderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder());
WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId);
WebEventUtils.addNavigatorNodeUpdatedEvent(
session.getProjectById(projectId),
session,
oldFolderNode,
WSConstants.EventAction.DELETE
);
WebEventUtils.addNavigatorNodeUpdatedEvent(
session.getProjectById(projectId),
session,
newFolderNode,
WSConstants.EventAction.CREATE
);
folderInfo.getDataSourceFolder().setName(newPath);
WebServiceUtils.refreshDatabases(session, projectId);
return folderInfo;
}
@@ -599,17 +574,10 @@ public class WebServiceCore implements DBWServiceCore {
if (folder.getDataSourceRegistry().getProject() != project) {
throw new DBWebException("Global folder '" + folderInfo.getId() + "' cannot be deleted");
}
var folderNode = DBNLocalFolder.makeLocalFolderItemPath(folderInfo.getDataSourceFolder());
session.addInfoMessage("Delete folder");
DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry();
sessionRegistry.removeFolder(folderInfo.getDataSourceFolder(), false);
WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId);
WebEventUtils.addNavigatorNodeUpdatedEvent(
session.getProjectById(projectId),
session,
folderNode,
WSConstants.EventAction.DELETE
);
WebServiceUtils.refreshDatabases(session, projectId);
} catch (DBException e) {
throw new DBWebException(e.getMessage(), e);
}
@@ -624,12 +592,6 @@ public class WebServiceCore implements DBWServiceCore {
DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor) connectionInfo.getDataSourceContainer());
dataSourceDescriptor.setNavigatorSettings(settings);
dataSourceDescriptor.persistConfiguration();
WebEventUtils.addDataSourceUpdatedEvent(
webSession.getProjectById(projectId),
webSession,
id,
WSConstants.EventAction.UPDATE,
WSDataSourceProperty.CONFIGURATION);
return connectionInfo;
}
@@ -656,16 +618,4 @@ public class WebServiceCore implements DBWServiceCore {
}
return project;
}
private void validateDriverLibrariesPresence(@NotNull DBPDataSourceContainer container) throws DBWebException {
if (!DBWorkbench.isDistributed() && container.getDriver().getDriverLoader(container).needsExternalDependencies()) {
throwDriverNotFoundException(container);
}
}
@NotNull
private static String throwDriverNotFoundException(@NotNull DBPDataSourceContainer container) throws DBWebException {
throw new DBWebException("Driver files for %s are not found. Please ask the administrator to download it."
.formatted(container.getDriver().getName()));
}
}
@@ -55,7 +55,6 @@ import org.jkiss.dbeaver.model.struct.DBSObjectFilter;
import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog;
import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.CommonUtils;
@@ -461,28 +460,10 @@ public class WebServiceNavigator implements DBWServiceNavigator {
if (siblings.contains(newName)) {
throw new DBWebException("Name " + newName + " is unavailable or invalid");
}
var oldNodePath = node.getNodeItemPath();
node.rename(session.getProgressMonitor(), newName);
var newNodePath = node.getNodeItemPath();
addNavigatorNodeMoveEvent(session, node, oldNodePath, newNodePath);
return node.getName();
}
private void addNavigatorNodeMoveEvent(@NotNull WebSession session, DBNNode node, String oldNodePath, String newNodePath) {
WebEventUtils.addNavigatorNodeUpdatedEvent(
node.getOwnerProject(),
session,
oldNodePath,
WSConstants.EventAction.DELETE
);
WebEventUtils.addNavigatorNodeUpdatedEvent(
node.getOwnerProject(),
session,
newNodePath,
WSConstants.EventAction.CREATE
);
}
@NotNull
private String renameRmResourceNode(@NotNull WebSession session, DBNNode node, @NotNull String newName) throws DBException {
if (newName.contains("/") || newName.contains("\\")) {
@@ -570,14 +551,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
throw e;
}
} else if (node instanceof DBNLocalFolder) {
var nodePath = node.getNodeItemPath();
node.getOwnerProject().getDataSourceRegistry().removeFolder(((DBNLocalFolder) node).getFolder(), false);
WebEventUtils.addNavigatorNodeUpdatedEvent(
session.getProjectById(projectId),
session,
nodePath,
WSConstants.EventAction.DELETE
);
} else if (node instanceof DBNResourceManagerResource) {
DBNResourceManagerResource rmResource = ((DBNResourceManagerResource) node);
String resourceProjectId = rmResource.getResourceProject().getId();
@@ -592,7 +566,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
}
}
if (containsFolderNodes) {
WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId);
WebServiceUtils.refreshDatabases(session, projectId);
}
return nodes.size();
@@ -645,15 +619,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
folder = localFolderNode.getFolder();
}
dataSourceNode.moveToFolder(folderNode.getOwnerProject(), folder);
node.getOwnerProject().getDataSourceRegistry().updateDataSource(
dataSourceNode.getDataSourceContainer());
WebEventUtils.addDataSourceUpdatedEvent(
node.getOwnerProject(),
session,
dataSourceNode.getDataSourceContainer().getId(),
WSConstants.EventAction.UPDATE,
WSDataSourceProperty.CONFIGURATION
);
node.getOwnerProject().getDataSourceRegistry().updateDataSource(dataSourceNode.getDataSourceContainer());
} else if (node instanceof DBNLocalFolder dbnLocalFolder) {
DBPDataSourceFolder parentFolder = null;
if (folderNode instanceof DBNLocalFolder parentFolderNode) {
@@ -667,15 +633,12 @@ public class WebServiceNavigator implements DBWServiceNavigator {
throw new DBWebException("Node " + folderNodePath + " contains folder with name '" + node.getName() + "'");
}
}
var oldNodePath = node.getNodeItemPath();
node.getOwnerProject().getDataSourceRegistry().moveFolder(
dbnLocalFolder.getFolder().getFolderPath(),
dbnLocalFolder.generateNewFolderPath(parentFolder, dbnLocalFolder.getNodeDisplayName())
);
node.getOwnerProject().getDataSourceRegistry().checkForErrors();
var newNodePath = node.getNodeItemPath();
WebServiceUtils.updateConfigAndRefreshDatabases(session, node.getOwnerProject().getId());
addNavigatorNodeMoveEvent(session, node, oldNodePath, newNodePath);
WebServiceUtils.refreshDatabases(session, node.getOwnerProject().getId());
} else if (node instanceof DBNResourceManagerResource) {
boolean rmNewNode = folderNode instanceof DBNAbstractResourceManagerNode;
DBNResourceManagerResource rmOldNode = (DBNResourceManagerResource) node;
@@ -103,7 +103,7 @@ public class WebSQLCompletionContext implements SQLCompletionContext {
@Override
public boolean isSortAlphabetically() {
return false;
return true;
}
@Override
@@ -67,4 +67,8 @@ public class WebSQLCompletionProposal {
return null;
}
public CompletionProposalBase getProposal() {
return proposal;
}
}
@@ -55,6 +55,7 @@ import org.jkiss.dbeaver.model.sql.parser.SQLParserContext;
import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser;
import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorConfigurationRegistry;
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.struct.DBSDataContainer;
import org.jkiss.dbeaver.model.struct.DBSObject;
@@ -200,6 +201,11 @@ public class WebServiceSQL implements DBWServiceSQL {
for (int i = 0; i < proposals.size(); i++) {
result[i] = new WebSQLCompletionProposal(proposals.get(i));
}
SQLCompletionProposalComparator sqlCompletionProposalComparator = new SQLCompletionProposalComparator(
completionContext.isSortAlphabetically(),
completionContext.isSearchInsideNames()
);
Arrays.sort(result, (o1, o2) -> sqlCompletionProposalComparator.compare(o1.getProposal(), o2.getProposal()));
return result;
} catch (Exception e) {
throw new DBWebException("Error processing SQL proposals", e);
@@ -313,6 +313,7 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
DatabaseTransferConsumer consumer = new DatabaseTransferConsumer(dataContainer);
DatabaseConsumerSettings databaseConsumerSettings = new DatabaseConsumerSettings();
databaseConsumerSettings.setContainer((DBSObjectContainer) dataContainer.getDataSource());
databaseConsumerSettings.setEnableQmLogging(true);
consumer.setSettings(databaseConsumerSettings);
StreamProducerSettings producerSettings = new StreamProducerSettings();
@@ -1,5 +1,20 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { DataGrid as DataGridBase, type ColumnOrColumnGroup, type CellSelectArgs, type DataGridHandle, type ColumnWidth, type ColumnWidths } from 'react-data-grid';
import {
DataGrid as DataGridBase,
type ColumnOrColumnGroup,
type CellSelectArgs,
type DataGridHandle,
type ColumnWidth,
type ColumnWidths,
} from 'react-data-grid';
import { rowRenderer } from './renderers/rowRenderer.js';
import { cellRenderer } from './renderers/cellRenderer.js';
import { DataGridCellHeaderContext, type IDataGridHeaderCellContext } from './DataGridHeaderCellContext.js';
@@ -111,9 +126,12 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
innerGridRef.current?.scrollToCell({ idx: position.colIdx && dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
},
openEditor: (position: ICellPosition) => {
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx }, {
enableEditor: true,
});
innerGridRef.current?.selectCell(
{ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx },
{
enableEditor: true,
},
);
},
}));
@@ -126,7 +144,7 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
}
}
let rows = useMemo(
const rows = useMemo(
() =>
new Array<IInnerRow>(rowsCount).fill({ idx: 0 }).map((_, i) => ({
idx: i,
@@ -142,21 +160,16 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
onCellKeyDown?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx }, event);
}
const isMeasurementRender = columnWidths.size === 0 && columnCount.get() > 0;
if (isMeasurementRender) {
rows = rows.slice(0, 100);
}
return (
<HeaderDnDContext value={dndHeaderContext}>
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState, onHeaderKeyDown }}>
<DataGridCellHeaderContext
value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState, onHeaderKeyDown }}
>
<DataGridBase
ref={innerGridRef}
columns={dndHeaderContext.columns}
enableVirtualization={!isMeasurementRender}
rows={rows}
className={className}
headerRowHeight={getHeaderHeight?.()}
@@ -29,6 +29,6 @@
padding-inline: calc(var(--tw-spacing) * 3);
padding-block: calc(var(--tw-spacing) * 2);
gap: calc(var(--tw-spacing) * 2);
line-height: 1;
line-height: 1.25;
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -71,14 +71,14 @@ export class LocalizationService extends Bootstrap {
this.localeProviders.push(provider);
}
setSupportedLanguages(locales: ILocale[]) {
setSupportedLanguages(locales: ILocale[]): void {
this.supportedLanguages = locales;
if (this.supportedLanguages.length === 0) {
this.supportedLanguages = [DEFAULT_LOCALE];
}
}
setLanguage(lang: string) {
setLanguage(lang: string): void {
this.language = lang;
}
@@ -142,7 +142,6 @@ export class LocalizationService extends Bootstrap {
name: 'Vietnamese',
nativeName: 'Tiếng Việt',
},
]);
this.addProvider(this.coreProvider.bind(this));
}
@@ -47,12 +47,7 @@ export const MenuActionElement = observer<IMenuActionElementProps>(function Menu
focusable
onClick={handleClick}
>
<MenuItemElement
label={actionInfo.label}
icon={<Radio checked={checked} size="small" />}
tooltip={actionInfo.tooltip}
loading={loading}
/>
<MenuItemElement label={actionInfo.label} icon={<Radio checked={checked} size="small" />} tooltip={actionInfo.tooltip} loading={loading} />
</MenuItemRadio>
);
}
@@ -72,7 +67,13 @@ export const MenuActionElement = observer<IMenuActionElementProps>(function Menu
focusable
onClick={handleClick}
>
<MenuItemElement label={actionInfo.label} icon={<Checkbox checked={checked} size="small" />} tooltip={actionInfo.tooltip} loading={loading} />
<MenuItemElement
label={actionInfo.label}
binding={binding}
icon={<Checkbox checked={checked} size="small" />}
tooltip={actionInfo.tooltip}
loading={loading}
/>
</MenuItemCheckbox>
);
}
@@ -21,11 +21,13 @@ import {
useListKeyboardNavigation,
useMergeRefs,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { type IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context';
import {
DATA_CONTEXT_MENU_NESTED,
DATA_CONTEXT_SUBMENU_ITEM,
getBindingLabel,
type IMenuActionItem,
type IMenuData,
type IMenuInfo,
@@ -146,11 +148,14 @@ export interface IMenuBarActionProps {
export const MenuBarAction = registry(
observer<IMenuBarActionProps>(function MenuBarAction({ item, parentMenuInfo, submenu, className }) {
const translate = useTranslate();
const actionInfo = item.action.actionInfo;
const loading = item.action.isLoading();
/** @deprecated must be refactored (#1)*/
const displayLabel = item.action.isLabelVisible();
const binding = item.action.binding ? getBindingLabel(item.action.binding.binding) : null;
const title = translate(actionInfo.tooltip ?? actionInfo.label);
function handleClick() {
item.events?.onSelect?.();
@@ -165,7 +170,7 @@ export const MenuBarAction = registry(
label={actionInfo.label}
displayLabel={displayLabel}
icon={actionInfo.icon ?? parentMenuInfo?.icon}
title={actionInfo.tooltip ?? parentMenuInfo?.tooltip}
title={binding ? `${title} (${binding})` : title}
disabled={item.disabled}
loading={loading}
submenu={submenu}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -8,6 +8,6 @@
import { createAction } from '../createAction.js';
export const ACTION_COLLAPSE_ALL = createAction('collapse-all', {
label: 'collapse-all',
label: 'app_navigationTree_action_collapse_all',
icon: '/icons/collapse_sm.svg',
});
@@ -0,0 +1,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createKeyBinding } from '../createKeyBinding.js';
export const KEY_BINDING_ADD = createKeyBinding({
id: 'add',
keys: ['alt+r'],
});
@@ -0,0 +1,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createKeyBinding } from '../createKeyBinding.js';
export const KEY_BINDING_DUPLICATE = createKeyBinding({
id: 'duplicate',
keys: ['alt+shift+r'],
});
@@ -1,18 +1,67 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { getOSSpecificKeys } from './getCommonAndOSSpecificKeys.js';
import { getOS, OperatingSystem } from '@cloudbeaver/core-utils';
import { getCommonAndOSSpecificKeys } from './getCommonAndOSSpecificKeys.js';
import type { IKeyBinding } from './IKeyBinding.js';
export function getBindingLabel(binding: IKeyBinding) {
let bindingLabel = getOSSpecificKeys(binding) ?? binding.keys;
if (Array.isArray(bindingLabel)) {
bindingLabel = bindingLabel[0];
}
const FORMAT_SHORTCUT_KEYS_MAP: Record<string, string> = {
comma: ',',
slash: '/',
backslash: '\\',
backspace: '⌫',
tab: 'tab',
clear: 'clear',
enter: '↵',
return: '↵',
escape: 'escape',
esc: 'escape',
space: '␣',
up: '↑',
down: '↓',
left: '←',
right: '→',
pageup: 'pageup',
pagedown: 'pagedown',
del: '⌦',
delete: '⌦',
};
const SOURCE_DIVIDER_REGEXP = /\+/gi;
const APPLIED_DIVIDER = ' + ';
return bindingLabel?.replace(/\+/g, ' + ').replace(/\b\w/g, letter => letter.toUpperCase());
function transformKeys(keyBinding: IKeyBinding): string[] {
return getCommonAndOSSpecificKeys(keyBinding).map(shortcut =>
shortcut.split(SOURCE_DIVIDER_REGEXP).map(formatKeyToDisplayKey).join(APPLIED_DIVIDER).toLocaleUpperCase(),
);
}
function formatKeyToDisplayKey(code: string): string {
const lowerCaseCode = code.toLowerCase();
const OS = getOS();
switch (lowerCaseCode) {
case 'mod':
if (OS === OperatingSystem.windowsOS || OS === OperatingSystem.linuxOS) {
return 'CTRL';
}
if (OS === OperatingSystem.macOS) {
return 'CMD';
}
return code;
case 'alt':
if (OS === OperatingSystem.macOS) {
return 'OPTION';
}
return 'ALT';
default:
return FORMAT_SHORTCUT_KEYS_MAP[lowerCaseCode] ?? code;
}
}
export function getBindingLabel(binding: IKeyBinding): string | undefined {
return transformKeys(binding)[0];
}
+2
View File
@@ -37,6 +37,8 @@ export * from './Action/KeyBinding/Bindings/KEY_BINDING_OPEN_IN_TAB.js';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_REDO.js';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_UNDO.js';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_SAVE.js';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_ADD.js';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_DUPLICATE.js';
export * from './Action/KeyBinding/KeyBindingService.js';
export * from './Action/KeyBinding/createKeyBinding.js';
export * from './Action/ActionService.js';
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@cloudbeaver/core-di';
import { ACTION_EDIT, ActionService, MenuService } from '@cloudbeaver/core-view';
import { ACTION_EDIT, ActionService, getBindingLabel, KEY_BINDING_ADD, KEY_BINDING_DUPLICATE, MenuService, type IAction } from '@cloudbeaver/core-view';
import {
DATA_CONTEXT_DV_DDM,
DATA_CONTEXT_DV_DDM_RESULT_INDEX,
@@ -22,6 +22,8 @@ import {
ResultSetSelectAction,
ResultSetViewAction,
} from '@cloudbeaver/plugin-data-viewer';
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ACTION_DATA_GRID_EDITING_ADD_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_ADD_ROW.js';
import { ACTION_DATA_GRID_EDITING_DELETE_ROW } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_DELETE_ROW.js';
@@ -32,13 +34,16 @@ import { ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW } from '../Actions/Editing
import { ACTION_DATA_GRID_EDITING_SET_TO_NULL } from '../Actions/Editing/ACTION_DATA_GRID_EDITING_SET_TO_NULL.js';
import { MENU_DATA_GRID_EDITING } from './MENU_DATA_GRID_EDITING.js';
@injectable(() => [ActionService, MenuService])
@injectable(() => [ActionService, LocalizationService, MenuService])
export class DataGridContextMenuCellEditingService {
constructor(
private readonly actionService: ActionService,
private readonly localizationService: LocalizationService,
private readonly menuService: MenuService,
) { }
register(): void {
this.menuService.addCreator({
root: true,
@@ -140,13 +145,7 @@ export class DataGridContextMenuCellEditingService {
ACTION_DATA_GRID_EDITING_REVERT_SELECTED_ROW,
].includes(action);
},
getActionInfo(context, action) {
if (action === ACTION_EDIT) {
return { ...action.info, label: 'data_grid_table_editing_open_inline_editor', icon: 'edit' };
}
return action.info;
},
getActionInfo: this.getActionInfo.bind(this),
handler(context, action) {
const model = context.get(DATA_CONTEXT_DV_DDM)!;
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!;
@@ -188,4 +187,20 @@ export class DataGridContextMenuCellEditingService {
},
});
}
private getActionInfo(context: IDataContextProvider, action: IAction) {
const t = this.localizationService.translate;
if (action === ACTION_DATA_GRID_EDITING_ADD_ROW) {
return { ...action.info, label: 'data_grid_table_editing_row_add', tooltip: t('data_grid_table_editing_row_add') + ' (' + getBindingLabel(KEY_BINDING_ADD) + ')' };
}
if (action === ACTION_DATA_GRID_EDITING_DUPLICATE_ROW) {
return { ...action.info, label: 'data_grid_table_editing_row_add_copy', tooltip: t('data_grid_table_editing_row_add_copy') + ' (' + getBindingLabel(KEY_BINDING_DUPLICATE) + ')' };
}
if (action === ACTION_EDIT) {
return { ...action.info, label: t('data_grid_table_editing_open_inline_editor'), icon: 'edit' };
}
return action.info;
}
}
@@ -31,7 +31,7 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
}
get columnKeys(): IResultSetColumnKey[] {
return this.columns.map(c => ({ index: this.data.columns.indexOf(c) }));
return this.columnsOrder.map(index => ({ index }));
}
get rows(): IResultSetValue[][] {
@@ -14,8 +14,14 @@ import {
ACTION_REVERT,
ACTION_SAVE,
ActionService,
getBindingLabel,
KEY_BINDING_ADD,
KEY_BINDING_DUPLICATE,
MenuService,
type IAction,
} from '@cloudbeaver/core-view';
import { LocalizationService } from '@cloudbeaver/core-localization';
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { DatabaseEditAction } from '../../../DatabaseDataModel/Actions/DatabaseEditAction.js';
import { DatabaseSelectAction } from '../../../DatabaseDataModel/Actions/DatabaseSelectAction.js';
@@ -26,14 +32,15 @@ import { DATA_CONTEXT_DV_PRESENTATION, DataViewerPresentationType } from '../../
import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel.js';
import { DATA_VIEWER_DATA_MODEL_ACTIONS_MENU } from './DATA_VIEWER_DATA_MODEL_ACTIONS_MENU.js';
@injectable(() => [ActionService, MenuService])
@injectable(() => [ActionService, LocalizationService, MenuService])
export class TableFooterMenuService {
constructor(
private readonly actionService: ActionService,
private readonly localizationService: LocalizationService,
private readonly menuService: MenuService,
) {}
) { }
register() {
register(): void {
this.registerEditingActions();
}
@@ -144,63 +151,69 @@ export class TableFooterMenuService {
return false;
},
getActionInfo(context, action) {
switch (action) {
case ACTION_ADD:
return { ...action.info, label: '', icon: '/icons/data_add_sm.svg', tooltip: 'data_viewer_action_edit_add' };
case ACTION_DUPLICATE:
return { ...action.info, label: '', icon: '/icons/data_add_copy_sm.svg', tooltip: 'data_viewer_action_edit_add_copy' };
case ACTION_DELETE:
return { ...action.info, label: '', icon: '/icons/data_delete_sm.svg', tooltip: 'data_viewer_action_edit_delete' };
case ACTION_REVERT:
return { ...action.info, label: '', icon: '/icons/data_revert_sm.svg', tooltip: 'data_viewer_action_edit_revert' };
case ACTION_SAVE:
return { ...action.info, icon: 'table-save' };
case ACTION_CANCEL:
return { ...action.info, icon: '/icons/data_revert_all_sm.svg', tooltip: 'data_viewer_value_revert_title' };
}
return action.info;
},
handler: (context, action) => {
const model = context.get(DATA_CONTEXT_DV_DDM)!;
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!;
const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction);
if (!editor) {
return;
}
const select = model.source.getActionImplementation(resultIndex, DatabaseSelectAction);
const selectedElements = getActiveElements(model, resultIndex);
switch (action) {
case ACTION_ADD: {
editor.add(select?.getFocusedElement());
break;
}
case ACTION_DUPLICATE: {
editor.duplicate(...selectedElements);
break;
}
case ACTION_DELETE: {
editor.delete(...selectedElements);
break;
}
case ACTION_REVERT: {
editor.revert(...selectedElements);
break;
}
case ACTION_SAVE:
model.save().catch(() => {});
break;
case ACTION_CANCEL: {
editor.clear();
break;
}
}
},
getActionInfo: this.tableFooterMenuGetActionInfo.bind(this),
handler: this.tableFooterMenuActionHandler.bind(this),
});
}
private tableFooterMenuActionHandler(context: IDataContextProvider, action: IAction) {
const model = context.get(DATA_CONTEXT_DV_DDM)!;
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!;
const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction);
if (!editor) {
return;
}
const select = model.source.getActionImplementation(resultIndex, DatabaseSelectAction);
const selectedElements = getActiveElements(model, resultIndex);
switch (action) {
case ACTION_ADD: {
editor.add(select?.getFocusedElement());
break;
}
case ACTION_DUPLICATE: {
editor.duplicate(...selectedElements);
break;
}
case ACTION_DELETE: {
editor.delete(...selectedElements);
break;
}
case ACTION_REVERT: {
editor.revert(...selectedElements);
break;
}
case ACTION_SAVE:
model.save().catch(() => { });
break;
case ACTION_CANCEL: {
editor.clear();
break;
}
}
}
private tableFooterMenuGetActionInfo(context: IDataContextProvider, action: IAction) {
const t = this.localizationService.translate;
switch (action) {
case ACTION_ADD:
return { ...action.info, label: '', icon: '/icons/data_add_sm.svg', tooltip: t('data_viewer_action_edit_add') + ' (' + getBindingLabel(KEY_BINDING_ADD) + ')' };
case ACTION_DUPLICATE:
return { ...action.info, label: '', icon: '/icons/data_add_copy_sm.svg', tooltip: t('data_viewer_action_edit_add_copy') + ' (' + getBindingLabel(KEY_BINDING_DUPLICATE) + ')' };
case ACTION_DELETE:
return { ...action.info, label: '', icon: '/icons/data_delete_sm.svg', tooltip: t('data_viewer_action_edit_delete') };
case ACTION_REVERT:
return { ...action.info, label: '', icon: '/icons/data_revert_sm.svg', tooltip: t('data_viewer_action_edit_revert') };
case ACTION_SAVE:
return { ...action.info, icon: 'table-save' };
case ACTION_CANCEL:
return { ...action.info, icon: '/icons/data_revert_all_sm.svg', tooltip: t('data_viewer_value_revert_title') };
}
return action.info;
}
}
function getActiveElements(model: IDatabaseDataModel, resultIndex: number): unknown[] {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -197,7 +197,7 @@ export const TableViewer = observer<TableViewerProps, HTMLDivElement>(
const isStatistics = result?.loadedFully && !result.data;
const resultExist = dataModel.source.hasResult(resultIndex);
const overlay = dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat;
const overlay = getComputed(() => dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat);
const valuePanelDisplayed =
valuePresentation &&
(valuePresentation.dataFormat === undefined || valuePresentation.dataFormat === dataFormat) &&
@@ -8,12 +8,10 @@
import { UserDataService } from '@cloudbeaver/core-authentication';
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
import {
ACTION_COLLAPSE_ALL,
ACTION_FILTER,
ActionService,
getBindingLabel,
type IAction,
KeyBindingService,
MenuService,
@@ -27,15 +25,14 @@ import { createElementsTreeSettings, validateElementsTreeSettings } from './Navi
import { DATA_CONTEXT_NAV_TREE_ROOT } from './NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT.js';
import { KEY_BINDING_ENABLE_FILTER } from './NavigationTreeSettings/KEY_BINDING_ENABLE_FILTER.js';
@injectable(() => [ActionService, KeyBindingService, UserDataService, MenuService, LocalizationService])
@injectable(() => [ActionService, KeyBindingService, UserDataService, MenuService])
export class ElementsTreeToolsMenuService {
constructor(
private readonly actionService: ActionService,
private readonly keyBindingService: KeyBindingService,
private readonly userDataService: UserDataService,
private readonly menuService: MenuService,
private readonly localizationService: LocalizationService,
) {}
) { }
register() {
this.actionService.addHandler({
@@ -53,20 +50,6 @@ export class ElementsTreeToolsMenuService {
return false;
},
getActionInfo: (context, action) => {
switch (action) {
case ACTION_COLLAPSE_ALL: {
const bindingLabel = getBindingLabel(KEY_BINDING_COLLAPSE_ALL);
const tooltip = this.localizationService.translate('app_navigationTree_action_collapse_all') + (bindingLabel ? ` (${bindingLabel})` : '');
return {
...action.info,
tooltip,
};
}
}
return action.info;
},
handler: this.elementsTreeActionHandler.bind(this),
});
@@ -135,7 +118,7 @@ export class ElementsTreeToolsMenuService {
state.filter = !state.filter;
}
private async elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) {
private elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) {
const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE);
if (tree === undefined) {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -15,9 +15,9 @@ export function navigationTreeDuplicateFilter(navNodeViewService: NavNodeViewSer
const { nodes, duplicates } = navNodeViewService.filterDuplicates(children);
untracked(() => {
navNodeViewService.logDuplicates(node.id, duplicates);
navNodeViewService.logDuplicates(node.id, Array.from(duplicates));
});
return nodes;
return Array.from(nodes);
};
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -13,8 +13,8 @@ import { NotificationService } from '@cloudbeaver/core-events';
import type { INavNodeFolderTransform, NavNodeFolderTransformFn, NavNodeTransformView } from './IFolderTransform.js';
export interface INodeDuplicateList {
nodes: string[];
duplicates: string[];
nodes: Set<string>;
duplicates: Set<string>;
}
export interface INodeLimitedList {
@@ -59,10 +59,10 @@ export class NavNodeViewService {
const { nodes, duplicates } = this.filterDuplicates(children);
untracked(() => {
this.logDuplicates(nodeId, duplicates);
this.logDuplicates(nodeId, Array.from(duplicates));
});
return nodes;
return Array.from(nodes);
},
});
}
@@ -80,25 +80,24 @@ export class NavNodeViewService {
}
filterDuplicates(nodes: string[]): INodeDuplicateList {
const seen = new Set<string>();
const duplicatesSet = new Set<string>();
const nextChildren: string[] = [];
const duplicates: string[] = [];
for (const child of nodes) {
const isDuplicate = duplicates.includes(child);
if (nextChildren.includes(child) || isDuplicate) {
if (!isDuplicate) {
duplicates.push(child);
nextChildren.splice(nextChildren.indexOf(child), 1);
}
if (seen.has(child)) {
duplicatesSet.add(child);
} else {
seen.add(child);
nextChildren.push(child);
}
}
const uniqueChildren = new Set(nextChildren.filter(child => !duplicatesSet.has(child)));
return {
nodes: nextChildren,
duplicates,
nodes: uniqueChildren,
duplicates: duplicatesSet,
};
}
@@ -8,6 +8,6 @@
import { createAction } from '@cloudbeaver/core-view';
export const ACTION_LINK_OBJECT = createAction('link-object', {
label: 'link-object',
label: 'app_navigationTree_action_link_with_editor',
icon: '/icons/reveal_in_explorer_view_sm.svg',
});
@@ -8,14 +8,13 @@
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { NavigationTreeService, DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ActionService, getBindingLabel, type IAction, KeyBindingService, MenuService } from '@cloudbeaver/core-view';
import { ActionService, type IAction, KeyBindingService, MenuService } from '@cloudbeaver/core-view';
import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch';
import { ACTION_LINK_OBJECT } from './ACTION_LINK_OBJECT.js';
import { KEY_BINDING_LINK_OBJECT } from './KEY_BINDING_LINK_OBJECT.js';
@injectable(() => [ActionService, KeyBindingService, NavigationTreeService, ConnectionSchemaManagerService, MenuService, LocalizationService])
@injectable(() => [ActionService, KeyBindingService, NavigationTreeService, ConnectionSchemaManagerService, MenuService])
export class ObjectViewerNavTreeLinkMenuService extends Bootstrap {
constructor(
private readonly actionService: ActionService,
@@ -23,7 +22,6 @@ export class ObjectViewerNavTreeLinkMenuService extends Bootstrap {
private readonly navigationTreeService: NavigationTreeService,
private readonly connectionSchemaManagerService: ConnectionSchemaManagerService,
private readonly menuService: MenuService,
private readonly localizationService: LocalizationService,
) {
super();
}
@@ -35,21 +33,6 @@ export class ObjectViewerNavTreeLinkMenuService extends Bootstrap {
id: 'object-viewer-nav-tree-link-handler',
contexts: [DATA_CONTEXT_ELEMENTS_TREE],
actions: [ACTION_LINK_OBJECT],
getActionInfo: (context, action) => {
switch (action) {
case ACTION_LINK_OBJECT: {
const bindingLabel = getBindingLabel(KEY_BINDING_LINK_OBJECT);
const tooltip =
this.localizationService.translate('app_navigationTree_action_link_with_editor') + (bindingLabel ? ` (${bindingLabel})` : '');
return {
...action.info,
tooltip,
};
}
}
return action.info;
},
isHidden: context => {
const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!;
@@ -35,12 +35,10 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi
const allData = dbObjectLoader.resource.get(pagination.allPages).filter(isDefined);
const { nodes, duplicates } = navNodeViewService.filterDuplicates(allData.map(node => node?.id) || []);
const objects = allData.filter(
object => object && nodes.includes(object.id) && navNodeInfoResource.get(object.id)?.nodeType === nodeType,
) as DBObject[];
const objects = allData.filter(object => object && nodes.has(object.id) && navNodeInfoResource.get(object.id)?.nodeType === nodeType) as DBObject[];
useEffect(() => {
navNodeViewService.logDuplicates(nodeId, duplicates);
navNodeViewService.logDuplicates(nodeId, Array.from(duplicates));
});
return (
@@ -38,15 +38,15 @@ export const ObjectPropertyTable = observer<ObjectPropertyTableProps>(function O
const allData = dbObjectLoader.resource.get(pagination.allPages).filter(isDefined);
const { nodes, duplicates } = navNodeViewService.filterDuplicates(allData.map(node => node?.id) || []);
const objects = allData.filter(node => nodes.includes(node.id)) as DBObject[];
const objects = allData.filter(node => nodes.has(node.id)) as DBObject[];
useEffect(() => {
navNodeViewService.logDuplicates(objectId, duplicates);
navNodeViewService.logDuplicates(objectId, Array.from(duplicates));
});
return (
<>
{nodes.length === 0 ? (
{nodes.size === 0 ? (
<TextPlaceholder>{translate('plugin_object_viewer_table_no_items')}</TextPlaceholder>
) : (
<div className={s(styles, { box: true }, className)}>
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_title', 'Enregistrer comme script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_title', 'Enregistrer comme script'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renommer le script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Lecture du script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Enregistrement du script...'],
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Сохранить как скрипт (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Сохранить как скрипт'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Переименовывание скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Чтение скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Сохранение скрипта...'],
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Lưu dưới dạng script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Lưu dưới dạng script'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Đang đổi tên script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Đang đọc script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Đang lưu script...'],
@@ -1,5 +1,5 @@
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', '保存为脚本 (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', '保存为脚本'],
['plugin_sql_editor_navigation_tab_script_state_renaming', '重命名脚本中...'],
['plugin_sql_editor_navigation_tab_script_state_reading', '读取脚本中...'],
['plugin_sql_editor_navigation_tab_script_state_saving', '保存脚本中...'],
@@ -46,6 +46,7 @@ import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js';
import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.js';
import { SQL_EDITOR_TOOLS_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MENU.js';
import { SQL_EDITOR_TOOLS_MORE_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.js';
import { SQL_EDITOR_ACTIONS_MENU } from './SqlEditor/SQL_EDITOR_ACTIONS_MENU.js';
import { getSqlEditorName } from './getSqlEditorName.js';
import type { ISqlEditorTabState } from './ISqlEditorTabState.js';
import { SqlEditorSettingsService } from './SqlEditorSettingsService.js';
@@ -53,6 +54,12 @@ import { SqlEditorSettingsService } from './SqlEditorSettingsService.js';
const SYNC_DELAY = 5 * 60 * 1000;
const ScriptImportDialog = importLazyComponent(() => import('./SqlEditor/ScriptImportDialog.js').then(m => m.ScriptImportDialog));
const EXECUTIONS_ACTIONS = [
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
ACTION_SQL_EDITOR_EXECUTE_SCRIPT,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
];
@injectable(() => [
MenuService,
@@ -207,6 +214,15 @@ export class MenuBootstrap extends Bootstrap {
getItems: (context, items) => [...items, ACTION_SAVE],
});
this.menuService.addCreator({
menus: [SQL_EDITOR_ACTIONS_MENU],
contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE],
getItems: (context, items) => [
...items,
...EXECUTIONS_ACTIONS,
],
});
this.keyBindingService.addKeyBindingHandler({
id: 'sql-editor-save',
binding: KEY_BINDING_SAVE,
@@ -227,13 +243,10 @@ export class MenuBootstrap extends Bootstrap {
this.actionService.addHandler({
id: 'sql-editor-actions',
actions: [
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
ACTION_SQL_EDITOR_EXECUTE_SCRIPT,
...EXECUTIONS_ACTIONS,
ACTION_SQL_EDITOR_FORMAT,
ACTION_REDO,
ACTION_UNDO,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
],
contexts: [DATA_CONTEXT_SQL_EDITOR_DATA],
isActionApplicable: (contexts, action): boolean => {
@@ -245,12 +258,7 @@ export class MenuBootstrap extends Bootstrap {
if (
!sqlEditorData.isExecutionAllowed &&
[
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
ACTION_SQL_EDITOR_EXECUTE_SCRIPT,
ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN,
].includes(action)
EXECUTIONS_ACTIONS.includes(action)
) {
return false;
}
@@ -259,6 +267,11 @@ export class MenuBootstrap extends Bootstrap {
return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.script) && !sqlEditorData.activeSegmentMode.activeSegmentMode;
}
if (action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN) {
return !!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) &&
!!sqlEditorData.dialect?.supportsExplainExecutionPlan;
}
// TODO we have to add check for output action ?
if (
!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) &&
@@ -271,6 +284,11 @@ export class MenuBootstrap extends Bootstrap {
},
isDisabled: (context, action) => {
const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!;
if (EXECUTIONS_ACTIONS.includes(action)) {
return data.isDisabled || data.isScriptEmpty;
}
switch (action) {
case ACTION_SQL_EDITOR_FORMAT:
return data.isDisabled || data.isScriptEmpty || data.readonly;
@@ -278,6 +296,15 @@ export class MenuBootstrap extends Bootstrap {
return false;
},
getActionInfo: (context, action) => {
if (EXECUTIONS_ACTIONS.includes(action)) {
return {
...action.info,
label: '',
};
}
return action.info;
},
handler: this.sqlEditorActionHandler.bind(this),
});
@@ -7,14 +7,18 @@
*/
import { observer } from 'mobx-react-lite';
import { ActionIconButton, getComputed, preventFocusHandler, s, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { preventFocusHandler, s, useS } from '@cloudbeaver/core-blocks';
import { useDataContextLink } from '@cloudbeaver/core-data-context';
import { useMenu } from '@cloudbeaver/core-view';
import type { ISqlEditorTabState } from '../ISqlEditorTabState.js';
import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js';
import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA.js';
import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE.js';
import type { ISQLEditorData } from './ISQLEditorData.js';
import style from './SQLEditorActions.module.css';
import { SqlEditorActionsMenu } from './SqlEditorActionsMenu.js';
import { SqlEditorTools } from './SqlEditorTools.js';
import { SQL_EDITOR_ACTIONS_MENU } from './SQL_EDITOR_ACTIONS_MENU.js';
interface Props {
data: ISQLEditorData;
@@ -22,58 +26,21 @@ interface Props {
className?: string;
}
export const SQLEditorActions = observer<Props>(function SQLEditorActions({ data, state }) {
export const SQLEditorActions = observer<Props>(function SQLEditorActions({ data, state, className }) {
const styles = useS(style);
const translate = useTranslate();
const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode);
const disabled = getComputed(() => data.isScriptEmpty || data.isDisabled);
const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = data.isExecutionAllowed;
const menu = useMenu({ menu: SQL_EDITOR_ACTIONS_MENU });
useDataContextLink(menu.context, (context, id) => {
context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state, id);
});
useDataContextLink(menu.context, (context, id) => {
context.set(DATA_CONTEXT_SQL_EDITOR_DATA, data, id);
});
return (
<div className={s(styles, { container: true })}>
<div className={s(styles, { container: true }, className)}>
<div className={s(styles, { actions: true })} onMouseDown={preventFocusHandler}>
{isExecutable && (
<>
{isQuery && (
<>
<ActionIconButton
name="/icons/sql_exec.svg"
disabled={disabled}
title={translate('sql_editor_sql_execution_button_tooltip')}
img
onClick={data.executeQuery}
/>
<ActionIconButton
name="/icons/sql_exec_new.svg"
disabled={disabled}
title={translate('sql_editor_sql_execution_new_tab_button_tooltip')}
img
onClick={data.executeQueryNewTab}
/>
</>
)}
<ActionIconButton
name="/icons/sql_script_exec.svg"
disabled={disabled}
hidden={isActiveSegmentMode}
title={translate('sql_editor_sql_execution_script_button_tooltip')}
img
onClick={data.executeScript}
/>
{isQuery && data.dialect?.supportsExplainExecutionPlan && (
<ActionIconButton
name="/icons/sql_execution_plan.svg"
disabled={disabled}
hidden={isActiveSegmentMode}
title={translate('sql_editor_execution_plan_button_tooltip')}
img
onClick={data.showExecutionPlan}
/>
)}
</>
)}
<SqlEditorActionsMenu state={state} />
<SqlEditorActionsMenu state={state} context={menu.context} />
</div>
<SqlEditorTools data={data} state={state} />
</div>
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,4 +10,5 @@ import { createAction } from '@cloudbeaver/core-view';
export const ACTION_SQL_EDITOR_EXECUTE = createAction('sql-editor-execute', {
icon: '/icons/sql_exec.svg',
label: 'sql_editor_sql_execution_button_tooltip',
tooltip: 'sql_editor_sql_execution_button_tooltip',
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,4 +10,5 @@ import { createAction } from '@cloudbeaver/core-view';
export const ACTION_SQL_EDITOR_EXECUTE_NEW = createAction('sql-editor-execute-new', {
icon: '/icons/sql_exec_new.svg',
label: 'sql_editor_sql_execution_new_tab_button_tooltip',
tooltip: 'sql_editor_sql_execution_new_tab_button_tooltip',
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,4 +10,5 @@ import { createAction } from '@cloudbeaver/core-view';
export const ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN = createAction('sql-editor-show-execution-plan', {
icon: '/icons/sql_execution_plan.svg',
label: 'sql_editor_execution_plan_button_tooltip',
tooltip: 'sql_editor_execution_plan_button_tooltip',
});
@@ -14,15 +14,15 @@ export default [
['sql_editor_placeholder', 'Execute query with Ctrl+Enter to see results'],
['sql_editor_hint_empty', 'There is no proposals...'],
['sql_editor_tools_more_menu_tooltip', 'More tools'],
['sql_editor_execution_plan_button_tooltip', 'Explain execution plan (Shift + Ctrl + E)'],
['sql_editor_output_logs_button_tooltip', 'Show server output (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button_tooltip', 'Explain execution plan'],
['sql_editor_output_logs_button_tooltip', 'Show server output'],
['sql_editor_output_logs_tab_title', 'Output'],
['sql_editor_output_logs_input_placeholder', 'Enter a part of a message to search for here'],
['sql_editor_output_logs_wrap_mode', 'Wrap mode'],
['sql_editor_sql_execution_button_tooltip', 'Execute SQL Statement (Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Execute SQL Statement in new tab (Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', 'Execute SQL Script (Alt + X)'],
['sql_editor_sql_format_button_tooltip', 'Format SQL Script (Shift + Ctrl + F)'],
['sql_editor_sql_execution_button_tooltip', 'Execute SQL Statement'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Execute SQL Statement in new tab'],
['sql_editor_sql_execution_script_button_tooltip', 'Execute SQL Script'],
['sql_editor_sql_format_button_tooltip', 'Format SQL Script'],
['sql_editor_sql_execution_script_lock_tooltip', 'Lock editor to prevent modifications'],
['sql_editor_sql_statistics_unavailable', 'Statistics is not available'],
['sql_editor_sql_execution_executed_queries', 'Executed queries:'],
@@ -13,21 +13,21 @@ export default [
['sql_editor_placeholder', 'Exécuter une requête (appuyez sur Ctrl+Entrée pour voir les résultats)'],
['sql_editor_hint_empty', "Il n'y a aucune proposition..."],
['sql_editor_tools_more_menu_tooltip', "Plus d'outils"],
['sql_editor_execution_plan_button', "Afficher le plan d'exécution (Shift + Ctrl + E)"],
['sql_editor_output_logs_button_tooltip', 'Afficher la sortie du serveur (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button', "Afficher le plan d'exécution"],
['sql_editor_output_logs_button_tooltip', 'Afficher la sortie du serveur'],
['sql_editor_output_logs_tab_title', 'Journaux'],
['sql_editor_output_logs_hint', 'Aucun message du serveur'],
['sql_editor_output_logs_wrap_mode', 'Mode Wrap'],
['sql_editor_execution_plan_button_tooltip', "Afficher le plan d'exécution (Shift + Ctrl + E)"],
['sql_editor_execution_plan_button_tooltip', "Afficher le plan d'exécution"],
['sql_editor_execution_plan_button', "Afficher le plan d'exécution"],
['sql_editor_execution_script_button_tooltip', 'Exécuter le script SQL (Alt + X)'],
['sql_editor_execution_script_button_tooltip', 'Exécuter le script SQL'],
['sql_editor_execution_script_button', 'Exécuter le script SQL'],
['sql_editor_execution_query_button_tooltip', 'Exécuter la déclaration SQL (Ctrl + Entrée)'],
['sql_editor_execution_query_button_tooltip', 'Exécuter la déclaration SQL'],
['sql_editor_execution_query_button', 'Exécuter la déclaration SQL'],
['sql_editor_execution_query_new_tab_button_tooltip', 'Exécuter la déclaration SQL dans un nouvel onglet (Ctrl + \\)(Shift + Ctrl + Entrée)'],
['sql_editor_execution_query_new_tab_button_tooltip', 'Exécuter la déclaration SQL dans un nouvel onglet'],
['sql_editor_execution_query_new_tab_button', 'Exécuter dans un nouvel onglet'],
['sql_editor_execution_query_tooltip', 'Exécuter les requêtes'],
['sql_editor_lock_button_tooltip', "Verrouiller l'éditeur pour éviter les modifications (Shift + Ctrl + F)"],
['sql_editor_lock_button_tooltip', "Verrouiller l'éditeur pour éviter les modifications"],
['sql_editor_lock_button', "Verrouiller l'éditeur"],
['sql_editor_sql_statistics_unavailable', 'Les statistiques SQL ne sont pas disponibles'],
['sql_editor_queries_tab_title', 'Requêtes exécutées'],
@@ -14,15 +14,15 @@ export default [
['sql_editor_placeholder', 'Esegui la query con Ctrl+Enter per vedere i risultati'],
['sql_editor_hint_empty', 'There is no proposals...'],
['sql_editor_tools_more_menu_tooltip', 'Più strumenti'],
['sql_editor_execution_plan_button_tooltip', 'Mostra il piano di esecuzione (Shift + Ctrl + E)'],
['sql_editor_output_logs_button_tooltip', 'Show server output (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button_tooltip', 'Mostra il piano di esecuzione'],
['sql_editor_output_logs_button_tooltip', 'Show server output'],
['sql_editor_output_logs_tab_title', 'Output'],
['sql_editor_output_logs_input_placeholder', 'Enter a part of a message to search for here'],
['sql_editor_output_logs_wrap_mode', 'Wrap mode'],
['sql_editor_sql_execution_button_tooltip', "Esegui l'istruzione SQL (Ctrl + Enter)"],
['sql_editor_sql_execution_new_tab_button_tooltip', "Esegui l'istruzione SQL in una nuova tab (Ctrl + \\)(Shift + Ctrl + Enter)"],
['sql_editor_sql_execution_script_button_tooltip', 'Esegui lo script SQL (Alt + X)'],
['sql_editor_sql_format_button_tooltip', 'Format SQL Script (Shift + Ctrl + F)'],
['sql_editor_sql_execution_button_tooltip', "Esegui l'istruzione SQL"],
['sql_editor_sql_execution_new_tab_button_tooltip', "Esegui l'istruzione SQL in una nuova tab"],
['sql_editor_sql_execution_script_button_tooltip', 'Esegui lo script SQL'],
['sql_editor_sql_format_button_tooltip', 'Format SQL Script'],
['sql_editor_sql_execution_script_lock_tooltip', 'Lock editor to prevent modifications'],
['sql_editor_sql_statistics_unavailable', 'Statistiche non disponibili'],
['sql_editor_sql_execution_executed_queries', 'Executed queries:'],
@@ -14,15 +14,15 @@ export default [
['sql_editor_placeholder', 'Нажмите Ctrl+Enter, чтобы выполнить запрос и увидеть результат'],
['sql_editor_hint_empty', 'Нет авто-дополнений...'],
['sql_editor_tools_more_menu_tooltip', 'Больше инструментов'],
['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса (Shift + Ctrl + E)'],
['sql_editor_output_logs_button_tooltip', 'Показать вывод сервера (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button_tooltip', 'Посмотреть информацию о плане выполнения запроса'],
['sql_editor_output_logs_button_tooltip', 'Показать вывод сервера'],
['sql_editor_output_logs_tab_title', 'Вывод логов'],
['sql_editor_output_logs_input_placeholder', 'Введите часть сообщения для поиска'],
['sql_editor_output_logs_wrap_mode', 'Перенос строк'],
['sql_editor_sql_execution_button_tooltip', 'Выполнить SQL Выражение (Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Выполнить SQL Выражение в новой вкладке (Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', 'Исполнить SQL Скрипт (Alt + X)'],
['sql_editor_sql_format_button_tooltip', 'Форматировать SQL скрипт (Shift + Ctrl + F)'],
['sql_editor_sql_execution_button_tooltip', 'Выполнить SQL Выражение'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Выполнить SQL Выражение в новой вкладке'],
['sql_editor_sql_execution_script_button_tooltip', 'Исполнить SQL Скрипт'],
['sql_editor_sql_format_button_tooltip', 'Форматировать SQL скрипт'],
['sql_editor_sql_execution_script_lock_tooltip', 'Заблокировать скрипт во избежание изменений'],
['sql_editor_sql_statistics_unavailable', 'Статистика не доступна'],
['sql_editor_sql_execution_executed_queries', 'Выполненные запросы:'],
@@ -14,15 +14,15 @@ export default [
['sql_editor_placeholder', 'Thực thi truy vấn với Ctrl+Enter để xem kết quả'],
['sql_editor_hint_empty', 'Không có đề xuất nào...'],
['sql_editor_tools_more_menu_tooltip', 'Nhiều công cụ hơn'],
['sql_editor_execution_plan_button_tooltip', 'Giải thích kế hoạch thực thi (Shift + Ctrl + E)'],
['sql_editor_output_logs_button_tooltip', 'Hiển thị đầu ra máy chủ (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button_tooltip', 'Giải thích kế hoạch thực thi'],
['sql_editor_output_logs_button_tooltip', 'Hiển thị đầu ra máy chủ'],
['sql_editor_output_logs_tab_title', 'Đầu ra'],
['sql_editor_output_logs_input_placeholder', 'Nhập một phần của tin nhắn để tìm kiếm tại đây'],
['sql_editor_output_logs_wrap_mode', 'Chế độ wrap'],
['sql_editor_sql_execution_button_tooltip', 'Thực thi câu lệnh SQL (Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Thực thi câu lệnh SQL trong tab mới (Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', 'Thực thi kịch bản SQL (Alt + X)'],
['sql_editor_sql_format_button_tooltip', 'Định dạng kịch bản SQL (Shift + Ctrl + F)'],
['sql_editor_sql_execution_button_tooltip', 'Thực thi câu lệnh SQL'],
['sql_editor_sql_execution_new_tab_button_tooltip', 'Thực thi câu lệnh SQL trong tab mới'],
['sql_editor_sql_execution_script_button_tooltip', 'Thực thi kịch bản SQL'],
['sql_editor_sql_format_button_tooltip', 'Định dạng kịch bản SQL'],
['sql_editor_sql_execution_script_lock_tooltip', 'Khóa trình soạn thảo để ngăn sửa đổi'],
['sql_editor_sql_statistics_unavailable', 'Thống kê không khả dụng'],
['sql_editor_sql_execution_executed_queries', 'Các truy vấn đã thực thi:'],
@@ -14,15 +14,15 @@ export default [
['sql_editor_placeholder', '使用Ctrl + Enter执行查询以查看结果'],
['sql_editor_hint_empty', '无提示信息...'],
['sql_editor_tools_more_menu_tooltip', '更多工具'],
['sql_editor_execution_plan_button_tooltip', '解释执行计划(Shift + Ctrl + E)'],
['sql_editor_output_logs_button_tooltip', '显示服务端输出 (Shift + Ctrl + O)'],
['sql_editor_execution_plan_button_tooltip', '解释执行计划'],
['sql_editor_output_logs_button_tooltip', '显示服务端输出'],
['sql_editor_output_logs_tab_title', '输出'],
['sql_editor_output_logs_input_placeholder', '在此处输入要搜索的消息关键词'],
['sql_editor_output_logs_wrap_mode', '换行模式'],
['sql_editor_sql_execution_button_tooltip', '执行SQL语句(Ctrl + Enter)'],
['sql_editor_sql_execution_new_tab_button_tooltip', '在新选项卡中执行SQL语句(Ctrl + \\)(Shift + Ctrl + Enter)'],
['sql_editor_sql_execution_script_button_tooltip', '执行SQL脚本(Alt + X)'],
['sql_editor_sql_format_button_tooltip', '格式化SQL脚本(Shift + Ctrl + F)'],
['sql_editor_sql_execution_button_tooltip', '执行SQL语句'],
['sql_editor_sql_execution_new_tab_button_tooltip', '在新选项卡中执行SQL语句'],
['sql_editor_sql_execution_script_button_tooltip', '执行SQL脚本'],
['sql_editor_sql_format_button_tooltip', '格式化SQL脚本'],
['sql_editor_sql_execution_script_lock_tooltip', '锁定编辑器阻止修改'],
['sql_editor_sql_statistics_unavailable', '统计不可用'],
['sql_editor_sql_execution_queries', '查询:'],