mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge branch 'devel' into 5170-cb-6198-migrate-secrets-ui-public-from-backend-to-frontend-part
This commit is contained in:
+2
-1
@@ -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);
|
||||
|
||||
+17
-4
@@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
+241
@@ -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(
|
||||
|
||||
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-25
@@ -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) {
|
||||
|
||||
+1
-4
@@ -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);
|
||||
|
||||
-47
@@ -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,
|
||||
|
||||
+21
-71
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-40
@@ -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;
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ public class WebSQLCompletionContext implements SQLCompletionContext {
|
||||
|
||||
@Override
|
||||
public boolean isSortAlphabetically() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
@@ -67,4 +67,8 @@ public class WebSQLCompletionProposal {
|
||||
return null;
|
||||
}
|
||||
|
||||
public CompletionProposalBase getProposal() {
|
||||
return proposal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -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);
|
||||
|
||||
+1
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+24
-9
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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[][] {
|
||||
|
||||
+71
-58
@@ -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) &&
|
||||
|
||||
+3
-20
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
+14
-15
@@ -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',
|
||||
});
|
||||
|
||||
+2
-19
@@ -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)!;
|
||||
|
||||
|
||||
+2
-4
@@ -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 (
|
||||
|
||||
+3
-3
@@ -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',
|
||||
});
|
||||
|
||||
+2
-1
@@ -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', '查询:'],
|
||||
|
||||
Reference in New Issue
Block a user