mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-13 07:47:05 +08:00
Merge remote-tracking branch 'origin/devel' into CB-2507-configuration-for-token-lifetime
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-2
@@ -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();
|
||||
|
||||
+3
-8
@@ -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 {
|
||||
|
||||
-65
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
-10
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
+10
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
|
||||
+5
-5
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-19
@@ -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?.();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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]))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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!;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -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: [
|
||||
|
||||
-18
@@ -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> { }
|
||||
|
||||
-97
@@ -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,
|
||||
],
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
+1
-3
@@ -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')}
|
||||
|
||||
+11
-8
@@ -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();
|
||||
},
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
+5
-3
@@ -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}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+8
-6
@@ -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
Reference in New Issue
Block a user