mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into tech/CB-2229
This commit is contained in:
@@ -20,6 +20,7 @@ import io.cloudbeaver.model.rm.RMUtils;
|
||||
import org.eclipse.core.resources.IProject;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.auth.SMSessionContext;
|
||||
import org.jkiss.dbeaver.model.rm.RMProject;
|
||||
import org.jkiss.dbeaver.registry.BaseProjectImpl;
|
||||
@@ -55,7 +56,7 @@ public class VirtualProjectImpl extends BaseProjectImpl {
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return project.getName();
|
||||
return project.getDisplayName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -93,7 +94,7 @@ public class VirtualProjectImpl extends BaseProjectImpl {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected DataSourceRegistry createDataSourceRegistry() {
|
||||
return new WebDataSourceRegistryProxy(super.createDataSourceRegistry(), dataSourceFilter);
|
||||
protected DBPDataSourceRegistry createDataSourceRegistry() {
|
||||
return new WebDataSourceRegistryProxy(new DataSourceRegistry(this), dataSourceFilter);
|
||||
}
|
||||
}
|
||||
|
||||
+246
-13
@@ -7,30 +7,40 @@ import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.*;
|
||||
import org.jkiss.dbeaver.model.access.DBAAuthProfile;
|
||||
import org.jkiss.dbeaver.model.access.DBACredentialsProvider;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.net.DBWNetworkProfile;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.struct.DBSObjectFilter;
|
||||
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
|
||||
import org.jkiss.dbeaver.registry.DataSourceFolder;
|
||||
import org.jkiss.dbeaver.registry.DataSourceConfigurationManager;
|
||||
import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry;
|
||||
import org.jkiss.dbeaver.registry.DataSourceRegistry;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WebDataSourceRegistryProxy extends DataSourceRegistry {
|
||||
public class WebDataSourceRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry {
|
||||
private final DataSourceFilter dataSourceFilter;
|
||||
private final DataSourceRegistry dataSourceRegistry;
|
||||
|
||||
public WebDataSourceRegistryProxy(DataSourceRegistry dataSourceRegistry, DataSourceFilter filter) {
|
||||
super(dataSourceRegistry.getProject());
|
||||
this.dataSourceRegistry = dataSourceRegistry;
|
||||
this.dataSourceFilter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBPProject getProject() {
|
||||
return dataSourceRegistry.getProject();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataSourceDescriptor getDataSource(String id) {
|
||||
DataSourceDescriptor dataSource = super.getDataSource(id);
|
||||
public DBPDataSourceContainer getDataSource(String id) {
|
||||
DBPDataSourceContainer dataSource = dataSourceRegistry.getDataSource(id);
|
||||
if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource)) {
|
||||
return null;
|
||||
}
|
||||
@@ -39,17 +49,17 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataSourceDescriptor getDataSource(DBPDataSource dataSource) {
|
||||
public DBPDataSourceContainer getDataSource(DBPDataSource dataSource) {
|
||||
if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource.getContainer())) {
|
||||
return null;
|
||||
}
|
||||
return super.getDataSource(dataSource);
|
||||
return dataSourceRegistry.getDataSource(dataSource);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataSourceDescriptor findDataSourceByName(String name) {
|
||||
var dataSource = super.findDataSourceByName(name);
|
||||
public DBPDataSourceContainer findDataSourceByName(String name) {
|
||||
var dataSource = dataSourceRegistry.findDataSourceByName(name);
|
||||
if (dataSource != null) {
|
||||
if (dataSourceFilter == null || dataSourceFilter.filter(dataSource)) {
|
||||
return dataSource;
|
||||
@@ -61,7 +71,7 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
|
||||
@NotNull
|
||||
@Override
|
||||
public List<? extends DBPDataSourceContainer> getDataSourcesByProfile(@NotNull DBWNetworkProfile profile) {
|
||||
return super.getDataSourcesByProfile(profile)
|
||||
return dataSourceRegistry.getDataSourcesByProfile(profile)
|
||||
.stream()
|
||||
.filter(dataSourceFilter::filter)
|
||||
.collect(Collectors.toList());
|
||||
@@ -69,10 +79,233 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DataSourceDescriptor> getDataSources() {
|
||||
return super.getDataSources()
|
||||
public List<DBPDataSourceContainer> getDataSources() {
|
||||
return dataSourceRegistry.getDataSources()
|
||||
.stream()
|
||||
.filter(dataSourceFilter::filter)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DBPDataSourceContainer createDataSource(DBPDriver driver, DBPConnectionConfiguration connConfig) {
|
||||
return dataSourceRegistry.createDataSource(driver, connConfig);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DBPDataSourceContainer createDataSource(DBPDataSourceContainer source) {
|
||||
return dataSourceRegistry.createDataSource(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDataSourceListener(@NotNull DBPEventListener listener) {
|
||||
dataSourceRegistry.addDataSourceListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeDataSourceListener(@NotNull DBPEventListener listener) {
|
||||
return dataSourceRegistry.removeDataSourceListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDataSource(@NotNull DBPDataSourceContainer dataSource) {
|
||||
dataSourceRegistry.addDataSource(dataSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeDataSource(@NotNull DBPDataSourceContainer dataSource) {
|
||||
dataSourceRegistry.removeDataSource(dataSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSource(@NotNull DBPDataSourceContainer dataSource) {
|
||||
dataSourceRegistry.updateDataSource(dataSource);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<? extends DBPDataSourceFolder> getAllFolders() {
|
||||
return dataSourceRegistry.getAllFolders();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<? extends DBPDataSourceFolder> getRootFolders() {
|
||||
return dataSourceRegistry.getRootFolders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBPDataSourceFolder getFolder(String path) {
|
||||
return dataSourceRegistry.getFolder(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBPDataSourceFolder addFolder(DBPDataSourceFolder parent, String name) {
|
||||
return dataSourceRegistry.addFolder(parent, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFolder(DBPDataSourceFolder folder, boolean dropContents) {
|
||||
dataSourceRegistry.removeFolder(folder, dropContents);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DBSObjectFilter getSavedFilter(String name) {
|
||||
return dataSourceRegistry.getSavedFilter(name);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBSObjectFilter> getSavedFilters() {
|
||||
return dataSourceRegistry.getSavedFilters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSavedFilter(DBSObjectFilter filter) {
|
||||
dataSourceRegistry.updateSavedFilter(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSavedFilter(String filterName) {
|
||||
dataSourceRegistry.removeSavedFilter(filterName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DBWNetworkProfile getNetworkProfile(String name) {
|
||||
return dataSourceRegistry.getNetworkProfile(name);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBWNetworkProfile> getNetworkProfiles() {
|
||||
return dataSourceRegistry.getNetworkProfiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNetworkProfile(DBWNetworkProfile profile) {
|
||||
dataSourceRegistry.updateNetworkProfile(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNetworkProfile(DBWNetworkProfile profile) {
|
||||
dataSourceRegistry.removeNetworkProfile(profile);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DBAAuthProfile getAuthProfile(String id) {
|
||||
return dataSourceRegistry.getAuthProfile(id);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBAAuthProfile> getAllAuthProfiles() {
|
||||
return dataSourceRegistry.getAllAuthProfiles();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBAAuthProfile> getApplicableAuthProfiles(@Nullable DBPDriver driver) {
|
||||
return dataSourceRegistry.getApplicableAuthProfiles(driver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAuthProfile(DBAAuthProfile profile) {
|
||||
dataSourceRegistry.updateAuthProfile(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAuthProfile(DBAAuthProfile profile) {
|
||||
dataSourceRegistry.removeAuthProfile(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushConfig() {
|
||||
dataSourceRegistry.flushConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshConfig() {
|
||||
dataSourceRegistry.refreshConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getLastError() {
|
||||
return dataSourceRegistry.getLastError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasError() {
|
||||
return dataSourceRegistry.hasError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForErrors() throws DBException {
|
||||
dataSourceRegistry.checkForErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDataSourceListeners(DBPEvent event) {
|
||||
dataSourceRegistry.notifyDataSourceListeners(event);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ISecurePreferences getSecurePreferences() {
|
||||
return dataSourceRegistry.getSecurePreferences();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DBACredentialsProvider getAuthCredentialsProvider() {
|
||||
return dataSourceRegistry.getAuthCredentialsProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
dataSourceRegistry.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthCredentialsProvider(DBACredentialsProvider authCredentialsProvider) {
|
||||
dataSourceRegistry.setAuthCredentialsProvider(authCredentialsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<DBPDataSourceFolder> getTemporaryFolders() {
|
||||
return dataSourceRegistry.getTemporaryFolders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadDataSources(
|
||||
@NotNull List<DBPDataSourceConfigurationStorage> storages,
|
||||
@NotNull DataSourceConfigurationManager manager,
|
||||
boolean refresh,
|
||||
boolean purgeUntouched
|
||||
) {
|
||||
dataSourceRegistry.loadDataSources(storages, manager, refresh, purgeUntouched);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDataSources() {
|
||||
dataSourceRegistry.saveDataSources();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSourceConfigurationManager getConfigurationManager() {
|
||||
return dataSourceRegistry.getConfigurationManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveConfigurationToManager(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull DataSourceConfigurationManager configurationManager,
|
||||
@Nullable Predicate<DBPDataSourceContainer> filter
|
||||
) {
|
||||
dataSourceRegistry.saveConfigurationToManager(monitor, configurationManager, filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -22,6 +22,7 @@ import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractSessionFederated implements SMSessionFederated {
|
||||
@@ -56,6 +57,12 @@ public abstract class AbstractSessionFederated implements SMSessionFederated {
|
||||
return parentSession.getSessionPrincipal();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public LocalDateTime getSessionStart() {
|
||||
return parentSession.getSessionStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicationSession() {
|
||||
return false;
|
||||
|
||||
+7
-6
@@ -27,6 +27,7 @@ import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.SMCredentials;
|
||||
@@ -259,7 +260,7 @@ public class LocalResourceController implements RMController {
|
||||
DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry();
|
||||
registry.checkForErrors();
|
||||
DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer();
|
||||
((DataSourceRegistry)registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, null);
|
||||
((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, null);
|
||||
registry.checkForErrors();
|
||||
return new String(buffer.getData(), StandardCharsets.UTF_8);
|
||||
}
|
||||
@@ -267,22 +268,22 @@ public class LocalResourceController implements RMController {
|
||||
@Override
|
||||
public void saveProjectDataSources(@NotNull String projectId, @NotNull String configuration) throws DBException {
|
||||
final DBPProject project = getProjectMetadata(projectId);
|
||||
final DataSourceRegistry registry = (DataSourceRegistry) project.getDataSourceRegistry();
|
||||
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
final DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(StandardCharsets.UTF_8));
|
||||
final DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer();
|
||||
registry.loadDataSources(List.of(storage), manager, true, false);
|
||||
((DataSourcePersistentRegistry) registry).loadDataSources(List.of(storage), manager, true, false);
|
||||
registry.checkForErrors();
|
||||
registry.saveDataSources();
|
||||
((DataSourcePersistentRegistry) registry).saveDataSources();
|
||||
registry.checkForErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteProjectDataSources(@NotNull String projectId, @NotNull String[] dataSourceIds) throws DBException {
|
||||
final DBPProject project = getProjectMetadata(projectId);
|
||||
final DataSourceRegistry registry = (DataSourceRegistry) project.getDataSourceRegistry();
|
||||
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
|
||||
for (String dataSourceId : dataSourceIds) {
|
||||
final DataSourceDescriptor dataSource = registry.getDataSource(dataSourceId);
|
||||
final DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId);
|
||||
|
||||
if (dataSource != null) {
|
||||
registry.removeDataSource(dataSource);
|
||||
|
||||
+11
-5
@@ -16,10 +16,7 @@
|
||||
*/
|
||||
package io.cloudbeaver.model.session;
|
||||
|
||||
import io.cloudbeaver.DBWConstants;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.DataSourceFilter;
|
||||
import io.cloudbeaver.VirtualProjectImpl;
|
||||
import io.cloudbeaver.*;
|
||||
import io.cloudbeaver.model.WebAsyncTaskInfo;
|
||||
import io.cloudbeaver.model.WebConnectionInfo;
|
||||
import io.cloudbeaver.model.WebServerMessage;
|
||||
@@ -70,6 +67,9 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
@@ -160,6 +160,12 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
|
||||
return id;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public LocalDateTime getSessionStart() {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
public WebApplication getApplication() {
|
||||
return application;
|
||||
}
|
||||
@@ -357,7 +363,7 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
|
||||
this,
|
||||
filter);
|
||||
DBPDataSourceRegistry dataSourceRegistry = sessionProject.getDataSourceRegistry();
|
||||
((DataSourceRegistry) dataSourceRegistry).setAuthCredentialsProvider(this);
|
||||
dataSourceRegistry.setAuthCredentialsProvider(this);
|
||||
addSessionProject(sessionProject);
|
||||
if (!project.isShared() || application.isConfigurationMode()) {
|
||||
this.defaultProject = sessionProject;
|
||||
|
||||
+14
@@ -21,6 +21,10 @@ import io.cloudbeaver.model.WebConnectionFolderInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNLocalFolder;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNNode;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNProject;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNRoot;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
public class WebConnectionFolderUtils {
|
||||
@@ -44,4 +48,14 @@ public class WebConnectionFolderUtils {
|
||||
throw new DBWebException("Folder name '" + folderName + "' contains illegal characters: /");
|
||||
}
|
||||
}
|
||||
|
||||
public static DBPDataSourceFolder getParentFolder(DBNNode folderNode) throws DBWebException {
|
||||
if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) {
|
||||
return null;
|
||||
} else if (folderNode instanceof DBNLocalFolder) {
|
||||
return ((DBNLocalFolder) folderNode).getFolder();
|
||||
} else {
|
||||
throw new DBWebException("Navigator node '" + folderNode.getNodeItemPath() + "' is not a folder node");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebTestUtils {
|
||||
@@ -42,12 +43,24 @@ public class WebTestUtils {
|
||||
|
||||
|
||||
public static Map<String, Object> doPost(String apiUrl, String input, HttpClient client) throws Exception {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
return doPostWithHeaders(apiUrl, input, client, List.of());
|
||||
}
|
||||
|
||||
public static Map<String, Object> doPostWithHeaders(
|
||||
String apiUrl,
|
||||
String input,
|
||||
HttpClient client,
|
||||
List<String> headers
|
||||
) throws Exception {
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(input))
|
||||
.header("Content-Type", "application/json")
|
||||
.build();
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if (!headers.isEmpty()) {
|
||||
requestBuilder.headers(headers.toArray(String[]::new));
|
||||
}
|
||||
HttpRequest request = requestBuilder.build();
|
||||
HttpResponse<String> response = client.send(request,
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ type DriverInfo {
|
||||
embedded: Boolean
|
||||
enabled: Boolean!
|
||||
requiresServerName: Boolean
|
||||
requiresDatabaseName: Boolean
|
||||
|
||||
licenseRequired: Boolean
|
||||
license: String
|
||||
|
||||
@@ -112,7 +112,7 @@ extend type Query {
|
||||
limit: Int,
|
||||
onlyFolders: Boolean): [ NavigatorNodeInfo! ]!
|
||||
|
||||
# Get child nodes
|
||||
# Get node's parents
|
||||
navNodeParents( nodePath: ID! ): [ NavigatorNodeInfo! ]!
|
||||
|
||||
navNodeInfo( nodePath: ID! ): NavigatorNodeInfo!
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
|
||||
import org.jkiss.dbeaver.model.access.DBAAuthCredentials;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials;
|
||||
@@ -42,7 +43,6 @@ import org.jkiss.dbeaver.model.navigator.DBNModel;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNProject;
|
||||
import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
|
||||
import org.jkiss.dbeaver.model.net.ssh.SSHConstants;
|
||||
import org.jkiss.dbeaver.model.rm.RMProject;
|
||||
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
|
||||
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
|
||||
import org.jkiss.dbeaver.registry.DataSourceProviderDescriptor;
|
||||
@@ -169,7 +169,6 @@ public class WebServiceUtils extends WebCommonUtils {
|
||||
if (config.getServerName() != null) {
|
||||
dsConfig.setServerName(config.getServerName());
|
||||
}
|
||||
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
|
||||
}
|
||||
if (config.getProperties() != null) {
|
||||
Map<String, String> newProps = new LinkedHashMap<>();
|
||||
@@ -197,6 +196,9 @@ public class WebServiceUtils extends WebCommonUtils {
|
||||
if (config.getConfigurationType() != null) {
|
||||
dsConfig.setConfigurationType(config.getConfigurationType());
|
||||
}
|
||||
if (CommonUtils.isEmpty(config.getUrl())) {
|
||||
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
|
||||
}
|
||||
// Save network handlers
|
||||
if (config.getNetworkHandlersConfig() != null) {
|
||||
for (WebNetworkHandlerConfigInput nhc : config.getNetworkHandlersConfig()) {
|
||||
@@ -340,6 +342,9 @@ public class WebServiceUtils extends WebCommonUtils {
|
||||
DBNModel.updateConfigAndRefreshDatabases(projectNode.getDatabases());
|
||||
}
|
||||
|
||||
public static boolean isGlobalProject(DBPProject project) {
|
||||
return CommonUtils.equalObjects(CBApplication.getInstance().getDefaultProjectName(), project.getName());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -44,6 +44,7 @@ import java.util.Map;
|
||||
public class WebDatabaseDriverConfig {
|
||||
|
||||
public static final String URL_SERVER_FIELD = "{server}";
|
||||
public static final String URL_DATABASE_FIELD = ".*(?:\\{(?:database|file|folder)}).*";
|
||||
private final WebSession webSession;
|
||||
private final DBPDriver driver;
|
||||
private String id;
|
||||
@@ -249,4 +250,13 @@ public class WebDatabaseDriverConfig {
|
||||
public DBPDriverConfigurationType[] getConfigurationTypes() {
|
||||
return driver.getSupportedConfigurationTypes().toArray(DBPDriverConfigurationType[]::new);
|
||||
}
|
||||
|
||||
|
||||
@Property
|
||||
public boolean getRequiresDatabaseName() {
|
||||
if (driver.getSampleURL() == null) {
|
||||
return false;
|
||||
}
|
||||
return driver.getSampleURL().matches(URL_DATABASE_FIELD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ import java.util.Map;
|
||||
* Application configuration
|
||||
*/
|
||||
public class CBAppConfig extends BaseAuthWebAppConfiguration implements WebAuthConfiguration {
|
||||
public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = DataSourceNavigatorSettings.PRESET_FULL.getSettings();
|
||||
public static final DataSourceNavigatorSettings.Preset PRESET_WEB = new DataSourceNavigatorSettings.Preset("web", "Web", "Default view");
|
||||
|
||||
public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = PRESET_WEB.getSettings();
|
||||
|
||||
private boolean supportsCustomConnections;
|
||||
private boolean supportsConnectionBrowser;
|
||||
|
||||
+11
-6
@@ -21,17 +21,14 @@ import io.cloudbeaver.DBWConstants;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.WebServiceUtils;
|
||||
import io.cloudbeaver.model.*;
|
||||
import io.cloudbeaver.model.app.BaseWebApplication;
|
||||
import io.cloudbeaver.model.app.WebAppConfiguration;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.registry.WebHandlerRegistry;
|
||||
import io.cloudbeaver.registry.WebSessionHandlerDescriptor;
|
||||
import io.cloudbeaver.server.CBAppConfig;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.service.core.DBWServiceCore;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import io.cloudbeaver.utils.WebConnectionFolderUtils;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
@@ -55,7 +52,6 @@ import org.jkiss.dbeaver.registry.DataSourceDescriptor;
|
||||
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -294,6 +290,8 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
connectionInfo.clearSavedCredentials();
|
||||
}
|
||||
// Mark all specified network configs as saved
|
||||
boolean[] saveConfig = new boolean[1];
|
||||
|
||||
if (networkCredentials != null) {
|
||||
networkCredentials.forEach(c -> {
|
||||
if (CommonUtils.toBoolean(c.isSavePassword()) && !CommonUtils.isEmpty(c.getUserName())) {
|
||||
@@ -302,7 +300,7 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
handlerCfg.setUserName(c.getUserName());
|
||||
handlerCfg.setPassword(c.getPassword());
|
||||
handlerCfg.setSavePassword(true);
|
||||
dataSourceContainer.persistConfiguration();
|
||||
saveConfig[0] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -316,6 +314,13 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
true);
|
||||
|
||||
WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, dataSourceContainer, dataSourceContainer.getConnectionConfiguration());
|
||||
saveConfig[0] = true;
|
||||
}
|
||||
if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) {
|
||||
// Do not flush config for global project (only admin can do it - CB-2415)
|
||||
saveConfig[0] = false;
|
||||
}
|
||||
if (saveConfig[0]) {
|
||||
dataSourceContainer.persistConfiguration();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ public class WebNavigatorNodeInfo {
|
||||
} else if (node instanceof DBNLocalFolder) {
|
||||
DataSourceFolder folder = (DataSourceFolder) ((DBNLocalFolder) node).getFolder();
|
||||
String projectName = folder.getDataSourceRegistry().getProject().getName();
|
||||
Set<DBPDataSourceFolder> tempFolders = ((DataSourceRegistry) folder.getDataSourceRegistry()).getTemporaryFolders();
|
||||
Set<DBPDataSourceFolder> tempFolders = folder.getDataSourceRegistry().getTemporaryFolders();
|
||||
isShared = !projectName.equals(session.getUserId()) || tempFolders.contains(folder);
|
||||
}
|
||||
if (isShared) {
|
||||
|
||||
+13
-8
@@ -466,17 +466,22 @@ public class WebServiceNavigator implements DBWServiceNavigator {
|
||||
}
|
||||
checkProjectEditAccess(node, session);
|
||||
if (node instanceof DBNDataSource) {
|
||||
DBPDataSourceFolder folder;
|
||||
if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) {
|
||||
folder = null;
|
||||
} else if (folderNode instanceof DBNLocalFolder) {
|
||||
folder = ((DBNLocalFolder) folderNode).getFolder();
|
||||
} else {
|
||||
throw new DBWebException("Navigator node '" + folderNodePath + "' is not a folder node");
|
||||
}
|
||||
DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode);
|
||||
((DBNDataSource) node).moveToFolder(folderNode.getOwnerProject(), folder);
|
||||
node.getOwnerProject().getDataSourceRegistry().updateDataSource(
|
||||
((DBNDataSource) node).getDataSourceContainer());
|
||||
} else if (node instanceof DBNLocalFolder) {
|
||||
DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode);
|
||||
if (folder != null) {
|
||||
List<String> siblings = Arrays.stream(folder.getChildren())
|
||||
.map(DBPDataSourceFolder::getName)
|
||||
.collect(Collectors.toList());
|
||||
if (siblings.contains(node.getName())) {
|
||||
throw new DBWebException("Node " + folderNodePath + " contains folder with name '" + node.getName() + "'");
|
||||
}
|
||||
}
|
||||
((DBNLocalFolder) node).getFolder().setParent(folder);
|
||||
WebServiceUtils.updateConfigAndRefreshDatabases(session, node.getOwnerProject().getId());
|
||||
} else if (node instanceof DBNResourceManagerResource) {
|
||||
boolean rmNewNode = folderNode instanceof DBNAbstractResourceManagerNode;
|
||||
DBNResourceManagerResource rmOldNode = (DBNResourceManagerResource) node;
|
||||
|
||||
+2
-2
@@ -660,7 +660,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
String connectionId
|
||||
) throws DBWebException {
|
||||
DBPProject globalProject = webSession.getProjectById(projectId);
|
||||
if (!CommonUtils.equalObjects(globalProject.getName(), CBApplication.getInstance().getDefaultProjectName())) {
|
||||
if (!WebServiceUtils.isGlobalProject(globalProject)) {
|
||||
throw new DBWebException("Project '" + projectId + "'is not global");
|
||||
}
|
||||
try {
|
||||
@@ -685,7 +685,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
@NotNull List<String> subjects
|
||||
) throws DBWebException {
|
||||
DBPProject globalProject = webSession.getProjectById(projectId);
|
||||
if (!CommonUtils.equalObjects(globalProject.getName(), CBApplication.getInstance().getDefaultProjectName())) {
|
||||
if (!WebServiceUtils.isGlobalProject(globalProject)) {
|
||||
throw new DBWebException("Project '" + projectId + "'is not global");
|
||||
}
|
||||
DBPDataSourceContainer dataSource = getGlobalRegistry(webSession).getDataSource(connectionId);
|
||||
|
||||
+4
-31
@@ -16,31 +16,26 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.auth;
|
||||
|
||||
import io.cloudbeaver.DBWUserIdentity;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.auth.SMAuthProviderExternal;
|
||||
import io.cloudbeaver.auth.provider.rp.RPAuthProvider;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebUser;
|
||||
import io.cloudbeaver.model.session.WebSessionAuthProcessor;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.DBWSessionHandler;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
|
||||
import org.jkiss.dbeaver.model.auth.SMSession;
|
||||
import org.jkiss.dbeaver.model.security.SMConstants;
|
||||
import org.jkiss.dbeaver.model.security.SMController;
|
||||
import org.jkiss.dbeaver.model.security.exception.SMException;
|
||||
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
|
||||
import org.jkiss.dbeaver.registry.auth.AuthProviderRegistry;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -70,7 +65,6 @@ public class RPSessionHandler implements DBWSessionHandler {
|
||||
String userName = request.getHeader(RPAuthProvider.X_USER);
|
||||
String roles = request.getHeader(RPAuthProvider.X_ROLE);
|
||||
List<String> userRoles = roles == null ? Collections.emptyList() : List.of(roles.split("\\|"));
|
||||
SMSession authSession;
|
||||
if (userName != null) {
|
||||
try {
|
||||
Map<String, Object> credentials = new HashMap<>();
|
||||
@@ -79,39 +73,18 @@ public class RPSessionHandler implements DBWSessionHandler {
|
||||
sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLES, userRoles);
|
||||
Map<String, Object> userCredentials = authProviderExternal.authExternalUser(
|
||||
webSession.getProgressMonitor(), sessionParameters, credentials);
|
||||
String currentSmSessionId = webSession.getUser() == null ? null : webSession.getUserContext().getSmSessionId();
|
||||
try {
|
||||
SMAuthInfo smAuthInfo = securityController.authenticate(
|
||||
webSession.getSessionId(),
|
||||
webSession.getUserContext().getSmSessionId(),
|
||||
currentSmSessionId,
|
||||
sessionParameters,
|
||||
WebSession.CB_SESSION_TYPE, authProvider.getId(), null, userCredentials);
|
||||
webSession.updateSMAuthInfo(smAuthInfo);
|
||||
new WebSessionAuthProcessor(webSession, smAuthInfo, false).authenticateSession();
|
||||
} catch (SMException e) {
|
||||
log.debug("Error during user authentication", e);
|
||||
throw e;
|
||||
}
|
||||
WebUser user = webSession.getUser();
|
||||
DBWUserIdentity userIdentity = authProviderExternal.getUserIdentity(
|
||||
webSession.getProgressMonitor(), sessionParameters, credentials);
|
||||
|
||||
if (CommonUtils.isEmpty(user.getDisplayName())) {
|
||||
user.setDisplayName(userIdentity.getDisplayName());
|
||||
}
|
||||
authSession = authProviderExternal.openSession(
|
||||
webSession.getProgressMonitor(),
|
||||
webSession,
|
||||
sessionParameters,
|
||||
userCredentials);
|
||||
|
||||
WebAuthInfo authInfo = new WebAuthInfo(
|
||||
webSession,
|
||||
user,
|
||||
authProvider,
|
||||
userIdentity,
|
||||
authSession,
|
||||
OffsetDateTime.now());
|
||||
authInfo.setMessage("Authenticated with " + authProvider.getLabel() + " provider");
|
||||
webSession.addAuthInfo(authInfo);
|
||||
} catch (Exception e) {
|
||||
throw new DBWebException("Error", e);
|
||||
}
|
||||
|
||||
+8
@@ -24,6 +24,8 @@ import org.jkiss.dbeaver.model.auth.SMSession;
|
||||
import org.jkiss.dbeaver.model.auth.SMSessionContext;
|
||||
import org.jkiss.dbeaver.model.auth.SMSessionPrincipal;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Local auth provider
|
||||
*/
|
||||
@@ -65,6 +67,12 @@ public class LocalAuthSession implements SMSession {
|
||||
return webSession.getSessionId();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public LocalDateTime getSessionStart() {
|
||||
return webSession.getSessionStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicationSession() {
|
||||
return false;
|
||||
|
||||
+42
@@ -1,15 +1,23 @@
|
||||
package io.cloudbeaver.test.platform;
|
||||
|
||||
import io.cloudbeaver.auth.provider.rp.RPAuthProvider;
|
||||
import io.cloudbeaver.utils.WebTestUtils;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AuthenticationTest {
|
||||
public static final String GQL_TEMPLATE_OPEN_SESSION = "openSession.json";
|
||||
public static final String GQL_TEMPLATE_ACTIVE_USER = "activeUser.json";
|
||||
public static final String REVERSE_PROXY_TEST_USER = "reverseProxyTestUser";
|
||||
|
||||
@Test
|
||||
public void testLoginUser() throws Exception {
|
||||
@@ -18,4 +26,38 @@ public class AuthenticationTest {
|
||||
client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL);
|
||||
Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReverseProxyAnonymousModeLogin() throws Exception {
|
||||
HttpClient client = CEServerTestSuite.getClient();
|
||||
Map<String, Object> sessionInfo = openSession(client);
|
||||
Assert.assertTrue(JSONUtils.getBoolean(sessionInfo, "valid"));
|
||||
Map<String, Object> activeUser = getActiveUser(client);
|
||||
Assert.assertEquals(REVERSE_PROXY_TEST_USER, JSONUtils.getString(activeUser, "userId"));
|
||||
}
|
||||
|
||||
private Map<String, Object> openSession(HttpClient client) throws Exception {
|
||||
Map<String, Object> data = doPostQuery(client, GQL_TEMPLATE_OPEN_SESSION);
|
||||
if (data != null) {
|
||||
return JSONUtils.getObject(data, "session");
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
private Map<String, Object> getActiveUser(HttpClient client) throws Exception {
|
||||
Map<String, Object> data = doPostQuery(client, GQL_TEMPLATE_ACTIVE_USER);
|
||||
if (data != null) {
|
||||
return JSONUtils.getObject(data, "user");
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
private Map<String, Object> doPostQuery(HttpClient client, String gqlScript) throws Exception {
|
||||
String input = WebTestUtils.readScriptTemplate(gqlScript, CEServerTestSuite.getScriptsPath());
|
||||
List<String> headers = List.of(RPAuthProvider.X_USER, REVERSE_PROXY_TEST_USER, RPAuthProvider.X_ROLE, "user");
|
||||
Map<String, Object> map = WebTestUtils.doPostWithHeaders(CEServerTestSuite.GQL_API_URL, input, client, headers);
|
||||
return JSONUtils.getObjectOrNull(map, "data");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
server: {
|
||||
serverPort: "${CLOUDBEAVER_TEST_PORT:18978}",
|
||||
serverName: "CloudBeaver CE Test Server",
|
||||
|
||||
workspaceLocation: "workspace",
|
||||
contentRoot: "workspace/web",
|
||||
@@ -36,8 +37,10 @@
|
||||
anonymousAccessEnabled: true,
|
||||
anonymousUserRole: "user",
|
||||
supportsCustomConnections: true,
|
||||
enableReverseProxyAuth: true,
|
||||
enabledAuthProviders: [
|
||||
"local"
|
||||
"local",
|
||||
"reverseProxy"
|
||||
],
|
||||
enabledFeatures: [
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"query": "\n query activeUser {\n user: activeUser {\n userId\n }\n}\n ",
|
||||
"operationName": "activeUser"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"query": "\n mutation openSession($defaultLocale: String) {\n session: openSession(defaultLocale: $defaultLocale) {\n valid\n }\n}\n ",
|
||||
"variables": {
|
||||
"defaultLocale": "en"
|
||||
},
|
||||
"operationName": "openSession"
|
||||
}
|
||||
@@ -90,7 +90,7 @@ const styles = css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon {
|
||||
@@ -112,12 +112,12 @@ const styles = css`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 12px;
|
||||
|
||||
|
||||
& IconOrImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
&:not(:empty) + input {
|
||||
padding-left: 34px !important;
|
||||
}
|
||||
@@ -131,8 +131,8 @@ type BaseProps<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>,
|
||||
searchable?: boolean;
|
||||
defaultValue?: TKey;
|
||||
loading?: boolean;
|
||||
keySelector: (item: TValue, index: number) => TKey;
|
||||
valueSelector: (item: TValue) => string;
|
||||
keySelector?: (item: TValue, index: number) => TKey;
|
||||
valueSelector?: (item: TValue) => string;
|
||||
titleSelector?: (item: TValue) => string | undefined;
|
||||
iconSelector?: (item: TValue) => string | React.ReactElement | undefined;
|
||||
isDisabled?: (item: TValue) => boolean;
|
||||
|
||||
@@ -11,7 +11,7 @@ export function isControlPresented(
|
||||
): boolean {
|
||||
if (state !== undefined && name !== undefined) {
|
||||
if (name in state) {
|
||||
return state[name] !== null;
|
||||
return state[name] !== null && state[name] !== undefined;
|
||||
}
|
||||
return defaultValue !== undefined;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', 'Search for user or role name'],
|
||||
['connections_connection_access_admin_info', 'Administrators see all connections.'],
|
||||
['connections_connection_description', 'Description'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_driver', 'Driver'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', 'Host'],
|
||||
|
||||
@@ -42,6 +42,7 @@ export default [
|
||||
['connections_connection_access_admin_info', 'Gli amministratori vedono tutte le connessioni.'],
|
||||
['connections_connection_description', 'Descrizione'],
|
||||
['connections_connection_driver', 'Driver'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', 'Host'],
|
||||
['connections_connection_port', 'Porta'],
|
||||
|
||||
@@ -36,6 +36,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', 'Поиск по имени пользователя или роли'],
|
||||
['connections_connection_access_admin_info', 'Администраторы видят все подключения.'],
|
||||
['connections_connection_description', 'Описание'],
|
||||
['connections_connection_project', 'Проект'],
|
||||
['connections_connection_driver', 'Драйвер'],
|
||||
['connections_connection_configuration', 'Конфигурация'],
|
||||
['connections_connection_host', 'Хост'],
|
||||
|
||||
@@ -39,6 +39,7 @@ export default [
|
||||
['connections_connection_access_filter_placeholder', '搜索用户或角色名称'],
|
||||
['connections_connection_access_admin_info', '管理员查看所有连接。'],
|
||||
['connections_connection_description', '描述'],
|
||||
['connections_connection_project', 'Project'],
|
||||
['connections_connection_driver', '驱动'],
|
||||
['connections_connection_configuration', 'Configuration'],
|
||||
['connections_connection_host', '主机'],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { action, makeObservable } from 'mobx';
|
||||
import { action, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { SessionPermissionsResource, EPermission } from '@cloudbeaver/core-root';
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { NavNode } from './EntityTypes';
|
||||
import { NodeManagerUtils } from './NodeManagerUtils';
|
||||
|
||||
type NavNodeInfo = NavNodeInfoFragment;
|
||||
|
||||
@@ -99,10 +100,14 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
|
||||
const parents: string[] = [];
|
||||
let current = this.get(key);
|
||||
|
||||
if (!current) {
|
||||
return NodeManagerUtils.parentsFromPath(key);
|
||||
}
|
||||
|
||||
while (
|
||||
current
|
||||
&& current.parentId !== current.id
|
||||
&& current.parentId !== ROOT_NODE_PATH
|
||||
// && current.parentId !== ROOT_NODE_PATH
|
||||
) {
|
||||
parents.unshift(current.parentId);
|
||||
current = this.get(current.parentId);
|
||||
@@ -174,13 +179,42 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
|
||||
}
|
||||
|
||||
private async loadNodeInfo(nodePath: string): Promise<NavNode> {
|
||||
if (this.has(nodePath)) {
|
||||
const metadata = this.metadata.get(nodePath);
|
||||
const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({
|
||||
nodePath,
|
||||
withDetails: metadata.withDetails,
|
||||
});
|
||||
|
||||
return this.navNodeInfoToNavNode(navNodeInfo);
|
||||
} else {
|
||||
return await this.loadNodeParents(nodePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadNodeParents(nodePath: string): Promise<NavNode> {
|
||||
const metadata = this.metadata.get(nodePath);
|
||||
const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({
|
||||
const { node, parents } = await this.graphQLService.sdk.getNodeParents({
|
||||
nodePath,
|
||||
withDetails: metadata.withDetails,
|
||||
});
|
||||
|
||||
return this.navNodeInfoToNavNode(navNodeInfo);
|
||||
|
||||
return runInAction(() => {
|
||||
const navNode = this.navNodeInfoToNavNode(node, parents[0]?.id ?? ROOT_NODE_PATH);
|
||||
|
||||
this.updateNode(
|
||||
resourceKeyList(parents.map(node => node.id), node.id),
|
||||
[
|
||||
...parents.reduce((list, node, index, array) => {
|
||||
list.push(this.navNodeInfoToNavNode(node, array[index + 1]?.id ?? ROOT_NODE_PATH));
|
||||
return list;
|
||||
}, [] as NavNode[]),
|
||||
navNode,
|
||||
]
|
||||
);
|
||||
return navNode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,21 +362,7 @@ export class NavNodeManagerService extends Bootstrap {
|
||||
canOpen = true;
|
||||
};
|
||||
|
||||
const getParents = () => {
|
||||
const parents: string[] = [];
|
||||
let parent = this.getNode(nodeId);
|
||||
|
||||
if (!parent) {
|
||||
return NodeManagerUtils.parentsFromPath(nodeId);
|
||||
}
|
||||
|
||||
while (parent && parent.id !== parent.parentId) {
|
||||
parents.unshift(parent.parentId);
|
||||
parent = this.getNode(parent.parentId);
|
||||
}
|
||||
|
||||
return parents;
|
||||
};
|
||||
const getParents = () => this.navNodeInfoResource.getParents(nodeId);
|
||||
|
||||
const loadParents = async (parents: string[]) => {
|
||||
let parentId = ROOT_NODE_PATH;
|
||||
|
||||
@@ -11,14 +11,14 @@ query navGetStructContainers(
|
||||
) {
|
||||
catalogList {
|
||||
catalog {
|
||||
...NavNodeInfo
|
||||
...NavNodeInfo
|
||||
}
|
||||
schemaList {
|
||||
...NavNodeInfo
|
||||
...NavNodeInfo
|
||||
}
|
||||
}
|
||||
schemaList {
|
||||
...NavNodeInfo
|
||||
...NavNodeInfo
|
||||
}
|
||||
supportsCatalogChange
|
||||
supportsSchemaChange
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
query getNodeParents(
|
||||
$nodePath: ID!
|
||||
$withDetails: Boolean!
|
||||
) {
|
||||
node: navNodeInfo(nodePath: $nodePath) {
|
||||
...NavNodeInfo
|
||||
}
|
||||
parents: navNodeParents(nodePath: $nodePath) {
|
||||
...NavNodeInfo
|
||||
}
|
||||
}
|
||||
@@ -2330,6 +2330,14 @@ export type GetNavNodeFullNameQueryVariables = Exact<{
|
||||
|
||||
export type GetNavNodeFullNameQuery = { navNodeInfo: { fullName?: string } };
|
||||
|
||||
export type GetNodeParentsQueryVariables = Exact<{
|
||||
nodePath: Scalars['ID'];
|
||||
withDetails: Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> };
|
||||
|
||||
export type NavDeleteNodesMutationVariables = Exact<{
|
||||
nodePaths: Array<Scalars['ID']> | Scalars['ID'];
|
||||
}>;
|
||||
@@ -3779,6 +3787,16 @@ export const GetNavNodeFullNameDocument = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const GetNodeParentsDocument = `
|
||||
query getNodeParents($nodePath: ID!, $withDetails: Boolean!) {
|
||||
node: navNodeInfo(nodePath: $nodePath) {
|
||||
...NavNodeInfo
|
||||
}
|
||||
parents: navNodeParents(nodePath: $nodePath) {
|
||||
...NavNodeInfo
|
||||
}
|
||||
}
|
||||
${NavNodeInfoFragmentDoc}`;
|
||||
export const NavDeleteNodesDocument = `
|
||||
mutation navDeleteNodes($nodePaths: [ID!]!) {
|
||||
navDeleteNodes(nodePaths: $nodePaths)
|
||||
@@ -4356,6 +4374,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
|
||||
getNavNodeFullName(variables: GetNavNodeFullNameQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetNavNodeFullNameQuery> {
|
||||
return withWrapper((wrappedRequestHeaders) => client.request<GetNavNodeFullNameQuery>(GetNavNodeFullNameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNavNodeFullName', 'query');
|
||||
},
|
||||
getNodeParents(variables: GetNodeParentsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetNodeParentsQuery> {
|
||||
return withWrapper((wrappedRequestHeaders) => client.request<GetNodeParentsQuery>(GetNodeParentsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNodeParents', 'query');
|
||||
},
|
||||
navDeleteNodes(variables: NavDeleteNodesMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<NavDeleteNodesMutation> {
|
||||
return withWrapper((wrappedRequestHeaders) => client.request<NavDeleteNodesMutation>(NavDeleteNodesDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'navDeleteNodes', 'mutation');
|
||||
},
|
||||
|
||||
@@ -23,11 +23,12 @@ import { AuthDialogService } from './Dialog/AuthDialogService';
|
||||
import type { IAuthOptions } from './IAuthOptions';
|
||||
import { isAutoLoginSessionAction } from './isAutoLoginSessionAction';
|
||||
|
||||
export type LogoutEventType = 'before' | 'after';
|
||||
type AuthEventType = 'before' | 'after';
|
||||
|
||||
@injectable()
|
||||
export class AuthenticationService extends Bootstrap {
|
||||
readonly onLogout: Executor<LogoutEventType>;
|
||||
readonly onLogout: Executor<AuthEventType>;
|
||||
readonly onLogin: Executor<AuthEventType>;
|
||||
|
||||
configureAuthProvider: (() => void) | null;
|
||||
configureIdentityProvider: (() => void) | null;
|
||||
@@ -52,6 +53,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
super();
|
||||
|
||||
this.onLogout = new Executor();
|
||||
this.onLogin = new Executor();
|
||||
|
||||
this.authPromise = null;
|
||||
this.configureAuthProvider = null;
|
||||
@@ -131,13 +133,24 @@ export class AuthenticationService extends Bootstrap {
|
||||
}
|
||||
|
||||
private async auth(persistent: boolean, options: IAuthOptions) {
|
||||
const contexts = await this.onLogin.execute('before');
|
||||
|
||||
if (ExecutorInterrupter.isInterrupted(contexts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.authPromise) {
|
||||
return this.authPromise;
|
||||
await this.authPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
options = observable(options);
|
||||
|
||||
this.authPromise = this.authDialogService.showLoginForm(persistent, options);
|
||||
this.authPromise = this.authDialogService.showLoginForm(persistent, options)
|
||||
.then(async state => {
|
||||
await this.onLogin.execute('after');
|
||||
return state;
|
||||
});
|
||||
|
||||
if (this.serverConfigResource.redirectOnFederatedAuth) {
|
||||
await this.authProvidersResource.loadAll();
|
||||
@@ -158,7 +171,7 @@ export class AuthenticationService extends Bootstrap {
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.authPromise;
|
||||
await this.authPromise;
|
||||
} finally {
|
||||
this.authPromise = null;
|
||||
}
|
||||
|
||||
@@ -13,13 +13,11 @@ import { DATA_CONTEXT_MENU, MenuBaseItem, MenuService } from '@cloudbeaver/core-
|
||||
import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
|
||||
|
||||
import { AuthenticationService } from './AuthenticationService';
|
||||
import { AuthDialogService } from './Dialog/AuthDialogService';
|
||||
|
||||
@injectable()
|
||||
export class PluginBootstrap extends Bootstrap {
|
||||
constructor(
|
||||
private readonly serverConfigResource: ServerConfigResource,
|
||||
private readonly authDialogService: AuthDialogService,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly authInfoService: AuthInfoService,
|
||||
private readonly menuService: MenuService,
|
||||
@@ -40,7 +38,7 @@ export class PluginBootstrap extends Bootstrap {
|
||||
label: 'authentication_login',
|
||||
tooltip: 'authentication_login',
|
||||
},
|
||||
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, linkUser: false }) }
|
||||
{ onSelect: () => this.authenticationService.authUser(null, false) }
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -54,7 +52,7 @@ export class PluginBootstrap extends Bootstrap {
|
||||
label: 'authentication_logout',
|
||||
tooltip: 'authentication_logout',
|
||||
},
|
||||
{ onSelect: this.authenticationService.logout.bind(this.authenticationService) }
|
||||
{ onSelect: () => this.authenticationService.logout() }
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
|
||||
import { Button, PlaceholderComponent, useMapResource } from '@cloudbeaver/core-blocks';
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { Button, PlaceholderComponent } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
|
||||
|
||||
@@ -21,16 +20,12 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
onCancel,
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
const driverMap = useMapResource(
|
||||
ConnectionFormBaseActions,
|
||||
DBDriverResource,
|
||||
state.config.driverId || null
|
||||
);
|
||||
const driver = driverMap.data;
|
||||
const authentication = useAuthenticationAction({
|
||||
origin: state.info?.origin ?? { type: AUTH_PROVIDER_LOCAL_ID, displayName: 'Local' },
|
||||
});
|
||||
|
||||
const authorized = authentication.type === AUTH_PROVIDER_LOCAL_ID || authentication.authorized;
|
||||
|
||||
return (
|
||||
<>
|
||||
{onCancel && (
|
||||
@@ -45,7 +40,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
disabled={state.disabled || !(!driver?.anonymousAccess && authentication.authorized)}
|
||||
disabled={state.disabled || !authorized}
|
||||
mod={['outlined']}
|
||||
loader
|
||||
onClick={state.test}
|
||||
|
||||
@@ -152,6 +152,7 @@ export class ConnectionFormState implements IConnectionFormState {
|
||||
.next(this.formStateTask);
|
||||
|
||||
makeObservable<IConnectionFormState, '_availableDrivers' | 'stateInfo' | 'updateFormState'>(this, {
|
||||
projectId: observable,
|
||||
mode: observable,
|
||||
type: observable,
|
||||
config: observable,
|
||||
@@ -208,12 +209,17 @@ export class ConnectionFormState implements IConnectionFormState {
|
||||
}
|
||||
|
||||
setConfig(projectId: string, config: ConnectionConfig): this {
|
||||
this.projectId = projectId;
|
||||
this.setProject(projectId);
|
||||
this.config = config;
|
||||
this.reset();
|
||||
return this;
|
||||
}
|
||||
|
||||
setProject(projectId: string): this {
|
||||
this.projectId = projectId;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAvailableDrivers(drivers: string[]): this {
|
||||
this._availableDrivers = drivers;
|
||||
this.reset();
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface IConnectionFormState {
|
||||
type: ConnectionFormType
|
||||
) => this;
|
||||
readonly setConfig: (projectId: string, config: ConnectionConfig) => this;
|
||||
readonly setProject: (projectId: string) => this;
|
||||
readonly setAvailableDrivers: (drivers: string[]) => this;
|
||||
readonly save: () => Promise<void>;
|
||||
readonly test: () => Promise<void>;
|
||||
|
||||
+18
-4
@@ -11,6 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx';
|
||||
import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
@@ -25,6 +26,7 @@ import { Options } from './Options';
|
||||
@injectable()
|
||||
export class ConnectionOptionsTabService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly projectsResource: ProjectsResource,
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly dbDriverResource: DBDriverResource,
|
||||
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
|
||||
@@ -120,14 +122,22 @@ export class ConnectionOptionsTabService extends Bootstrap {
|
||||
const validation = contexts.getContext(this.connectionFormService.connectionValidationContext);
|
||||
|
||||
if (!state.config.name?.length) {
|
||||
validation.error("Field 'name' can't be empty");
|
||||
validation.error('plugin_connections_connection_form_name_invalid');
|
||||
}
|
||||
|
||||
if (state.config.driverId && state.config.configurationType) {
|
||||
const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']);
|
||||
|
||||
if (!driver.configurationTypes.includes(state.config.configurationType)) {
|
||||
validation.error('Configuration is not supported');
|
||||
validation.error('plugin_connections_connection_form_host_configuration_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
if (state.projectId !== null && state.mode === 'create') {
|
||||
const project = this.projectsResource.get(state.projectId);
|
||||
|
||||
if (!project?.canCreateConnections) {
|
||||
validation.error('plugin_connections_connection_form_project_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +236,11 @@ export class ConnectionOptionsTabService extends Bootstrap {
|
||||
}
|
||||
|
||||
tempConfig.description = state.config.description;
|
||||
tempConfig.template = state.config.template;
|
||||
|
||||
if (state.projectId === PROJECT_GLOBAL_ID) {
|
||||
tempConfig.template = state.config.template;
|
||||
}
|
||||
|
||||
tempConfig.driverId = state.config.driverId;
|
||||
|
||||
if (!state.config.template && state.config.folder) {
|
||||
@@ -246,7 +260,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
|
||||
tempConfig.databaseName = state.config.databaseName;
|
||||
}
|
||||
|
||||
if (state.config.authModelId || driver.defaultAuthModel) {
|
||||
if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) {
|
||||
tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel;
|
||||
tempConfig.saveCredentials = state.config.saveCredentials;
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ import {
|
||||
import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
|
||||
import { Project, ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
|
||||
import { usePermission } from '@cloudbeaver/core-root';
|
||||
import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import { CachedMapAllKey, CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
|
||||
@@ -116,6 +116,17 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
optionsHook.setAuthModel(model);
|
||||
}, []);
|
||||
|
||||
const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey);
|
||||
const projects = projectsLoader.data as Project[];
|
||||
|
||||
function handleProjectSelect(projectId: string) {
|
||||
const project = projectsLoader.resource.get(projectId);
|
||||
|
||||
if (project?.canCreateConnections) {
|
||||
state.setProject(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
const driverMap = useMapResource(
|
||||
Options,
|
||||
DBDriverResource,
|
||||
@@ -293,6 +304,24 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
<Combobox
|
||||
name='projectId'
|
||||
value={state.projectId ?? PROJECT_GLOBAL_ID}
|
||||
items={projects}
|
||||
keySelector={project => project.id}
|
||||
valueSelector={project => project.name}
|
||||
titleSelector={project => project.description}
|
||||
isDisabled={project => !project.canCreateConnections}
|
||||
searchable={projects.length > 10}
|
||||
readOnly={readonly || edit || projects.length < 2}
|
||||
disabled={disabled}
|
||||
loading={projectsLoader.isLoading()}
|
||||
tiny
|
||||
fill
|
||||
onSelect={handleProjectSelect}
|
||||
>
|
||||
{translate('connections_connection_project')}
|
||||
</Combobox>
|
||||
{!config.template && (
|
||||
<InputField
|
||||
type="text"
|
||||
@@ -301,6 +330,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
disabled={disabled}
|
||||
autoComplete={`section-${config.driverId || 'driver'} section-folder`}
|
||||
mod='surface'
|
||||
autoHide
|
||||
readOnly
|
||||
tiny
|
||||
fill
|
||||
|
||||
@@ -155,22 +155,30 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
|
||||
}: INodeMoveData,
|
||||
contexts: IExecutionContextProvider<INodeMoveData>
|
||||
) {
|
||||
if (![NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.projectsResource.load(CachedMapAllKey);
|
||||
|
||||
const move = contexts.getContext(navNodeMoveContext);
|
||||
const nodes = getNodesFromContext(moveContexts);
|
||||
const nodeIdList = nodes.map(node => node.id);
|
||||
const children = this.navTreeResource.get(targetNode.id);
|
||||
const children = this.navTreeResource.get(targetNode.id) ?? [];
|
||||
const targetProject = this.projectsNavNodeService.getProject(targetNode.id);
|
||||
|
||||
const supported = (
|
||||
[NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)
|
||||
&& nodes.every(node => (
|
||||
node.nodeType === NAV_NODE_TYPE_CONNECTION
|
||||
&& targetProject === this.projectsNavNodeService.getProject(node.id)
|
||||
&& !children?.includes(node.id)
|
||||
))
|
||||
);
|
||||
const supported = nodes.every(node => {
|
||||
if (
|
||||
![NAV_NODE_TYPE_CONNECTION, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_PROJECT].includes(node.nodeType!)
|
||||
|| targetProject !== this.projectsNavNodeService.getProject(node.id)
|
||||
|| children.includes(node.id)
|
||||
|| targetNode.id === node.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!supported) {
|
||||
return;
|
||||
@@ -181,6 +189,31 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
|
||||
move.setCanMove(true);
|
||||
}
|
||||
} else {
|
||||
const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children));
|
||||
const folderDuplicates = nodes.filter(node => (
|
||||
node.nodeType === NAV_NODE_TYPE_FOLDER
|
||||
&& (
|
||||
childrenNode.some(child => child?.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name)
|
||||
|| nodes.some(child => (
|
||||
child.nodeType === NAV_NODE_TYPE_FOLDER
|
||||
&& child.name === node.name
|
||||
&& child.id !== node.id
|
||||
))
|
||||
)
|
||||
));
|
||||
|
||||
if (folderDuplicates.length > 0) {
|
||||
this.notificationService.logError({
|
||||
title: 'connections_public_connection_folder_move_failed',
|
||||
message: this.localizationService.translate(
|
||||
'connections_public_connection_folder_move_duplication',
|
||||
undefined,
|
||||
{ name: folderDuplicates.map(node => `"${node.name}"`).join(', ') }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id);
|
||||
const connections = nodeIdList
|
||||
|
||||
+35
-12
@@ -13,9 +13,9 @@ 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 { SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import type { ConnectionConfig, ResourceKey } from '@cloudbeaver/core-sdk';
|
||||
import { OptionsPanelService } from '@cloudbeaver/core-ui';
|
||||
import { AuthenticationService } from '@cloudbeaver/plugin-authentication';
|
||||
|
||||
import { ConnectionAuthService } from '../ConnectionAuthService';
|
||||
import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService';
|
||||
@@ -36,14 +36,29 @@ export class PublicConnectionFormService {
|
||||
private readonly connectionFormService: ConnectionFormService,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly connectionAuthService: ConnectionAuthService,
|
||||
private readonly sessionDataResource: SessionDataResource
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
) {
|
||||
this.formState = null;
|
||||
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
|
||||
this.connectionInfoResource.onDataUpdate.addPostHandler(this.closeRemoved);
|
||||
this.connectionInfoResource.onItemDelete.addPostHandler(this.closeDeleted);
|
||||
this.sessionDataResource.onDataOutdated.addHandler(() => {
|
||||
this.close(true);
|
||||
|
||||
this.authenticationService.onLogin.addHandler(async (event, context) => {
|
||||
if (event === 'before') {
|
||||
const confirmed = await this.showUnsavedChangesDialog();
|
||||
if (!confirmed) {
|
||||
ExecutorInterrupter.interrupt(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.authenticationService.onLogout.addHandler(async (event, context) => {
|
||||
if (event === 'before') {
|
||||
const confirmed = await this.close(false);
|
||||
if (!confirmed) {
|
||||
ExecutorInterrupter.interrupt(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
makeObservable(this, {
|
||||
@@ -87,9 +102,9 @@ export class PublicConnectionFormService {
|
||||
return state;
|
||||
}
|
||||
|
||||
async close(saved?: boolean): Promise<void> {
|
||||
async close(saved?: boolean): Promise<boolean> {
|
||||
if (!this.formState) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (saved) {
|
||||
@@ -101,6 +116,8 @@ export class PublicConnectionFormService {
|
||||
if (state) {
|
||||
this.clearFormState();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
@@ -147,6 +164,14 @@ export class PublicConnectionFormService {
|
||||
};
|
||||
|
||||
private readonly closeHandler: IExecutorHandler<any> = async (data, contexts) => {
|
||||
const confirmed = await this.showUnsavedChangesDialog();
|
||||
|
||||
if (!confirmed) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
};
|
||||
|
||||
private async showUnsavedChangesDialog(): Promise<boolean> {
|
||||
if (
|
||||
!this.formState
|
||||
|| !this.optionsPanelService.isOpen(formGetter)
|
||||
@@ -159,13 +184,13 @@ export class PublicConnectionFormService {
|
||||
))
|
||||
)
|
||||
) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const state = await this.formState.checkFormState();
|
||||
|
||||
if (!state?.edited) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
@@ -174,10 +199,8 @@ export class PublicConnectionFormService {
|
||||
confirmActionText: 'ui_processing_ok',
|
||||
});
|
||||
|
||||
if (result === DialogueStateResult.Rejected) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
};
|
||||
return result !== DialogueStateResult.Rejected;
|
||||
}
|
||||
|
||||
private async tryReconnect(connectionKey: IConnectionInfoParams) {
|
||||
const result = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
|
||||
@@ -6,5 +6,9 @@ export default [
|
||||
['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'],
|
||||
['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'],
|
||||
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
|
||||
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
|
||||
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
|
||||
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
|
||||
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
|
||||
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
|
||||
];
|
||||
|
||||
@@ -6,5 +6,9 @@ export default [
|
||||
['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'],
|
||||
['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'],
|
||||
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
|
||||
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
|
||||
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
|
||||
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
|
||||
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
|
||||
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
|
||||
];
|
||||
|
||||
@@ -6,5 +6,9 @@ export default [
|
||||
['connections_public_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'],
|
||||
['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'],
|
||||
['connections_public_connection_folder_move_failed', 'Ошибка перемещения в папку'],
|
||||
['connections_public_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({args.name})'],
|
||||
['plugin_connections_connection_form_project_invalid', 'У вас нет разрешения создавать коннекшены в выбранном проекте'],
|
||||
['plugin_connections_connection_form_host_configuration_invalid', 'Конфигурация хоста не поддерживается'],
|
||||
['plugin_connections_connection_form_name_invalid', 'Поле \'Название\' не может быть пустым'],
|
||||
['connections_public_connection_folder_delete_confirmation', 'Вы удаляете "{args.name}". Подключения не будут удалены. Вы уверены?'],
|
||||
];
|
||||
|
||||
@@ -6,5 +6,9 @@ export default [
|
||||
['connections_public_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'],
|
||||
['connections_public_connection_edit_reconnect_failed', '重新连接失败'],
|
||||
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
|
||||
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
|
||||
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
|
||||
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
|
||||
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
|
||||
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Combobox } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { CrsKey } from './LeafletMap';
|
||||
|
||||
const styles = css`
|
||||
root {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 4px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
Combobox {
|
||||
width: 120px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
value: CrsKey;
|
||||
onChange: (value: CrsKey) => void;
|
||||
}
|
||||
|
||||
const items: CrsKey[] = [
|
||||
'Simple',
|
||||
'EPSG3395',
|
||||
'EPSG3857',
|
||||
'EPSG4326',
|
||||
'EPSG900913',
|
||||
];
|
||||
|
||||
export function CrsInput(props: Props) {
|
||||
return styled(styles)(
|
||||
<root>
|
||||
<label>CRS:</label>
|
||||
<Combobox
|
||||
items={items}
|
||||
value={props.value}
|
||||
onSelect={props.onChange}
|
||||
/>
|
||||
</root>
|
||||
);
|
||||
}
|
||||
@@ -5,18 +5,51 @@
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
import wellknown from 'wellknown';
|
||||
|
||||
import { TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { IDatabaseResultSet, ResultSetSelectAction, IResultSetElementKey, IDatabaseDataModel, ResultSetViewAction, ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import { IGeoJSONFeature, IAssociatedValue, LeafletMap } from './LeafletMap';
|
||||
import { CrsInput } from './CrsInput';
|
||||
import { IGeoJSONFeature, IAssociatedValue, LeafletMap, CrsKey } from './LeafletMap';
|
||||
import { ResultSetGISAction } from './ResultSetGISAction';
|
||||
|
||||
function getCrsKey(feature?: IGeoJSONFeature): CrsKey {
|
||||
switch (feature?.properties.srid) {
|
||||
case 3857:
|
||||
return 'EPSG3857';
|
||||
case 4326:
|
||||
return 'EPSG4326';
|
||||
case 3395:
|
||||
return 'EPSG3395';
|
||||
case 900913:
|
||||
return 'EPSG900913';
|
||||
default:
|
||||
return 'EPSG3857';
|
||||
}
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
map {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
toolbar {
|
||||
margin-top: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
model: IDatabaseDataModel<any, IDatabaseResultSet>;
|
||||
resultIndex: number;
|
||||
@@ -87,11 +120,25 @@ export const GISValuePresentation = observer<Props>(function GISValuePresentatio
|
||||
return values;
|
||||
}, [view]);
|
||||
|
||||
|
||||
const defaultCrsKey = getCrsKey(parsedGISData[0]);
|
||||
const [crsKey, setCrsKey] = useState(defaultCrsKey);
|
||||
|
||||
if (!parsedGISData.length) {
|
||||
return <TextPlaceholder>{translate('gis_presentation_placeholder')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return (
|
||||
<LeafletMap geoJSON={parsedGISData} getAssociatedValues={getAssociatedValues} />
|
||||
return styled(styles)(
|
||||
<root>
|
||||
<map>
|
||||
<LeafletMap key={crsKey} geoJSON={parsedGISData} crsKey={crsKey} getAssociatedValues={getAssociatedValues} />
|
||||
</map>
|
||||
<toolbar>
|
||||
<CrsInput
|
||||
value={crsKey}
|
||||
onChange={setCrsKey}
|
||||
/>
|
||||
</toolbar>
|
||||
</root>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,8 +42,11 @@ interface IBaseTile extends TileLayerProps {
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export type CrsKey = 'Simple' | 'EPSG3857' | 'EPSG4326' | 'EPSG3395' | 'EPSG900913';
|
||||
|
||||
interface Props {
|
||||
geoJSON: IGeoJSONFeature[];
|
||||
crsKey: CrsKey;
|
||||
getAssociatedValues: (cell: IResultSetElementKey) => IAssociatedValue[];
|
||||
}
|
||||
|
||||
@@ -87,17 +90,17 @@ function polyStyle() {
|
||||
};
|
||||
}
|
||||
|
||||
function getCRS(feature?: IGeoJSONFeature): leaflet.CRS {
|
||||
switch (feature?.properties.srid) {
|
||||
case 0:
|
||||
function getCRS(crsKey: CrsKey): leaflet.CRS {
|
||||
switch (crsKey) {
|
||||
case 'Simple':
|
||||
return leaflet.CRS.Simple;
|
||||
case 3857:
|
||||
case 'EPSG3857':
|
||||
return leaflet.CRS.EPSG3857;
|
||||
case 4326:
|
||||
case 'EPSG4326':
|
||||
return leaflet.CRS.EPSG4326;
|
||||
case 3395:
|
||||
case 'EPSG3395':
|
||||
return leaflet.CRS.EPSG3395;
|
||||
case 900913:
|
||||
case 'EPSG900913':
|
||||
return leaflet.CRS.EPSG900913;
|
||||
default:
|
||||
return leaflet.CRS.EPSG3857;
|
||||
@@ -111,14 +114,14 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeafletMap: React.FC<Props> = function LeafletMap({ geoJSON, getAssociatedValues }) {
|
||||
export const LeafletMap: React.FC<Props> = function LeafletMap({ geoJSON, crsKey, getAssociatedValues }) {
|
||||
const splitContext = useSplit();
|
||||
const translate = useTranslate();
|
||||
|
||||
const [mapRef, setMapRef] = useState<leaflet.Map | null>(null);
|
||||
const [geoJSONLayerRef, setGeoJSONLayerRef] = useState<leaflet.GeoJSON | null>(null);
|
||||
|
||||
const crs = getCRS(geoJSON[0]);
|
||||
const crs = getCRS(crsKey);
|
||||
|
||||
const onEachFeature = useCallback((feature: IGeoJSONFeature, layer: leaflet.Layer) => {
|
||||
const associatedValues = getAssociatedValues(feature.properties.associatedCell);
|
||||
|
||||
+4
-1
@@ -409,8 +409,11 @@ export function useElementsTree(options: IOptions): IElementsTree {
|
||||
this.select(node, false, false);
|
||||
this.actions.execute({ type: 'show', nodeId });
|
||||
});
|
||||
}
|
||||
|
||||
if (path.length > 0) {
|
||||
await functionsRef.loadTree(path[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
async expand(node: NavNode, state: boolean) {
|
||||
const treeNodeState = this.state.get(node.id);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed, makeObservable } from 'mobx';
|
||||
import { computed, makeObservable, untracked } from 'mobx';
|
||||
|
||||
import {
|
||||
ConnectionExecutionContextResource,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { NavNodeManagerService, objectNavNodeProvider, NodeManagerUtils } from '@cloudbeaver/core-navigation-tree';
|
||||
import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
|
||||
import { CachedMapAllKey, NavNodeInfoFragment, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { NavigationTabsService, TabHandler, ITab, ITabOptions } from '@cloudbeaver/plugin-navigation-tabs';
|
||||
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor';
|
||||
@@ -55,7 +55,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
private readonly connectionExecutionContextService: ConnectionExecutionContextService,
|
||||
private readonly connectionExecutionContextResource: ConnectionExecutionContextResource,
|
||||
private readonly connectionInfoResource: ConnectionInfoResource,
|
||||
private readonly navNodeManagerService: NavNodeManagerService,
|
||||
private readonly navNodeInfoResource: NavNodeInfoResource,
|
||||
private readonly sqlDataSourceService: SqlDataSourceService,
|
||||
private readonly connectionsManagerService: ConnectionsManagerService,
|
||||
private readonly containerResource: ContainerResource
|
||||
@@ -190,12 +190,9 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = NodeManagerUtils.parentsFromPath(nodeId);
|
||||
const parent = this.navNodeManagerService.getNode(parents[0]);
|
||||
const parents = this.navNodeInfoResource.getParents(nodeId);
|
||||
|
||||
if (parent) {
|
||||
parents.unshift(parent.parentId);
|
||||
}
|
||||
untracked(() => this.navNodeInfoResource.load(nodeId!));
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cloudbeaver/product-default",
|
||||
"sideEffects": false,
|
||||
"version": "22.1.5",
|
||||
"version": "22.2.0",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
|
||||
Reference in New Issue
Block a user