Merge remote-tracking branch 'origin/devel' into CB-2507-configuration-for-token-lifetime

This commit is contained in:
Aleksandr Skoblikov
2022-09-12 15:31:25 +03:00
130 changed files with 1617 additions and 623 deletions
+28
View File
@@ -22,6 +22,34 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io
## Changelog
### CloudBeaver 22.2.0 - 2022-09-06
Changes since 22.1.0
- Public page:
- administrators can create shared connections via the Connection menu.
- Resource Manager:
- users can group scripts into folders;
- when internet connection is lost, script editing is disabled to prevent users from losing data;
- users can refresh all scripts by clicking one button;
- users can rename scripts.
- Database Navigator:
- users can group connections into folders.
- SQL Editor:
- "Link with Editor" tool is now available for the SQL Editor and synchronizes it with the element in the Database Navigator;
- script formatting can be undone.
- The Data Editor:
- non-editable tables are marked;
- users can change the coordinate system for spatial data in the Value panel.
- Local configuration:
- the application language can be set;
- the number of table rows to load can be configured to prevent the application from slowing down in the following cases:
- the default number of loaded rows when users open a table and then scroll it,
- the minimum and maximum number of rows, which users can load at once when scrolling the table;
- SQL query execution time limit can be changed;
- The cookies can be disabled.
- The product upgrade scripts were improved to avoid possible UI issues.
- A lot of small bug fixes, enhancements and improvements have been made.
### CloudBeaver 22.1.4 - 2022-08-08
- Local configuration:
- The structure of the local configuration file has been improved to simplify the modification process;
@@ -22,9 +22,10 @@ package io.cloudbeaver;
public class DBWConstants {
public static final String PERMISSION_PUBLIC = "public";
public static final String PERMISSION_ADMIN = "admin";
public static final String PERMISSION_CONFIGURATION_MANAGER = "configuration-manager";
public static final String PERMISSION_EDIT_STRUCTURE = "edit-meta";
public static final String PERMISSION_EDIT_DATA = "edit-data";
@@ -17,8 +17,10 @@
package io.cloudbeaver.model.app;
import io.cloudbeaver.DataSourceFilter;
import io.cloudbeaver.VirtualProjectImpl;
import io.cloudbeaver.model.log.SLF4JLogHandler;
import io.cloudbeaver.model.rm.local.LocalResourceController;
import io.cloudbeaver.server.WebWorkspace;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.Platform;
import org.eclipse.equinox.app.IApplicationContext;
@@ -35,8 +37,6 @@ import org.jkiss.dbeaver.model.rm.RMController;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.registry.BaseApplicationImpl;
import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl;
import io.cloudbeaver.VirtualProjectImpl;
import org.jkiss.dbeaver.runtime.IVariableResolver;
import org.jkiss.dbeaver.utils.GeneralUtils;
@@ -60,7 +60,7 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements
@NotNull
@Override
public DBPWorkspace createWorkspace(@NotNull DBPPlatform platform, @NotNull IWorkspace eclipseWorkspace) {
return new EclipseWorkspaceImpl(platform, eclipseWorkspace);
return new WebWorkspace(platform, eclipseWorkspace);
}
@Nullable
@@ -17,7 +17,7 @@
package io.cloudbeaver.model.app;
import io.cloudbeaver.DataSourceFilter;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.VirtualProjectImpl;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.app.DBPApplication;
@@ -27,7 +27,6 @@ import org.jkiss.dbeaver.model.rm.RMController;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.security.SMAdminController;
import org.jkiss.dbeaver.model.security.SMController;
import io.cloudbeaver.VirtualProjectImpl;
import java.nio.file.Path;
@@ -41,6 +40,8 @@ public interface WebApplication extends DBPApplication {
Path getDataDirectory(boolean create);
Path getWorkspaceDirectory();
Path getHomeDirectory();
boolean isMultiNode();
@@ -204,7 +204,7 @@ public class LocalResourceController implements RMController {
public RMProject createProject(@NotNull String name, @Nullable String description) throws DBException {
if (!Files.exists(sharedProjectsPath)) {
try {
Files.createDirectory(sharedProjectsPath);
Files.createDirectories(sharedProjectsPath);
} catch (IOException e) {
throw new DBException("Error creating shared project path", e);
}
@@ -216,7 +216,7 @@ public class LocalResourceController implements RMController {
throw new DBException("Project '" + name + "' already exists");
}
try {
Files.createDirectory(getProjectPath(project.getId()));
Files.createDirectories(getProjectPath(project.getId()));
return project;
} catch (IOException e) {
throw new DBException("Error creating project path", e);
@@ -354,7 +354,7 @@ public class LocalResourceController implements RMController {
}
try {
if (isFolder) {
Files.createDirectory(targetPath);
Files.createDirectories(targetPath);
} else {
Files.createFile(targetPath);
}
@@ -413,11 +413,6 @@ public class LocalResourceController implements RMController {
);
}
@Override
public RMResource[] getResourcePath(@NotNull String projectId, @NotNull String resourcePath) throws DBException {
return makeResourcePath(projectId, getTargetPath(projectId, resourcePath), false).toArray(RMResource[]::new);
}
@NotNull
@Override
public byte[] getResourceContents(@NotNull String projectId, @NotNull String resourcePath) throws DBException {
@@ -1,65 +0,0 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.model.session;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.auth.SMAuthSpace;
import org.jkiss.dbeaver.model.auth.SMAuthToken;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
public class SMWebSessionContext implements SMSessionContext {
private final WebSession session;
private final SMSessionContext parentContext;
public SMWebSessionContext(SMSessionContext parentContext, WebSession session) {
this.parentContext = parentContext;
this.session = session;
}
@Nullable
@Override
public SMSession getSpaceSession(@NotNull DBRProgressMonitor monitor, @NotNull SMAuthSpace space, boolean open) throws DBException {
return parentContext.getSpaceSession(monitor, session.getSessionSpace(), open);
}
@Nullable
@Override
public SMSession findSpaceSession(@NotNull SMAuthSpace space) {
return parentContext.findSpaceSession(session.getSessionSpace());
}
@Override
public SMAuthToken[] getSavedTokens() {
return parentContext.getSavedTokens();
}
@Override
public void addSession(@NotNull SMSession session) {
parentContext.addSession(session);
}
@Override
public boolean removeSession(@NotNull SMSession session) {
return parentContext.removeSession(session);
}
}
@@ -352,10 +352,9 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
public VirtualProjectImpl createVirtualProject(RMProject project) {
// Do not filter data sources from user project
DataSourceFilter filter = project.getType() == RMProject.Type.USER ? x -> true : this::isDataSourceAccessible;
SMSessionContext projectSessionContext = new SMWebSessionContext(sessionAuthContext, this);
VirtualProjectImpl sessionProject = application.createProjectImpl(
project,
projectSessionContext,
sessionAuthContext,
this,
filter);
DBPDataSourceRegistry dataSourceRegistry = sessionProject.getDataSourceRegistry();
@@ -958,8 +957,10 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
}
public synchronized void updateSMAuthInfo(SMAuthInfo smAuthInfo) throws DBException {
userContext.refresh(smAuthInfo);
refreshUserData();
boolean contextChanged = userContext.refresh(smAuthInfo);
if (contextChanged) {
refreshUserData();
}
}
@Override
@@ -65,7 +65,7 @@ public class WebUserContext implements SMCredentialsProvider {
* @param smAuthInfo - auth info from security manager
* @throws DBException - if user already authorized and new token come from another user
*/
public synchronized void refresh(SMAuthInfo smAuthInfo) throws DBException {
public synchronized boolean refresh(SMAuthInfo smAuthInfo) throws DBException {
if (smAuthInfo.getAuthStatus() != SMAuthStatus.SUCCESS) {
throw new DBCException("Authorization did not complete successfully");
}
@@ -92,7 +92,7 @@ public class WebUserContext implements SMCredentialsProvider {
this.smSessionId = smAuthInfo.getAuthPermissions().getSessionId();
setUser(authPermissions.getUserId() == null ? null : new WebUser(securityController.getUserById(authPermissions.getUserId())));
}
return isSessionChanged;
}
public synchronized void refreshSMSession() throws DBException {
@@ -72,16 +72,6 @@ public class WebPlatformActivator extends Plugin {
super.stop(context);
}
/**
* Returns configuration file
*/
public static synchronized File getConfigurationFile(String fileName) {
if (configDir == null) {
configDir = getInstance().getStateLocation().toFile();
}
return new File(configDir, fileName);
}
public DBPPreferenceStore getPreferences() {
return preferences;
}
@@ -17,7 +17,6 @@
package io.cloudbeaver.server;
import org.eclipse.core.resources.IWorkspace;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.app.DBPPlatform;
import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl;
@@ -28,17 +27,8 @@ import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl;
*/
public class WebWorkspace extends EclipseWorkspaceImpl {
private final String workspaceId;
public WebWorkspace(DBPPlatform platform, IWorkspace eclipseWorkspace) {
super(platform, eclipseWorkspace);
workspaceId = readWorkspaceId();
}
@NotNull
@Override
public String getWorkspaceId() {
return workspaceId;
}
}
@@ -100,11 +100,6 @@ public class WebServiceUtils extends WebCommonUtils {
return session.getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry();
}
@NotNull
public static DBNModel getGlobalNavigatorModel() throws DBWebException {
return DBWorkbench.getPlatform().getNavigatorModel();
}
public static InputStream openStaticResource(String path) {
return WebServiceUtils.class.getClassLoader().getResourceAsStream(path);
}
@@ -456,6 +456,11 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
return dataDir.toPath();
}
@Override
public Path getWorkspaceDirectory() {
return Path.of(workspaceLocation);
}
private void initializeSecurityController() throws DBException {
securityController = createGlobalSecurityController();
}
@@ -21,14 +21,13 @@ import io.cloudbeaver.registry.WebDriverRegistry;
import io.cloudbeaver.service.session.WebSessionManager;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
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.DBPExternalFileManager;
import org.jkiss.dbeaver.model.app.DBACertificateStorage;
import org.jkiss.dbeaver.model.app.DBASecureStorage;
import org.jkiss.dbeaver.model.app.DBPResourceHandler;
import org.jkiss.dbeaver.model.app.DBPWorkspace;
import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor;
import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderRegistry;
@@ -36,15 +35,15 @@ import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.connection.DBPDriverLibrary;
import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage;
import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore;
import org.jkiss.dbeaver.model.qm.QMController;
import org.jkiss.dbeaver.model.qm.QMRegistry;
import org.jkiss.dbeaver.model.qm.QMUtils;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.registry.BasePlatformImpl;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.runtime.SecurityProviderUtils;
import org.jkiss.dbeaver.runtime.qm.QMControllerImpl;
import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter;
import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl;
import org.jkiss.dbeaver.utils.ContentUtils;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.osgi.framework.Bundle;
@@ -76,7 +75,7 @@ public class CBPlatform extends BasePlatformImpl {
private Path tempFolder;
private QMControllerImpl queryManager;
private QMRegistryImpl queryManager;
private QMLogFileWriter qmLogWriter;
private DBACertificateStorage certificateStorage;
private WebWorkspace workspace;
@@ -141,7 +140,7 @@ public class CBPlatform extends BasePlatformImpl {
this.workspace.initializeProjects();
QMUtils.initApplication(this);
this.queryManager = new QMControllerImpl();
this.queryManager = new QMRegistryImpl();
this.qmLogWriter = new QMLogFileWriter();
this.queryManager.registerMetaListener(qmLogWriter);
@@ -206,12 +205,6 @@ public class CBPlatform extends BasePlatformImpl {
return workspace;
}
@NotNull
@Override
public DBPResourceHandler getDefaultResourceHandler() {
return CBResourceHandler.INSTANCE;
}
@NotNull
@Override
public CBApplication getApplication() {
@@ -229,7 +222,7 @@ public class CBPlatform extends BasePlatformImpl {
}
@NotNull
public QMController getQueryManager() {
public QMRegistry getQueryManager() {
return queryManager;
}
@@ -252,13 +245,7 @@ public class CBPlatform extends BasePlatformImpl {
}
@NotNull
@Override
public DBPExternalFileManager getExternalFileManager() {
return workspace;
}
@NotNull
public File getTempFolder(DBRProgressMonitor monitor, String name) {
public Path getTempFolder(DBRProgressMonitor monitor, String name) {
if (tempFolder == null) {
// Make temp folder
monitor.subTask("Create temp folder");
@@ -279,13 +266,12 @@ public class CBPlatform extends BasePlatformImpl {
log.error("Error creating temp folder '" + folder.toAbsolutePath() + "'", e);
}
}
return folder.toFile();
return folder;
}
@NotNull
@Override
public File getConfigurationFile(String fileName) {
return WebPlatformActivator.getConfigurationFile(fileName);
protected Plugin getProductPlugin() {
return WebPlatformActivator.getInstance();
}
@Override
@@ -1,85 +0,0 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.server;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.app.DBPResourceHandler;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.navigator.DBNNodeWithResource;
import org.jkiss.dbeaver.model.navigator.DBNResource;
import java.util.List;
/**
* Default resource handler
*/
public class CBResourceHandler implements DBPResourceHandler {
public static final CBResourceHandler INSTANCE = new CBResourceHandler();
@Override
public int getFeatures(IResource resource) {
if (resource instanceof IFile) {
return FEATURE_OPEN | FEATURE_DELETE | FEATURE_RENAME;
} else {
return FEATURE_DELETE | FEATURE_RENAME | FEATURE_CREATE_FOLDER | FEATURE_MOVE_INTO;
}
}
@NotNull
@Override
public String getTypeName(@NotNull IResource resource) {
return "resource"; //$NON-NLS-1$
}
@Override
public String getResourceDescription(@NotNull IResource resource) {
return "";
}
@Override
public List<DBPDataSourceContainer> getAssociatedDataSources(DBNResource resource) {
return null;
}
@NotNull
@Override
public String getResourceNodeName(@NotNull IResource resource) {
return resource.getName();
}
@NotNull
@Override
public DBNResource makeNavigatorNode(@NotNull DBNNode parentNode, @NotNull IResource resource) throws CoreException, DBException {
DBNResource node = new DBNResource(parentNode, resource, this);
return node;
}
@Override
public void updateNavigatorNodeFromResource(@NotNull DBNNodeWithResource node, @NotNull IResource resource) {
}
@Override
public void openResource(@NotNull IResource resource) throws CoreException, DBException {
}
}
@@ -28,9 +28,9 @@ import org.jkiss.dbeaver.model.struct.DBSDataContainer;
import org.jkiss.dbeaver.utils.ContentUtils;
import org.jkiss.utils.CommonUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.List;
@@ -38,7 +38,7 @@ import java.util.List;
public class WebSQLDataLOBReceiver implements DBDDataReceiver {
private static final Log log = Log.getLog(WebSQLDataLOBReceiver.class);
public static final File DATA_EXPORT_FOLDER = CBPlatform.getInstance().getTempFolder(new VoidProgressMonitor(), "sql-lob-files");
public static final Path DATA_EXPORT_FOLDER = CBPlatform.getInstance().getTempFolder(new VoidProgressMonitor(), "sql-lob-files");
private final String tableName;
private final DBSDataContainer dataContainer;
@@ -51,8 +51,12 @@ public class WebSQLDataLOBReceiver implements DBDDataReceiver {
this.tableName = tableName;
this.dataContainer = dataContainer;
this.rowIndex = rowIndex;
if (!DATA_EXPORT_FOLDER.exists()){
DATA_EXPORT_FOLDER.mkdirs();
if (!Files.exists(DATA_EXPORT_FOLDER)){
try {
Files.createDirectories(DATA_EXPORT_FOLDER);
} catch (IOException e) {
log.error("Error creating temp folder", e);
}
}
}
@@ -81,8 +85,8 @@ public class WebSQLDataLOBReceiver implements DBDDataReceiver {
throw new DBQuotaException(
"Data export quota exceeded", CBConstants.QUOTA_PROP_FILE_LIMIT, fileSizeLimit.longValue(), binaryValue.length);
}
File file = new File(DATA_EXPORT_FOLDER, exportFileName);
Files.write(file.toPath(), binaryValue);
Path file = DATA_EXPORT_FOLDER.resolve(exportFileName);
Files.write(file, binaryValue);
return exportFileName;
}
@@ -17,11 +17,10 @@ import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.regex.Pattern;
@@ -29,7 +28,7 @@ import java.util.regex.Pattern;
public class WebSQLResultServlet extends WebServiceServletBase {
private static final Log log = Log.getLog(WebSQLResultServlet.class);
private static final MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.getAbsolutePath());
private static final MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.toAbsolutePath().toString());
// context-id/result-id/row-number/attribute-name
private static final Pattern URL_PATTERN = Pattern.compile("/?([\\w]+)/([0-9]+)/([0-9]+)/([0-9]+)/(.+)[/\\?]?");
@@ -68,18 +67,18 @@ public class WebSQLResultServlet extends WebServiceServletBase {
valuePath = valuePath.substring(1);
}
File dataFile = new File(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER, valuePath);
Path dataFile = WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.resolve(valuePath);
session.addInfoMessage("Download LOB file ...");
response.setHeader("Content-Type", "application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + dataFile.getName() + "\"");
response.setHeader("Content-Length", String.valueOf(dataFile.length()));
response.setHeader("Content-Disposition", "attachment; filename=\"" + dataFile.getFileName().toString() + "\"");
response.setHeader("Content-Length", String.valueOf(Files.size(dataFile)));
response.setDateHeader("Expires", System.currentTimeMillis() + CBStaticServlet.STATIC_CACHE_SECONDS * 1000);
response.setHeader("Cache-Control", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS);
try (InputStream is = new FileInputStream(dataFile)) {
try (InputStream is = Files.newInputStream(dataFile)) {
IOUtils.copyStream(is, response.getOutputStream());
}
Files.deleteIfExists(dataFile.toPath());
Files.deleteIfExists(dataFile);
}
}
}
@@ -39,9 +39,9 @@ import org.jkiss.utils.Base64;
import org.jkiss.utils.CommonUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
/**
@@ -211,7 +211,7 @@ public class WebSQLUtils {
case WebSQLConstants.VALUE_TYPE_CONTENT: {
if (map.get(WebSQLConstants.ATTR_BINARY) != null) {
DBDContentStorage storage;
File openFile = new File(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER, (String) map.get("fileName"));
Path openFile = WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.resolve((String) map.get("fileName"));
Object tempValue = ((DBDAttributeBinding) attribute).getValueHandler().getValueFromObject(
session,
attribute,
@@ -404,8 +404,9 @@ public class WebServiceAdmin implements DBWServiceAdmin {
((DataSourceDescriptor) newDataSource).setNavigatorSettings(CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
globalDataSourceRegistry.addDataSource(newDataSource);
return new WebConnectionInfo(webSession, newDataSource);
WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource);
webSession.addConnection(connectionInfo);
return connectionInfo;
} catch (DBException e) {
throw new DBWebException("Error copying connection", e);
}
@@ -14,10 +14,10 @@ import org.jkiss.utils.IOUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class WebDataTransferServlet extends WebServiceServletBase {
@@ -56,13 +56,13 @@ public class WebDataTransferServlet extends WebServiceServletBase {
fileName = taskInfo.getDataFileId();
}
File dataFile = taskInfo.getDataFile();
Path dataFile = taskInfo.getDataFile();
session.addInfoMessage("Download data ...");
response.setHeader("Content-Type", processor.getContentType());
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setHeader("Content-Length", String.valueOf(dataFile.length()));
response.setHeader("Content-Length", String.valueOf(Files.size(dataFile)));
try (InputStream is = new FileInputStream(dataFile)) {
try (InputStream is = Files.newInputStream(dataFile)) {
IOUtils.copyStream(is, response.getOutputStream());
}
@@ -2,27 +2,29 @@ package io.cloudbeaver.service.data.transfer.impl;
import org.jkiss.dbeaver.Log;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class WebDataTransferTaskConfig {
private static final Log log = Log.getLog(WebDataTransferTaskConfig.class);
private File dataFile;
private Path dataFile;
private WebDataTransferParameters parameters;
private String exportFileName;
public WebDataTransferTaskConfig(File dataFile, WebDataTransferParameters parameters) {
public WebDataTransferTaskConfig(Path dataFile, WebDataTransferParameters parameters) {
this.dataFile = dataFile;
this.parameters = parameters;
}
public File getDataFile() {
public Path getDataFile() {
return dataFile;
}
public String getDataFileId() {
return dataFile.getName();
return dataFile.getFileName().toString();
}
public WebDataTransferParameters getParameters() {
@@ -38,8 +40,10 @@ public class WebDataTransferTaskConfig {
}
public void deleteFile() {
if (!dataFile.delete()) {
log.error("Error deleting export file " + dataFile.getAbsolutePath());
try {
Files.delete(dataFile);
} catch (IOException e) {
log.error("Error deleting export file " + dataFile.toAbsolutePath(), e);
}
}
}
@@ -51,9 +51,10 @@ import org.jkiss.dbeaver.tools.transfer.stream.StreamTransferConsumer;
import org.jkiss.dbeaver.utils.ContentUtils;
import org.jkiss.utils.CommonUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
@@ -66,21 +67,19 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
private static final Log log = Log.getLog(WebServiceDataTransfer.class);
private final File dataExportFolder;
private final Path dataExportFolder;
public WebServiceDataTransfer() {
dataExportFolder = CBPlatform.getInstance().getTempFolder(new VoidProgressMonitor(), "data-transfer");
ContentUtils.deleteFileRecursive(dataExportFolder);
if (!dataExportFolder.mkdirs()) {
log.error("Error re-creating temporary folder");
try {
Files.createDirectories(dataExportFolder);
} catch (IOException e) {
log.error("Error re-creating temporary folder", e);
}
}
public File getDataExportFolder() {
return dataExportFolder;
}
@Override
public List<WebDataTransferStreamProcessor> getAvailableStreamProcessors(WebSession session) {
List<DataTransferProcessorDescriptor> processors = DataTransferRegistry.getInstance().getAvailableProcessors(StreamTransferConsumer.class, DBSEntity.class);
@@ -130,10 +129,12 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
if (taskInfo == null) {
throw new DBWebException("Session task '" + dataFileId + "' not found");
}
File dataFile = taskInfo.getDataFile();
Path dataFile = taskInfo.getDataFile();
if (dataFile != null) {
if (!dataFile.delete()) {
log.warn("Error deleting data file '" + dataFile.getAbsolutePath() + "'");
try {
Files.delete(dataFile);
} catch (IOException e) {
log.warn("Error deleting data file '" + dataFile.toAbsolutePath() + "'", e);
}
}
dtConfig.removeTask(taskInfo);
@@ -151,13 +152,15 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
monitor.beginTask("Export data", 1);
try {
monitor.subTask("Export data using " + processor.getName());
File exportFile = new File(dataExportFolder, makeUniqueFileName(sqlProcessor, processor));
Path exportFile = dataExportFolder.resolve(makeUniqueFileName(sqlProcessor, processor));
try {
exportData(monitor, processor, dataContainer, parameters, resultsInfo, exportFile);
} catch (Exception e) {
if (exportFile.exists()) {
if (!exportFile.delete()) {
log.error("Error deleting export file " + exportFile.getAbsolutePath());
if (Files.exists(exportFile)) {
try {
Files.delete(exportFile);
} catch (IOException ex) {
log.error("Error deleting export file " + exportFile.toAbsolutePath(), e);
}
}
if (e instanceof DBException) {
@@ -170,7 +173,7 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
taskConfig.setExportFileName(exportFileName);
WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession()).addTask(taskConfig);
result = exportFile.getName();
result = exportFile.getFileName().toString();
} catch (Throwable e) {
throw new InvocationTargetException(e);
} finally {
@@ -187,7 +190,7 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
DBSDataContainer dataContainer,
WebDataTransferParameters parameters,
WebSQLResultsInfo resultsInfo,
File exportFile) throws DBException, IOException
Path exportFile) throws DBException, IOException
{
IDataTransferProcessor processorInstance = processor.getInstance();
if (!(processorInstance instanceof IStreamDataExporter)) {
@@ -211,8 +214,8 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
StreamConsumerSettings settings = new StreamConsumerSettings();
settings.setOutputEncodingBOM(false);
settings.setOutputFolder(exportFile.getParentFile().getAbsolutePath());
settings.setOutputFilePattern(exportFile.getName());
settings.setOutputFolder(exportFile.getParent().toAbsolutePath().toString());
settings.setOutputFilePattern(exportFile.getFileName().toString());
Map<String, Object> properties = new HashMap<>();
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { computed, makeObservable, runInAction } from 'mobx';
import { computed, makeObservable, observable, runInAction } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { SyncExecutor, ISyncExecutor, ITask, AutoRunningTask, whileTask } from '@cloudbeaver/core-executor';
@@ -253,7 +253,7 @@ UserInfoIncludes
protected resetIncludes(): void {
const metadata = this.getMetadata();
metadata.includes = [...this.defaultIncludes];
metadata.includes = observable([...this.defaultIncludes]);
}
getIncludes(key: void): string[] {
@@ -57,7 +57,6 @@ const buttonStyles = css`
text-decoration: none;
}
}
`;
const buttonMod = {
@@ -62,6 +62,10 @@ export const BASE_CONTAINERS_STYLES = css`
&[parent] {
padding: 24px;
&[compact] {
padding: 16px;
}
&[dense] {
padding: 8px;
}
@@ -70,6 +74,10 @@ export const BASE_CONTAINERS_STYLES = css`
&[gap] {
gap: 24px;
&[compact] {
gap: 16px;
}
&[dense] {
gap: 8px;
}
@@ -20,4 +20,5 @@ export interface IContainerProps extends ILayoutSizeProps {
gap?: boolean;
grid?: boolean;
dense?: boolean;
compact?: boolean;
}
@@ -22,6 +22,7 @@ export function filterContainerFakeProps<T extends IContainerProps>(props: T): O
gap,
grid,
dense,
compact,
...rest
} = filterLayoutFakeProps(props);
@@ -116,8 +116,9 @@ export const Loader = observer<Props>(function Loader({
if (loading) {
if (element.cancel) {
const cancelCopy = onCancel;
onCancel = () => {
onCancel?.();
cancelCopy?.();
element.cancel?.();
};
}
+37 -22
View File
@@ -13,6 +13,7 @@ import styled from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { ErrorBoundary } from '../ErrorBoundary';
import { useObjectRef } from '../useObjectRef';
import { MenuPanel } from './MenuPanel';
import { menuPanelStyles } from './menuPanelStyles';
@@ -25,6 +26,7 @@ interface IMenuProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
style?: ComponentStyle;
disclosure?: boolean;
placement?: MenuInitialState['placement'];
submenu?: boolean;
modal?: boolean;
visible?: boolean;
rtl?: boolean;
@@ -48,6 +50,7 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
getHasBindings,
onVisibleSwitch,
modal,
submenu,
rtl,
...props
}, ref) {
@@ -64,18 +67,49 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
propsRef.onVisibleSwitch?.(menu.visible);
}, [menu.visible]);
let menuVisible = menu.visible;
if (panelAvailable === false) {
menuVisible = false;
}
if (React.isValidElement(children) && disclosure) {
return styled(styles)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props} {...children.props}>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
</ErrorBoundary>
);
}
return styled(styles)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} {...props} {...children.props}>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props}>
<box>{children}</box>
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
@@ -83,25 +117,6 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
{items}
</MenuPanel>
</MenuStateContext.Provider>
);
}
return styled(styles)(
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} {...props}>
<box>{children}</box>
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
</ErrorBoundary>
);
}));
@@ -22,6 +22,7 @@ interface IMenuItemElementProps {
icon?: string | React.ReactNode;
menu?: boolean;
loading?: boolean;
panelAvailable?: boolean;
style?: ComponentStyle;
}
@@ -31,6 +32,7 @@ export const MenuItemElement = observer<IMenuItemElementProps>(function MenuItem
binding,
icon,
menu,
panelAvailable,
loading = false,
style = [],
}) {
@@ -52,7 +54,7 @@ export const MenuItemElement = observer<IMenuItemElementProps>(function MenuItem
</menu-item-binding>
<menu-item-content>
{loading && <Loader small fullSize />}
{menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
{panelAvailable !== false && menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
</menu-item-content>
</menu-panel-item>
@@ -13,6 +13,7 @@ import styled, { use } from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { ErrorBoundary } from '../ErrorBoundary';
import { getComputed } from '../getComputed';
import { MenuEmptyItem } from './MenuEmptyItem';
import { menuPanelStyles } from './menuPanelStyles';
@@ -26,6 +27,7 @@ export interface IMenuPanelProps {
getHasBindings?: () => boolean;
children: React.ReactNode | (() => React.ReactNode);
rtl?: boolean;
submenu?: boolean;
style?: ComponentStyle;
className?: string;
}
@@ -59,13 +61,15 @@ export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
}
return styled(styles)(
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(renderedChildren) === 0 && (
<MenuEmptyItem style={style} />
)}
{renderedChildren}
</menu-box>
</Menu>
<ErrorBoundary>
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(renderedChildren) === 0 && (
<MenuEmptyItem style={style} />
)}
{renderedChildren}
</menu-box>
</Menu>
</ErrorBoundary>
);
});
@@ -39,6 +39,9 @@ export const menuPanelStyles = css`
outline: none;
z-index: 999;
}
MenuPanel[submenu] {
margin-top: -12px;
}
menu-box {
composes: theme-typography--body2 theme-elevation-z5 from global;
min-width: 140px;
@@ -58,6 +61,12 @@ export const menuPanelStyles = css`
}
}
Menu[modal] menu-box {
overflow: auto;
max-height: 80vh;
overflow-x: hidden;
}
MenuItem, MenuItemCheckbox, MenuItemRadio {
display: flex;
align-items: center;
@@ -109,6 +118,7 @@ export const menuPanelStyles = css`
align-items: center;
height: 30px;
padding: 0 4px;
& menu-item-text, & menu-item-binding {
display: block;
padding: 0 4px;
@@ -145,4 +155,13 @@ export const menuPanelStyles = css`
width: 16px;
}
}
Menu[rtl] {
& menu-item-text {
text-align: right;
}
& menu-panel-item menu-item-content Icon {
transform: rotate(90deg);
}
}
`;
@@ -24,7 +24,7 @@ export const buttonStyles = css`
}
button {
composes: theme-button theme-button_unelevated theme-button_background from global;
composes: theme-button theme-button_unelevated theme-button_background theme-elevation-z3 from global;
opacity: 0;
transition-property: opacity, box-shadow !important;
}
@@ -36,6 +36,11 @@ export const splitStyles = css`
cursor: col-resize;
user-select: none;
z-index: 1;
transition: box-shadow 0.3s ease-in-out;
&:hover {
box-shadow: 0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%);
}
}
ResizerControls:before {
@@ -7,6 +7,7 @@
*/
interface Props {
big?: boolean;
className?: string;
}
@@ -128,6 +128,16 @@ export const TREE_NODE_STYLES = css`
TreeNodeNestedMessage {
composes: theme-typography--caption from global;
padding: 4px 12px;
[big] {
padding: 4px 24px;
}
}
tree-box[|big] {
& TreeNodeNestedMessage {
padding: 4px 24px;
}
}
node TreeNodeControl[big] {
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
import styled, { css, use } from 'reshadow';
import { uuid } from '@cloudbeaver/core-utils';
@@ -19,6 +19,10 @@ interface Props extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLIn
const styles = css`
label {
cursor: pointer;
&[|disabled] {
cursor: default;
}
}
`;
@@ -39,7 +43,7 @@ export const UploadArea: React.FC<Props> = function UploadArea({ id, reset, clas
return styled(styles)(
<>
<input {...rest} type='file' id={_id} hidden onChange={handleChange} />
<label htmlFor={_id} className={className} title={rest.title}>
<label htmlFor={_id} {...use({ disabled: rest.disabled })} className={className} title={rest.title}>
{children}
</label>
</>
@@ -14,6 +14,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { getComputed } from './getComputed';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObservableRef } from './useObservableRef';
@@ -276,6 +277,7 @@ export function useMapResource<
preloaded: computed,
exception: observable.ref,
loading: observable.ref,
resource: observable.ref,
}, {
exceptionObserved: false,
resource,
@@ -363,20 +365,22 @@ export function useMapResource<
loading: computed,
}, false);
// TODO: getComputed skips update somehow ...
const outdated = (
!result.loading
&& (result.outdated || !result.loaded)
);
const preloaded = refObj.preloaded; // make mobx subscription
const canLoad = getComputed(() => (
(
!keyRef.actual
|| result.outdated
|| !result.loaded
)
&& refObj.preloaded
&& keyRef.key !== null
&& (
result.exception === null
|| (Array.isArray(result.exception) && !result.exception.some(Boolean))
)
));
useEffect(() => {
if (!preloaded || (!outdated && keyRef.actual) || keyRef.key === null) {
return;
}
if (result.exception === null || (Array.isArray(result.exception) && !result.exception.some(Boolean))) {
if (canLoad) {
(refObj as any)[loadFunctionName]();
}
});
@@ -74,6 +74,10 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), {
minimizer: [new TerserPlugin({
extractComments: /Copyright \(C\)/i,
terserOptions: {
keep_classnames: true,
keep_fnames: true,
},
})],
},
plugins: [
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { computed, makeObservable } from 'mobx';
import { computed, makeObservable, runInAction } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { EPermission, SessionPermissionsResource, ServerConfigResource } from '@cloudbeaver/core-root';
@@ -37,7 +37,7 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
super();
permissionsResource.require(this, EPermission.public);
this.serverConfigResource.onDataOutdated.addHandler(this.markOutdated.bind(this));
this.serverConfigResource.onDataOutdated.addHandler(() => this.markOutdated());
makeObservable(this, {
enabledDrivers: computed,
@@ -69,19 +69,21 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
includeDriverParameters: false,
includeDriverProperties: false,
includeProviderProperties: false,
...this.getIncludesMap(driverId, includes),
...this.getIncludesMap(driverId, (all ? this.defaultIncludes : includes)),
});
if (all) {
this.resetIncludes();
this.data.clear();
}
if (driverId && !drivers.some(driver => driver.id === driverId)) {
throw new Error('Driver is not found');
}
this.updateDriver(...drivers);
runInAction(() => {
if (all) {
const removedDrivers = this.keys.filter(key => !drivers.some(driver => driver.id === key));
this.delete(resourceKeyList(removedDrivers));
}
this.updateDriver(...drivers);
});
});
return this.data;
@@ -91,6 +93,6 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
const keys = resourceKeyList(drivers.map(driver => driver.id));
const oldDriver = this.get(keys);
this.set(keys, oldDriver.map((oldDriver, i) => ({ ...oldDriver, ...drivers[i] })));
this.set(keys, oldDriver.map((oldDriver, i) => (Object.assign(oldDriver ?? {}, drivers[i]))));
}
}
+26 -24
View File
@@ -6,10 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { flat } from '@cloudbeaver/core-utils';
import { Bootstrap } from './Bootstrap';
import { Dependency } from './Dependency';
import type { DIContainer } from './DIContainer';
import type { IServiceCollection, IServiceInjector } from './IApp';
import type { IServiceCollection, IServiceConstructor, IServiceInjector } from './IApp';
import { IDiWrapper, inversifyWrapper } from './inversifyWrapper';
import type { PluginManifest } from './PluginManifest';
@@ -24,6 +26,14 @@ export class App {
this.getServiceCollection().addServiceByClass(App, this);
}
getPlugins(): PluginManifest[] {
return [...this.plugins];
}
getServices(): IServiceConstructor<any>[] {
return flat(this.plugins.map(plugin => plugin.providers));
}
registerChildContainer(container: DIContainer): void {
this.diWrapper.registerChildContainer(container);
}
@@ -42,41 +52,33 @@ export class App {
// first phase register all dependencies
registerServices(): void {
for (const plugin of this.plugins) {
if (plugin.providers.length) {
plugin.providers.forEach(provider => {
// console.log('provider', provider.name);
this.diWrapper.collection.addServiceByClass(provider);
});
}
for (const service of this.getServices()) {
// console.log('provider', provider.name);
this.diWrapper.collection.addServiceByClass(service);
}
}
async initializeServices(): Promise<void> {
for (const plugin of this.plugins) {
for (const service of plugin.providers) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
for (const service of this.getServices()) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('register' in serviceInstance) {
await serviceInstance.register();
}
} else if (service.prototype instanceof Dependency) {
this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('register' in serviceInstance) {
await serviceInstance.register();
}
} else if (service.prototype instanceof Dependency) {
this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
}
}
}
async loadServices(): Promise<void> {
for (const plugin of this.plugins) {
for (const service of plugin.providers) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
for (const service of this.getServices()) {
if (service.prototype instanceof Bootstrap) {
const serviceInstance = this.diWrapper.injector.getServiceByClass<Bootstrap>(service);
if ('load' in serviceInstance) {
await serviceInstance.load();
}
if ('load' in serviceInstance) {
await serviceInstance.load();
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
import type { ISyncContextLoader } from '@cloudbeaver/core-executor';
import type { App } from './App';
import { App } from './App';
import type { IServiceConstructor, IServiceInjector } from './IApp';
import { injectable } from './injectable';
@@ -54,6 +54,7 @@ export interface INotification<TProps extends INotificationExtraProps<any> = INo
message?: string;
timestamp: number;
details?: string | Error | null;
autoClose?: boolean;
persistent?: boolean;
state: { deleteDelay: number };
isSilent: boolean;
@@ -69,6 +70,7 @@ export interface INotificationOptions<TProps extends INotificationExtraProps<any
message?: string;
details?: string | Error | null;
isSilent?: boolean;
autoClose?: boolean;
persistent?: boolean;
extraProps?: TProps;
timestamp?: number;
@@ -80,6 +80,7 @@ export class NotificationService {
isSilent: !!options.isSilent,
customComponent: options.customComponent,
extraProps: options.extraProps || {} as TProps,
autoClose: options.autoClose,
persistent: options.persistent,
state: observable({ deleteDelay: 0 }),
timestamp: options.timestamp || Date.now(),
@@ -22,7 +22,7 @@ export class NotificationItemController implements IInitializableController {
isDetailsDialogOpen = false;
private notification!: INotification<any>;
constructor(private commonDialogService: CommonDialogService) {
constructor(private readonly commonDialogService: CommonDialogService) {
makeObservable(this, {
isDetailsDialogOpen: observable,
});
@@ -35,7 +35,11 @@ export class NotificationItemController implements IInitializableController {
this.handleShowDetails = this.showDetails.bind(this);
}
if (notification.type !== ENotificationType.Error && !notification.persistent) {
if (
notification.type !== ENotificationType.Error
&& !notification.persistent
&& notification.autoClose !== false
) {
this.closeAfter = FadeTimeout;
}
}
@@ -366,25 +366,21 @@ export abstract class CachedMapResource<
}
includes(param: ResourceKey<TKey>, key: ResourceKey<TKey>): boolean {
if (param === key) {
if
(
this.isAliasEqual(param, key)
|| (ResourceKeyUtils.isEmpty(param) && ResourceKeyUtils.isEmpty(key))
) {
return true;
}
if (this.isAlias(param) || this.isAlias(key)) {
return this.isAliasEqual(param, key);
}
if (ResourceKeyUtils.isEmpty(param) || ResourceKeyUtils.isEmpty(key)) {
return ResourceKeyUtils.isEmpty(param) && ResourceKeyUtils.isEmpty(key);
return true;
}
param = ResourceKeyUtils.mapKey(param, this.getKeyRef.bind(this));
key = ResourceKeyUtils.mapKey(key, this.getKeyRef.bind(this));
if (param === key) {
return true;
}
return ResourceKeyUtils.includes(param, key, this.isKeyEqual);
}
@@ -454,6 +450,7 @@ export abstract class CachedMapResource<
if (key === undefined) {
key = ResourceKeyUtils.join(resourceKeyList(this.keys), ...this.loadedKeys.map(key => this.transformParam(key)));
this.loadedKeys = [];
this.resetIncludes();
} else {
if (this.isAlias(key)) {
const index = this.loadedKeys.findIndex(loadedKey => this.isAliasEqual(key!, loadedKey));
@@ -466,10 +463,10 @@ export abstract class CachedMapResource<
key = this.transformParam(key);
}
runInAction(() => ResourceKeyUtils.forEach(key!, key => {
ResourceKeyUtils.forEach(key, key => {
const metadata = this.getMetadata(key);
metadata.outdated = true;
}));
});
this.onDataOutdated.execute(key);
}
@@ -98,7 +98,10 @@ export abstract class CachedResource<
this.spy(this.onDataError, 'onDataError');
}
makeObservable<CachedResource<TData, TParam, TKey, TContext>, 'loader' | 'loadedKeys'>(this, {
makeObservable<
CachedResource<TData, TParam, TKey, TContext>,
'loader' | 'loadedKeys' | 'commitIncludes' | 'resetIncludes' | 'markOutdatedSync'
>(this, {
loadedKeys: observable,
data: observable,
loader: action,
@@ -107,6 +110,9 @@ export abstract class CachedResource<
markDataError: action,
markOutdated: action,
markUpdated: action,
commitIncludes: action,
markOutdatedSync: action,
resetIncludes: action,
});
}
@@ -360,7 +366,7 @@ export abstract class CachedResource<
break transform;
}
} else {
if (this.includes(alias.param, param)) {
if (alias.param === param) {
param = alias.getAlias(param);
deep++;
// eslint-disable-next-line no-labels
@@ -406,7 +412,7 @@ export abstract class CachedResource<
protected resetIncludes(): void {
for (const metadata of this.metadata.values()) {
metadata.includes = [...this.defaultIncludes];
metadata.includes = observable([...this.defaultIncludes]);
}
}
@@ -441,12 +447,16 @@ export abstract class CachedResource<
}
isAliasEqual(param: TParam, second: TParam): boolean {
if (param === second) {
return true;
}
return this.paramAliases.some(alias => {
if ('getter' in alias && alias.getter) {
return alias.param(param) && alias.param(second);
} else {
return alias.param === param && alias.param === second;
}
return false;
});
}
@@ -455,14 +465,10 @@ export abstract class CachedResource<
}
protected includes(param: TParam, second: TParam): boolean {
if (param === second) {
if (this.isAliasEqual(param, second)) {
return true;
}
if (this.isAlias(param) || this.isAlias(second)) {
return this.isAliasEqual(param, second);
}
param = this.transformParam(param);
second = this.transformParam(second);
return this.isKeyEqual(param, second);
@@ -188,6 +188,10 @@ export const ResourceKeyUtils: ResourceKeyUtils = {
key: ResourceKey<TKey>,
isEqual = (keyA: TKey, keyB: TKey) => keyA === keyB
): boolean {
if (param === key) {
return true;
}
if (isResourceKeyList(param)) {
return param.includes(key);
}
@@ -21,24 +21,38 @@
opacity: 1;
}
}
.theme-button__label {
@extend .mdc-button__label;
}
.theme-button__icon {
@extend .mdc-button__icon;
}
.theme-button_ripple {
@extend .mdc-button__ripple;
}
.theme-button_raised {
@extend .mdc-button--raised;
}
.theme-button_unelevated {
@extend .mdc-button--unelevated;
}
.theme-button_outlined {
@extend .mdc-button--outlined;
&[disabled] {
border-color: change-color($color: $mdc-theme-on-surface, $alpha: 0.37);
color: change-color($color: $mdc-theme-on-surface, $alpha: 0.37);
cursor: default;
pointer-events: none;
}
}
.theme-button_secondary {
@include mdc-button-ink-color(secondary, $query);
@include mdc-states($color: secondary, $query: $query, $ripple-target: $mdc-button-ripple-target);
@@ -54,6 +68,7 @@
@include mdc-states($color: on-secondary, $query: $query, $ripple-target: $mdc-button-ripple-target);
}
}
.theme-button_background {
@include mdc-button-ink-color(background, $query);
@include mdc-states($color: background, $query: $query, $ripple-target: $mdc-button-ripple-target);
@@ -69,4 +84,4 @@
@include mdc-states($color: on-secondary, $query: $query, $ripple-target: $mdc-button-ripple-target);
}
}
}
}
@@ -100,6 +100,7 @@ export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(forwar
menuData={menuData}
rtl={rtl}
menu={menu.current}
modal={modal}
style={style}
onItemClose={handlers.handleItemClose}
/>
@@ -0,0 +1,20 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { MenuStateReturn } from 'reakit/ts';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { IMenuData, IMenuItem } from '@cloudbeaver/core-view';
export interface IContextMenuItemProps {
item: IMenuItem;
menu: MenuStateReturn;
menuData: IMenuData;
style?: ComponentStyle;
onClick: () => void;
}
@@ -8,18 +8,17 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { MenuItem, MenuItemCheckbox, MenuItemRadio, MenuStateReturn } from 'reakit/Menu';
import { MenuItem, MenuItemCheckbox, MenuItemRadio } from 'reakit/Menu';
import styled, { use } from 'reshadow';
import { Checkbox, MenuItemElement, menuPanelStyles, Radio } from '@cloudbeaver/core-blocks';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IMenuActionItem } from '@cloudbeaver/core-view';
interface IMenuActionElementProps {
import type { IContextMenuItemProps } from './IContextMenuItemProps';
interface IMenuActionElementProps extends IContextMenuItemProps {
item: IMenuActionItem;
menu: MenuStateReturn;
style?: ComponentStyle;
onClick: () => void;
}
export const MenuActionElement = observer<IMenuActionElementProps>(function MenuActionElement({
@@ -13,7 +13,7 @@ import styled, { use } from 'reshadow';
import { MenuItemElement, menuPanelStyles } from '@cloudbeaver/core-blocks';
import { ComponentStyle, joinStyles, useStyles } from '@cloudbeaver/core-theming';
import { IMenuItem, IMenuData, MenuSubMenuItem, MenuSeparatorItem, MenuActionItem, MenuBaseItem } from '@cloudbeaver/core-view';
import { IMenuItem, IMenuData, MenuSubMenuItem, MenuSeparatorItem, MenuActionItem, MenuBaseItem, MenuCustomItem } from '@cloudbeaver/core-view';
import { MenuActionElement } from './MenuActionElement';
import { SubMenuElement } from './SubMenuElement';
@@ -23,13 +23,14 @@ export interface IMenuItemRendererProps extends Omit<React.ButtonHTMLAttributes<
item: IMenuItem;
menuData: IMenuData;
menu: MenuStateReturn; // from reakit useMenuState
modal?: boolean;
rtl?: boolean;
onItemClose?: () => void;
style?: ComponentStyle;
}
export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuItemRenderer({
item, rtl, menuData, menu, onItemClose, style,
item, modal, rtl, menuData, menu, onItemClose, style,
}) {
const styles = useStyles(menuPanelStyles, style);
const onClick = useCallback(() => {
@@ -40,21 +41,36 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
}
}, [item, onItemClose]);
if (item instanceof MenuCustomItem) {
const CustomMenuItem = item.getComponent();
return styled(styles)(
<CustomMenuItem
item={item}
menu={menu}
menuData={menuData}
style={style}
onClick={onClick}
/>
);
}
if (item instanceof MenuSubMenuItem) {
return styled(styles)(
<MenuItem
{...menu}
{...{ as: SubMenuElement }}
{...use({ hidden: item.hidden })}
id={item.id}
aria-label={item.menu.label}
itemRenderer={MenuItemRenderer}
rtl={rtl}
menuRtl={rtl}
menuData={menuData}
menuModal={modal}
subMenu={item}
style={style}
onItemClose={onItemClose}
onClick={onClick}
{...{ as: SubMenuElement }}
/>
);
}
@@ -68,6 +84,7 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
<MenuActionElement
item={item}
menu={menu}
menuData={menuData}
style={style}
onClick={onClick}
/>
@@ -13,7 +13,7 @@ import styled from 'reshadow';
import { getComputed, IMenuState, Menu, MenuItemElement, menuPanelStyles, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { DATA_CONTEXT_MENU_NESTED, IMenuData, IMenuSubMenuItem, MenuActionItem, MenuService, useMenu } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_MENU_NESTED, DATA_CONTEXT_SUBMENU_ITEM, IMenuData, IMenuSubMenuItem, MenuActionItem, MenuService, useMenu } from '@cloudbeaver/core-view';
import type { IMenuItemRendererProps } from './MenuItemRenderer';
@@ -21,7 +21,8 @@ interface ISubMenuElementProps extends Omit<React.ButtonHTMLAttributes<any>, 'st
menuData: IMenuData;
subMenu: IMenuSubMenuItem;
itemRenderer: React.FC<IMenuItemRendererProps>;
rtl?: boolean;
menuModal?: boolean;
menuRtl?: boolean;
onItemClose?: () => void;
style?: ComponentStyle;
}
@@ -32,7 +33,8 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
subMenu,
itemRenderer,
style,
rtl,
menuModal: modal,
menuRtl: rtl,
onItemClose,
...rest
},
@@ -43,6 +45,7 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
const styles = useStyles(menuPanelStyles, style);
const subMenuData = useMenu({ menu: subMenu.menu, context: menuData.context });
subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true);
subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, subMenu);
const handler = menuService.getHandler(subMenuData.context);
const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context));
@@ -72,6 +75,7 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
const loading = getComputed(() => handler?.isLoading?.(subMenuData.context));
const disabled = getComputed(() => handler?.isDisabled?.(subMenuData.context));
const MenuItemRenderer = itemRenderer;
const panelAvailable = subMenuData.available && !loading;
return styled(styles)(
<Menu
@@ -84,22 +88,28 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
item={item}
menuData={subMenuData}
menu={menu.current}
rtl={rtl}
modal={modal}
style={style}
onItemClose={handlers.handleItemClose}
/>
))}
rtl={rtl}
modal={modal}
style={style}
panelAvailable={subMenuData.available && !loading}
placement={rtl ? 'left-start' : 'right-start'}
panelAvailable={panelAvailable}
disabled={disabled}
getHasBindings={handlers.hasBindings}
submenu
onVisibleSwitch={handlers.handleVisibleSwitch}
{...rest}
>
<MenuItemElement
label={subMenu.menu.label}
icon={subMenu.menu.icon}
tooltip={subMenu.menu.tooltip}
label={subMenu.label ?? subMenu.menu.label}
icon={subMenu.icon ?? subMenu.menu.icon}
tooltip={subMenu.tooltip ?? subMenu.menu.tooltip}
panelAvailable={panelAvailable}
loading={loading}
style={style}
menu
@@ -31,7 +31,7 @@ export class TabsBootstrap extends Bootstrap {
this.actionService.addHandler({
id: 'tabs-base-handler',
isActionApplicable: (context, action) => {
const menu = context.find(DATA_CONTEXT_MENU, MENU_TAB);
const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_TAB);
const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT);
const tab = context.tryGet(DATA_CONTEXT_TAB_ID);
+11 -3
View File
@@ -29,6 +29,7 @@ type Props<T = Record<string, any>> = ExtractContainerProps<T> & React.PropsWith
localState?: MetadataMap<string, any>;
lazy?: boolean;
manual?: boolean;
autoSelect?: boolean;
tabList?: string[];
enabledBaseActions?: boolean;
onChange?: (tab: ITabData<T>) => void;
@@ -43,6 +44,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
localState,
children,
lazy = false,
autoSelect = true,
manual,
tabList,
enabledBaseActions,
@@ -53,10 +55,16 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
const props = rest as any as T;
if (
!selectedId
&& currentTabId === undefined
&& container
&& (currentTabId === undefined || currentTabId === null)
&& autoSelect
) {
const displayed = container.getIdList(props);
let displayed: string[] = [];
if (container) {
displayed = container.getIdList(props);
} else if (tabList) {
displayed = tabList;
}
if (displayed.length > 0) {
selectedId = displayed[0];
+1
View File
@@ -2,6 +2,7 @@ export * from './Clipboard/ClipboardBootstrap';
export * from './Clipboard/ClipboardService';
export * from './ContextMenu/ContextMenu';
export * from './ContextMenu/IContextMenuItemProps';
export * from './ContextMenu/MenuBar/MenuBar';
export * from './ContextMenu/MenuBar/MENU_BAR_DEFAULT_STYLES';
export * from './ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES';
@@ -0,0 +1,26 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export type LastPromiseGetterKey = Array<string | number | boolean | undefined | null>;
export type LastPromiseGetter<R> = (key: LastPromiseGetterKey, query: () => Promise<R>) => Promise<R>;
export function createLastPromiseGetter<R>(): LastPromiseGetter<R> {
let lastKey: LastPromiseGetterKey | undefined = undefined;
let lastPromise: Promise<R> | undefined = undefined;
return async (key: LastPromiseGetterKey, getter: () => Promise<R>) => {
const currentKeyStr = JSON.stringify(key);
const lastKeyStr = lastKey ? JSON.stringify(lastKey) : undefined;
if (currentKeyStr !== lastKeyStr) {
lastKey = key;
lastPromise = getter();
}
return lastPromise!;
};
}
+2 -1
View File
@@ -44,4 +44,5 @@ export * from './createPath';
export * from './debounce';
export * from './svgToDataUri';
export * from './uriToBlob';
export * from './utf8ToBase64';
export * from './utf8ToBase64';
export * from './createLastPromiseGetter';
@@ -15,7 +15,7 @@ import type { DeleteVersionedContextCallback, IDataContext } from './IDataContex
import type { IDataContextProvider } from './IDataContextProvider';
export class DataContext implements IDataContext {
private readonly map: Map<DataContextGetter<any>, any>;
readonly map: Map<DataContextGetter<any>, any>;
private readonly versions: MetadataMap<DataContextGetter<any>, number>;
fallback?: IDataContextProvider;
@@ -40,7 +40,7 @@ export class DataContext implements IDataContext {
return this.map.has(context) || this.fallback?.has(context) || false;
}
find<T>(context: DataContextGetter<T>, value: T): boolean {
hasValue<T>(context: DataContextGetter<T>, value: T): boolean {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let provider: IDataContextProvider = this;
@@ -57,6 +57,25 @@ export class DataContext implements IDataContext {
}
}
find<T>(context: DataContextGetter<T>, predicate: (value: T) => boolean): T | undefined {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let provider: IDataContextProvider = this;
while (true) {
const value = provider.map.get(context);
if (predicate(value)) {
return value;
}
if (provider.fallback) {
provider = provider.fallback;
} else {
return undefined;
}
}
}
set<T>(context: DataContextGetter<T>, value: T): DeleteVersionedContextCallback {
const data = this.map.get(context);
let version = this.versions.get(context);
@@ -15,6 +15,10 @@ export class DynamicDataContext implements IDataContext {
fallback: IDataContext;
contexts: Map<DataContextGetter<any>, DeleteVersionedContextCallback>;
get map() {
return this.fallback.map;
}
constructor(fallback: IDataContext) {
this.fallback = fallback;
this.contexts = new Map();
@@ -56,8 +60,12 @@ export class DynamicDataContext implements IDataContext {
return this.fallback.get(context);
}
find <T>(context: DataContextGetter<T>, value: T): boolean {
return this.fallback.find(context, value);
hasValue <T>(context: DataContextGetter<T>, value: T): boolean {
return this.fallback.hasValue(context, value);
}
find<T>(context: DataContextGetter<T>, predicate: (value: T) => boolean): T | undefined {
return this.fallback.find(context, predicate);
}
tryGet <T>(context: DataContextGetter<T>): T | undefined {
@@ -10,8 +10,10 @@ import type { DataContextGetter } from './DataContextGetter';
export interface IDataContextProvider {
fallback?: IDataContextProvider;
readonly map: Map<DataContextGetter<any>, any>;
has: (context: DataContextGetter<any>) => boolean;
get: <T>(context: DataContextGetter<T>) => T;
find: <T>(context: DataContextGetter<T>, value: T) => boolean;
find: <T>(context: DataContextGetter<T>, predicate: (item: T) => boolean) => T | undefined;
hasValue: <T>(context: DataContextGetter<T>, value: T) => boolean;
tryGet: <T>(context: DataContextGetter<T>) => T | undefined;
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { createDataContext } from '../DataContext/createDataContext';
import type { IMenuSubMenuItem } from './MenuItem/IMenuSubMenuItem';
export const DATA_CONTEXT_SUBMENU_ITEM = createDataContext<IMenuSubMenuItem>('submenu-item');
@@ -0,0 +1,36 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { IMenuItem } from './IMenuItem';
interface IMenuCustomItemCommonProperties<TExtraProps = unknown> {
hidden?: boolean;
disabled?: boolean;
getExtraProps?: () => TExtraProps;
}
export type ICustomMenuItemComponent<TExtraProps = unknown> = React.FC<ICustomMenuItemProps<TExtraProps> & TExtraProps>;
export interface ICustomMenuItemProps<TExtraProps = unknown> {
item: IMenuCustomItem<TExtraProps>;
style?: ComponentStyle;
className?: string;
}
export interface IMenuCustomItemOptions<TExtraProps = unknown>
extends IMenuCustomItemCommonProperties<TExtraProps> {
id: string;
getComponent: () => ICustomMenuItemComponent<TExtraProps>;
}
export interface IMenuCustomItem<TExtraProps = unknown>
extends IMenuItem, IMenuCustomItemCommonProperties<TExtraProps> {
getComponent?: () => ICustomMenuItemComponent<TExtraProps>;
}
@@ -15,5 +15,8 @@ export interface IMenuSubMenuEvents extends IMenuItemEvents {
export interface IMenuSubMenuItem extends IMenuItem {
menu: IMenu;
label?: string;
icon?: string;
tooltip?: string;
events?: IMenuSubMenuEvents;
}
@@ -0,0 +1,39 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { ICustomMenuItemComponent, IMenuCustomItem, IMenuCustomItemOptions } from './IMenuCustomItem';
import type { IMenuItemEvents } from './IMenuItem';
import { MenuItem } from './MenuItem';
interface IMenuCustomItemPropertyGetters<TExtraProps = unknown> {
getExtraProps?: () => TExtraProps;
isDisabled?: () => boolean;
}
export class MenuCustomItem<TExtraProps = unknown> extends MenuItem implements IMenuCustomItem<TExtraProps> {
private readonly isDisabled?: () => boolean;
readonly hidden?: boolean;
readonly getComponent: () => ICustomMenuItemComponent<TExtraProps>;
readonly getExtraProps?: () => TExtraProps;
get disabled(): boolean {
return this.isDisabled?.() ?? false;
}
constructor(
options: IMenuCustomItemOptions<TExtraProps>,
events?: IMenuItemEvents,
getters?: IMenuCustomItemPropertyGetters<TExtraProps>,
) {
super(options.id, events);
this.getComponent = options.getComponent;
this.isDisabled = getters?.isDisabled;
this.getExtraProps = getters?.getExtraProps;
}
}
@@ -12,15 +12,24 @@ import { MenuItem } from './MenuItem';
export class MenuSubMenuItem extends MenuItem implements IMenuSubMenuItem {
readonly menu: IMenu;
readonly label?: string;
readonly icon?: string;
readonly tooltip?: string;
readonly events?: IMenuSubMenuEvents;
readonly hidden: boolean;
constructor(
menu: IMenu,
label?: string,
icon?: string,
tooltip?: string,
events?: IMenuSubMenuEvents
) {
super(menu.id);
this.menu = menu;
this.label = label;
this.icon = icon;
this.tooltip = tooltip;
this.hidden = false;
this.events = events;
}
@@ -114,7 +114,7 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC
}
if (creator.menus) {
const applicable = creator.menus.some(menu => contexts.find(DATA_CONTEXT_MENU, menu));
const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu));
if (!applicable) {
return false;
+3
View File
@@ -30,10 +30,12 @@ export * from './DataContext/useDataContext';
export * from './DataContext/useDynamicDataContext';
export * from './Menu/MenuItem/IMenuActionItem';
export * from './Menu/MenuItem/IMenuBaseItem';
export * from './Menu/MenuItem/IMenuCustomItem';
export * from './Menu/MenuItem/IMenuItem';
export * from './Menu/MenuItem/IMenuSubMenuItem';
export * from './Menu/MenuItem/MenuActionItem';
export * from './Menu/MenuItem/MenuBaseItem';
export * from './Menu/MenuItem/MenuCustomItem';
export * from './Menu/MenuItem/MenuGroupItem';
export * from './Menu/MenuItem/MenuLazyItem';
export * from './Menu/MenuItem/MenuSeparatorItem';
@@ -42,6 +44,7 @@ export * from './Menu/createMenu';
export * from './Menu/DATA_CONTEXT_MENU_LOCAL';
export * from './Menu/DATA_CONTEXT_MENU_NESTED';
export * from './Menu/DATA_CONTEXT_MENU';
export * from './Menu/DATA_CONTEXT_SUBMENU_ITEM';
export * from './Menu/IMenu';
export * from './Menu/menuExtractActions';
export * from './Menu/MenuService';
@@ -0,0 +1,17 @@
# dependencies
/node_modules
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,48 @@
{
"name": "@cloudbeaver/plugin-connection-search",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-administration": "~0.1.0",
"@cloudbeaver/core-app": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-connections": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-dialogs": "~0.1.0",
"@cloudbeaver/core-events": "~0.1.0",
"@cloudbeaver/core-executor": "~0.1.0",
"@cloudbeaver/core-extensions": "~0.1.0",
"@cloudbeaver/core-localization": "~0.1.0",
"@cloudbeaver/core-notifications": "~0.1.0",
"@cloudbeaver/core-plugin": "~0.1.0",
"@cloudbeaver/core-projects": "~0.1.0",
"@cloudbeaver/core-product": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-routing": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"@cloudbeaver/core-ui": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"@cloudbeaver/core-view": "~0.1.0",
"@cloudbeaver/plugin-top-app-bar": "~0.1.0",
"@cloudbeaver/plugin-connections": "~0.1.0",
"react": "~18.x.x",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"reshadow": "~0.x.x"
}
}
@@ -0,0 +1,176 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { observable, makeObservable } from 'mobx';
import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor';
import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { ConnectionFormService, ConnectionFormState, IConnectionFormState } from '@cloudbeaver/plugin-connections';
import { SearchDatabase } from './SearchDatabase';
const formGetter = () => SearchDatabase;
@injectable()
export class ConnectionSearchService {
hosts = 'localhost';
databases: AdminConnectionSearchInfo[];
disabled = false;
formState: IConnectionFormState | null = null;
constructor(
private readonly notificationService: NotificationService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly connectionFormService: ConnectionFormService,
private readonly optionsPanelService: OptionsPanelService,
private readonly commonDialogService: CommonDialogService,
private readonly projectsService: ProjectsService,
) {
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
this.databases = [];
this.search = this.search.bind(this);
this.change = this.change.bind(this);
this.select = this.select.bind(this);
makeObservable(this, {
hosts: observable,
databases: observable,
disabled: observable,
formState: observable.shallow,
});
}
open(): void {
this.optionsPanelService.open(formGetter);
}
close(): void {
this.hosts = 'localhost';
this.databases = [];
}
async load(): Promise<void> {
if (this.databases.length === 0) {
await this.search();
}
}
async search(): Promise<void> {
if (this.disabled || !this.hosts || !this.hosts.trim()) {
return;
}
this.disabled = true;
try {
const hosts = this.hosts
.trim()
.replace(/[\s,|+-]+/gm, ' ')
.split(/[\s,|+-]/);
this.databases = await this.connectionInfoResource.searchDatabases(hosts);
} catch (exception: any) {
this.notificationService.logException(exception, 'Databases search failed');
} finally {
this.disabled = false;
}
}
private readonly closeHandler: IExecutorHandler<void> = async (data, contexts) => {
const isDialogClosed = await this.showUnsavedChangesDialog();
if (!isDialogClosed) {
ExecutorInterrupter.interrupt(contexts);
return;
}
this.clearFormState();
this.close();
};
private async showUnsavedChangesDialog(): Promise<boolean> {
if (
!this.formState
|| !this.optionsPanelService.isOpen(formGetter)
|| (
this.formState.config.connectionId
&& this.formState.projectId !== null
&& !this.connectionInfoResource.has(createConnectionParam(
this.formState.projectId,
this.formState.config.connectionId
))
)
) {
return true;
}
const state = await this.formState.checkFormState();
if (!state?.edited) {
return true;
}
const result = await this.commonDialogService.open(ConfirmationDialog, {
title: 'connections_public_connection_edit_cancel_title',
message: 'connections_public_connection_edit_cancel_message',
confirmActionText: 'ui_processing_ok',
});
return result !== DialogueStateResult.Rejected;
}
change(hosts: string): void {
this.hosts = hosts;
}
saveConnection() {
this.goBack();
}
goBack() {
this.clearFormState();
}
select(database: AdminConnectionSearchInfo): void {
if (!this.formState) {
this.formState = new ConnectionFormState(
this.connectionFormService,
this.connectionInfoResource
);
}
this.formState
.setOptions(
'create',
'public'
)
.setConfig(this.projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID, {
...this.connectionInfoResource.getEmptyConfig(),
driverId: database.defaultDriver,
host: database.host,
port: `${database.port}`,
})
.setAvailableDrivers(database.possibleDrivers);
this.formState.load();
}
private clearFormState() {
this.formState?.dispose();
this.formState = null;
}
}
@@ -8,22 +8,28 @@
import { observer } from 'mobx-react-lite';
import { useMapResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, type AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { ConnectionForm } from '@cloudbeaver/plugin-connections';
import { CustomConnection } from '../Manual/CustomConnection';
import { ConnectionSearchService } from './ConnectionSearchService';
import { DatabaseList } from './DatabaseList';
export const SearchDatabase: React.FC = observer(function SearchDatabase() {
const connectionSearchService = useService(ConnectionSearchService);
useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey);
function select(database: AdminConnectionSearchInfo) {
connectionSearchService.select(PROJECT_GLOBAL_ID, database);
connectionSearchService.select(database);
}
if (connectionSearchService.formState) {
return (
<ConnectionForm
state={connectionSearchService.formState}
onSave={() => connectionSearchService.saveConnection()}
onCancel={() => connectionSearchService.goBack()}
/>
);
}
return (
@@ -0,0 +1,49 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { EAdminPermission } from '@cloudbeaver/core-administration';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ProjectsService } from '@cloudbeaver/core-projects';
import { PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root';
import { MainMenuService, EMainMenu } from '@cloudbeaver/plugin-top-app-bar';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
@injectable()
export class SearchConnectionPluginBootstrap extends Bootstrap {
constructor(
private readonly mainMenuService: MainMenuService,
private readonly serverConfigResource: ServerConfigResource,
private readonly projectsService: ProjectsService,
private readonly permissionsService: PermissionsService,
private readonly connectionSearchService: ConnectionSearchService,
) {
super();
}
register(): void | Promise<void> {
this.mainMenuService.onConnectionClick.addHandler(() => {
this.projectsService.load();
});
this.mainMenuService.registerMenuItem(EMainMenu.mainMenuConnectionsPanel, {
id: 'searchConnection',
order: 3,
title: 'connections_connection_create_search_database',
isHidden: () => (
!this.projectsService.activeProject?.canCreateConnections
|| !this.permissionsService.has(EAdminPermission.admin)
),
onClick: () => {
this.connectionSearchService.open();
},
});
}
load(): void | Promise<void> { }
}
@@ -0,0 +1,3 @@
import { connectionSearchPlugin } from './manifest';
export default connectionSearchPlugin;
@@ -0,0 +1,14 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
import { SearchConnectionPluginBootstrap } from './SearchConnectionPluginBootstrap';
export const connectionSearchPlugin: PluginManifest = {
info: {
name: 'Search connection plugin',
},
providers: [
SearchConnectionPluginBootstrap,
ConnectionSearchService,
],
};
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
"references": [{ "path": "../*" }]
}
@@ -14,7 +14,7 @@ import { TemplateConnectionsResource } from './TemplateConnectionsResource';
export const connectionTemplate: PluginManifest = {
info: {
name: 'Connections plugin',
name: 'Template Connections plugin',
},
providers: [
@@ -9,17 +9,12 @@
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { CreateConnectionService } from '../CreateConnectionService';
import { ConnectionManualService } from './Manual/ConnectionManualService';
import { CustomConnection } from './Manual/CustomConnection';
import { ConnectionSearchService } from './Search/ConnectionSearchService';
import { SearchDatabase } from './Search/SearchDatabase';
@injectable()
export class CreateConnectionBaseBootstrap extends Bootstrap {
constructor(
private readonly createConnectionService: CreateConnectionService,
private readonly connectionManualService: ConnectionManualService,
private readonly connectionSearchService: ConnectionSearchService
) {
super();
}
@@ -31,19 +26,6 @@ export class CreateConnectionBaseBootstrap extends Bootstrap {
order: 1,
panel: () => CustomConnection,
});
this.createConnectionService.tabsContainer.add({
key: 'search',
name: 'connections_connection_create_search_database',
order: 2,
panel: () => SearchDatabase,
onOpen: () => this.connectionSearchService.load(),
options: {
configurationWizard: {
activationPriority: 2,
},
close: () => this.connectionSearchService.close(),
},
});
}
load(): void | Promise<void> { }
@@ -1,97 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { observable, makeObservable } from 'mobx';
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk';
import { CreateConnectionService } from '../../CreateConnectionService';
@injectable()
export class ConnectionSearchService {
hosts = 'localhost';
databases: AdminConnectionSearchInfo[];
get disabled(): boolean {
return this.createConnectionService.disabled;
}
set disabled(value: boolean) {
this.createConnectionService.disabled = value;
}
constructor(
private readonly notificationService: NotificationService,
private readonly connectionInfoResource: ConnectionInfoResource,
private createConnectionService: CreateConnectionService,
private readonly administrationScreenService: AdministrationScreenService
) {
makeObservable(this, {
hosts: observable,
databases: observable,
});
this.databases = [];
this.search = this.search.bind(this);
this.change = this.change.bind(this);
this.select = this.select.bind(this);
}
close(): void {
this.hosts = 'localhost';
this.databases = [];
}
async load(): Promise<void> {
if (this.administrationScreenService.isConfigurationMode && this.databases.length === 0) {
await this.search();
}
}
async search(): Promise<void> {
if (this.disabled || !this.hosts || !this.hosts.trim()) {
return;
}
this.disabled = true;
try {
const hosts = this.hosts
.trim()
.replace(/[\s,|+-]+/gm, ' ')
.split(/[\s,|+-]/);
this.databases = await this.connectionInfoResource.searchDatabases(hosts);
} catch (exception: any) {
this.notificationService.logException(exception, 'Databases search failed');
} finally {
this.disabled = false;
}
}
change(hosts: string): void {
this.hosts = hosts;
}
select(projectId: string, database: AdminConnectionSearchInfo): void {
this.createConnectionService.setConnectionTemplate(
projectId,
{
...this.connectionInfoResource.getEmptyConfig(),
driverId: database.defaultDriver,
host: database.host,
port: `${database.port}`,
},
database.possibleDrivers
);
}
}
@@ -1,7 +1,6 @@
import { connectionPlugin } from './manifest';
export * from './Administration/Connections/ConnectionsAdministration';
export * from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
export * from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
export * from './Administration/Connections/ConnectionsAdministrationNavService';
export * from './Administration/Connections/ConnectionsAdministrationService';
export * from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
@@ -12,7 +12,6 @@ import { ConnectionsAdministrationNavService } from './Administration/Connection
import { ConnectionsAdministrationService } from './Administration/Connections/ConnectionsAdministrationService';
import { CreateConnectionBaseBootstrap } from './Administration/Connections/CreateConnection/CreateConnectionBaseBootstrap';
import { ConnectionManualService } from './Administration/Connections/CreateConnection/Manual/ConnectionManualService';
import { ConnectionSearchService } from './Administration/Connections/CreateConnection/Search/ConnectionSearchService';
import { CreateConnectionService } from './Administration/Connections/CreateConnectionService';
import { ConnectionAccessTabService } from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService';
import { LocaleService } from './LocaleService';
@@ -28,7 +27,6 @@ export const connectionPlugin: PluginManifest = {
ConnectionsAdministrationNavService,
CreateConnectionService,
ConnectionManualService,
ConnectionSearchService,
CreateConnectionBaseBootstrap,
ConnectionAccessTabService,
],
@@ -11,10 +11,10 @@ import { observer } from 'mobx-react-lite';
import { useMemo, useState } from 'react';
import styled, { css } from 'reshadow';
import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, IProperty, Loader, PropertiesTable, useMapResource } from '@cloudbeaver/core-blocks';
import { DBDriverResource } from '@cloudbeaver/core-connections';
import { useStyles } from '@cloudbeaver/core-theming';
import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
import { uuid } from '@cloudbeaver/core-utils';
import type { IConnectionFormProps } from '../IConnectionFormProps';
@@ -66,10 +66,10 @@ export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps>
DBDriverResource,
{ key: (selected && formState.config.driverId) || null, includes: ['includeDriverProperties'] },
{
isActive: () => selected && !!formState.config.driverId,
active: selected && !!formState.config.driverId,
onData: driver => {
for (const key of Object.keys(formState.config.properties)) {
if (driver.driverProperties?.some(property => property.id === key)
if (driver.driverProperties.some(property => property.id === key)
|| state.propertiesList.some(property => property.key === key)) {
continue;
}
@@ -237,9 +237,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
tempConfig.description = state.config.description;
if (state.projectId === PROJECT_GLOBAL_ID) {
tempConfig.template = state.config.template;
}
tempConfig.template = state.projectId === PROJECT_GLOBAL_ID ? state.config.template : false;
tempConfig.driverId = state.config.driverId;
@@ -202,7 +202,8 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({
onChange={handleKeyUpload}
>
<Button
tag='div'
tag="div"
disabled={disabled || readonly || !enabled}
mod={['outlined']}
>
{translate('ui_file')}
@@ -13,7 +13,6 @@ import { ILoadableState, isContainsException, useMapResource, useObservableRef }
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import type { ITab } from '@cloudbeaver/plugin-navigation-tabs';
import type { IObjectViewerTabState } from '@cloudbeaver/plugin-object-viewer';
@@ -38,26 +37,30 @@ export function useDataViewerDatabaseDataModel(tab: ITab<IObjectViewerTabState>)
const dataPresentationService = useService(DataPresentationService);
const dataViewerDataChangeConfirmationService = useService(DataViewerDataChangeConfirmationService);
const connections = useMapResource(useDataViewerDatabaseDataModel, connectionInfoResource, CachedMapAllKey);
const connection = useMapResource(
useDataViewerDatabaseDataModel,
ConnectionInfoResource,
tab.handlerState.connectionKey ?? null
);
const state = useObservableRef<IDataViewerDatabaseDataModel>(() => ({
_exception: null,
_loading: false,
get exception() {
if (isContainsException(connections.exception)) {
return connections.exception;
if (isContainsException(connection.exception)) {
return connection.exception;
}
return this._exception;
},
isLoading(): boolean {
return connections.isLoading() || this._loading;
return connection.isLoading() || this._loading;
},
isLoaded(): boolean {
return connections.isLoaded() && dataViewerTableService.get(this.tab.handlerState.tableId || '') !== undefined;
return connection.isLoaded() && dataViewerTableService.get(this.tab.handlerState.tableId || '') !== undefined;
},
async reload() {
if (isContainsException(connections.exception)) {
connections.reload?.();
if (isContainsException(connection.exception)) {
connection.reload?.();
}
this.init();
},
@@ -42,7 +42,7 @@ export class TableHeaderService extends Bootstrap {
this.actionService.addHandler({
id: 'table-header-menu-base-handler',
isActionApplicable(context, action) {
const menu = context.find(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
const menu = context.hasValue(DATA_CONTEXT_MENU, DATA_VIEWER_DATA_MODEL_TOOLS_MENU);
const model = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL);
const resultIndex = context.tryGet(DATA_CONTEXT_DATA_VIEWER_DATABASE_DATA_MODEL_RESULT_INDEX);
@@ -80,14 +80,16 @@ export const TablePresentationBar = observer<Props>(function TablePresentationBa
}
};
if (presentations.length <= 1 && type === DataPresentationType.main) {
const main = type === DataPresentationType.main;
if (presentations.length <= 1 && main) {
return null;
}
return styled(style)(
<table-left-bar className={className}>
<TabsState currentTabId={presentationId}>
<TabList {...use({ flexible: type === DataPresentationType.main })}>
<TabsState currentTabId={presentationId} autoSelect={main}>
<TabList {...use({ flexible: main })}>
{presentations.map(presentation => (
<Tab
key={presentation.id}
@@ -42,7 +42,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
this.menuService.setHandler({
id: 'connection-selector-base',
isApplicable: context => context.find(DATA_CONTEXT_MENU, MENU_CONNECTION_SELECTOR),
isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_SELECTOR),
isLoading: () => this.connectionSelectorLoading,
isDisabled: () => (
!this.connectionSchemaManagerService.isConnectionChangeable
@@ -104,7 +104,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
this.menuService.setHandler({
id: 'connection-data-container-selector-base',
isApplicable: context => context.find(DATA_CONTEXT_MENU, MENU_CONNECTION_DATA_CONTAINER_SELECTOR),
isApplicable: context => context.hasValue(DATA_CONTEXT_MENU, MENU_CONNECTION_DATA_CONTAINER_SELECTOR),
isDisabled: () => (
!this.connectionSchemaManagerService.currentConnection?.connected
|| this.connectionSelectorLoading
@@ -27,15 +27,17 @@ import { MENU_CONNECTION_SELECTOR } from '../MENU_CONNECTION_SELECTOR';
const menuStyles = css`
Menu {
max-height: 400px;
overflow: auto;
& menu-box {
max-height: 80vh;
overflow: auto;
}
& menu-panel-item {
overflow-x: hidden;
overflow-x: hidden;
}
& menu-item-text {
max-width: 400px;
overflow-x: hidden;
text-overflow: ellipsis;
max-width: 400px;
overflow-x: hidden;
text-overflow: ellipsis;
}
}
`;
@@ -31,7 +31,7 @@ export class DDLViewerFooterService {
this.actionsService.addHandler({
id: 'ddl-viewer-footer-base-handler',
isActionApplicable(context, action) {
const menu = context.find(DATA_CONTEXT_MENU, MENU_DDL_VIEWER_FOOTER);
const menu = context.hasValue(DATA_CONTEXT_MENU, MENU_DDL_VIEWER_FOOTER);
const node = context.tryGet(DATA_CONTEXT_DDL_VIEWER_NODE);
if (!menu || !node) {
@@ -0,0 +1,17 @@
# dependencies
/node_modules
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,47 @@
{
"name": "@cloudbeaver/plugin-devtools",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-administration": "~0.1.0",
"@cloudbeaver/core-app": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-connections": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-dialogs": "~0.1.0",
"@cloudbeaver/core-events": "~0.1.0",
"@cloudbeaver/core-executor": "~0.1.0",
"@cloudbeaver/core-extensions": "~0.1.0",
"@cloudbeaver/core-localization": "~0.1.0",
"@cloudbeaver/core-notifications": "~0.1.0",
"@cloudbeaver/core-plugin": "~0.1.0",
"@cloudbeaver/core-product": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-routing": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"@cloudbeaver/core-ui": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"@cloudbeaver/core-view": "~0.1.0",
"@cloudbeaver/plugin-top-app-bar": "~0.1.0",
"@cloudbeaver/plugin-user-profile": "~0.1.0",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"react": "~18.x.x",
"reshadow": "~0.x.x"
}
}
@@ -0,0 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { createDataContext } from '@cloudbeaver/core-view';
export const DATA_CONTEXT_MENU_SEARCH = createDataContext<string>('menu-local', '');
@@ -0,0 +1,21 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { IContextMenuItemProps } from '@cloudbeaver/core-ui';
import { MenuCustomItem } from '@cloudbeaver/core-view';
import { SearchResourceMenuItemComponent } from './SearchResourceMenuItemComponent';
export class SearchResourceMenuItem extends MenuCustomItem<IContextMenuItemProps> {
constructor() {
super({
id: 'search-resource',
getComponent: () => SearchResourceMenuItemComponent,
});
}
}
@@ -0,0 +1,48 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IContextMenuItemProps } from '@cloudbeaver/core-ui';
import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_MENU_SEARCH } from './DATA_CONTEXT_MENU_SEARCH';
const styles = css`
search-box {
padding: 8px 12px;
}
`;
export const SearchResourceMenuItemComponent: ICustomMenuItemComponent<IContextMenuItemProps> = observer(function SearchResourceMenuItemComponent({
item,
menu,
onClick,
menuData,
className,
style,
}) {
const value = menuData.context.tryGet(DATA_CONTEXT_MENU_SEARCH) ?? '';
function handleChange(value: string) {
menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value);
}
return styled(useStyles(style, styles))(
<search-box dir="ltr" className={className}>
<input
name='search'
placeholder='Search for resource...'
value={value}
autoComplete="off"
onChange={event => handleChange(event.target.value)}
/>
</search-box>
);
});
@@ -0,0 +1,44 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
interface IDevToolsSettings {
enabled: boolean;
}
const DEVTOOLS = 'devtools';
@injectable()
export class DevToolsService {
get isEnabled(): boolean {
return this.settings.enabled;
}
private readonly settings: IDevToolsSettings;
constructor(
private readonly autoSaveService: LocalStorageSaveService,
) {
this.settings = {
enabled: false,
};
makeObservable<this, 'settings'>(this, {
settings: observable,
});
this.autoSaveService.withAutoSave(this.settings, DEVTOOLS);
}
switch() {
this.settings.enabled = !this.settings.enabled;
}
}
@@ -0,0 +1,217 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { EAdminPermission } from '@cloudbeaver/core-administration';
import { App, Bootstrap, DIService, injectable, IServiceConstructor } from '@cloudbeaver/core-di';
import { PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root';
import { CachedResource } from '@cloudbeaver/core-sdk';
import { ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_SUBMENU_ITEM, MenuBaseItem, MenuService } from '@cloudbeaver/core-view';
import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
import { MainMenuService } from '@cloudbeaver/plugin-top-app-bar';
import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile';
import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS';
import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH';
import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem';
import { DevToolsService } from './DevToolsService';
import { MENU_DEVTOOLS } from './menu/MENU_DEVTOOLS';
import { MENU_PLUGIN } from './menu/MENU_PLUGIN';
import { MENU_PLUGINS } from './menu/MENU_PLUGINS';
import { MENU_RESOURCE } from './menu/MENU_RESOURCE';
import { MENU_RESOURCES } from './menu/MENU_RESOURCES';
import { PluginSubMenuItem } from './menu/PluginSubMenuItem';
import { ResourceSubMenuItem } from './menu/ResourceSubMenuItem';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private readonly app: App,
private readonly diService: DIService,
private readonly menuService: MenuService,
private readonly actionService: ActionService,
private readonly mainMenuService: MainMenuService,
private readonly devToolsService: DevToolsService,
private readonly permissionsService: PermissionsService,
private readonly serverConfigResource: ServerConfigResource
) {
super();
}
register(): void {
this.menuService.addCreator({
isApplicable: context => {
if (!this.permissionsService.has(EAdminPermission.admin)) {
return false;
}
return context.get(DATA_CONTEXT_MENU) === TOP_NAV_BAR_SETTINGS_MENU;
},
getItems: (context, items) => [
ACTION_DEVTOOLS,
...items,
],
});
this.actionService.addHandler({
id: 'devtools',
isActionApplicable: (context, action) => [
ACTION_DEVTOOLS,
].includes(action),
isChecked: (context, action) => {
switch (action) {
case ACTION_DEVTOOLS: {
return this.devToolsService.isEnabled;
}
}
return false;
},
handler: (context, action) => {
switch (action) {
case ACTION_DEVTOOLS: {
this.devToolsService.switch();
break;
}
}
},
});
this.menuService.addCreator({
isApplicable: context => {
if (!this.devToolsService.isEnabled) {
return false;
}
return context.get(DATA_CONTEXT_MENU) === MENU_USER_PROFILE;
},
getItems: (context, items) => [
MENU_DEVTOOLS,
...items,
],
});
this.menuService.addCreator({
isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_DEVTOOLS,
getItems: (context, items) => {
const search = context.tryGet(DATA_CONTEXT_MENU_SEARCH);
if (search) {
return [
new SearchResourceMenuItem(),
...this.getResources(this.app
.getServices()
.filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))),
];
}
return [
new SearchResourceMenuItem(),
MENU_PLUGINS,
...items,
];
},
});
this.menuService.addCreator({
isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS,
getItems: (context, items) => [
...this.app
.getPlugins()
.sort((a, b) => a.info.name.localeCompare(b.info.name))
.map(plugin => new PluginSubMenuItem(plugin)),
...items,
],
});
this.menuService.addCreator({
isApplicable: context => {
if (context.get(DATA_CONTEXT_MENU) !== MENU_PLUGIN) {
return false;
}
const item = context.tryGet(DATA_CONTEXT_SUBMENU_ITEM);
if (item instanceof PluginSubMenuItem) {
return item.plugin.providers.some(provider => provider.prototype instanceof CachedResource);
}
return false;
},
getItems: (_, items) => [
MENU_RESOURCES,
...items,
],
});
this.menuService.addCreator({
isApplicable: context => (
context.get(DATA_CONTEXT_MENU) === MENU_RESOURCES
&& context.has(DATA_CONTEXT_SUBMENU_ITEM)
),
getItems: (context, items) => {
const item = context.find(DATA_CONTEXT_SUBMENU_ITEM, item => item instanceof PluginSubMenuItem);
if (!item) {
return items;
}
const plugin = this.app
.getPlugins()
.find(plugin => plugin.info.name === item.id);
if (!plugin) {
return items;
}
return [
...this.getResources(plugin.providers),
...items,
];
},
});
this.menuService.addCreator({
isApplicable: context => (
context.get(DATA_CONTEXT_MENU) === MENU_RESOURCE
&& context.get(DATA_CONTEXT_SUBMENU_ITEM) instanceof ResourceSubMenuItem
),
getItems: (context, items) => {
const item = context.get(DATA_CONTEXT_SUBMENU_ITEM) as ResourceSubMenuItem;
return [
new MenuBaseItem(
{
id: 'markOutdated',
label: 'Mark outdated',
tooltip: 'Outdate resource',
},
{
onSelect: () => {
const instance = this.diService.serviceInjector.getServiceByClass<CachedResource<any, any>>(
item.resource
);
instance.markOutdated(undefined);
},
}
),
...items,
];
},
});
}
load(): void | Promise<void> {
}
private getResources(providers: IServiceConstructor<any>[]): ResourceSubMenuItem[] {
return providers
.filter(service => service.prototype instanceof CachedResource)
.sort((a, b) => a.name.localeCompare(b.name))
.map(resource => new ResourceSubMenuItem(resource));
}
}
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view';
export const ACTION_DEVTOOLS = createAction('devtools', {
type: 'checkbox',
label: 'DevTools',
tooltip: 'Show DevTools menu',
});

Some files were not shown because too many files have changed in this diff Show More