mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge branch 'devel' into feat/cloud-explorer-filter
This commit is contained in:
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.connect.provider.sample;singleton:=true
|
||||
Bundle-Version: 1.0.23.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.24.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.connect.provider.sample</artifactId>
|
||||
<version>1.0.23-SNAPSHOT</version>
|
||||
<version>1.0.24-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
|
||||
Bundle-Version: 20.0.4.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 20.0.5.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.product.ce</artifactId>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Base JDBC drivers
|
||||
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
|
||||
Bundle-Version: 1.0.20.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.21.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
|
||||
<version>1.0.20-SNAPSHOT</version>
|
||||
<version>1.0.21-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
|
||||
Bundle-Version: 20.0.4.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 20.0.5.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-Activator: io.cloudbeaver.server.WebPlatformActivator
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server</artifactId>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+5
-2
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package io.cloudbeaver;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebUser;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -49,7 +50,8 @@ public interface DBWAuthProviderExternal<AUTH_SESSION extends DBASession> extend
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull DBWSecurityController securityController,
|
||||
@NotNull Map<String, Object> providerConfig,
|
||||
@NotNull Map<String, Object> credentials) throws DBException;
|
||||
@NotNull Map<String, Object> credentials,
|
||||
@Nullable WebUser activeUser) throws DBException;
|
||||
|
||||
@Nullable
|
||||
String getUserDisplayName(
|
||||
@@ -60,7 +62,8 @@ public interface DBWAuthProviderExternal<AUTH_SESSION extends DBASession> extend
|
||||
@Nullable
|
||||
DBPObject getUserDetails(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
AUTH_SESSION session,
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull AUTH_SESSION session,
|
||||
@NotNull WebUser user) throws DBException;
|
||||
|
||||
}
|
||||
|
||||
+78
-27
@@ -97,7 +97,8 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap
|
||||
private final Map<String, WebAsyncTaskInfo> asyncTasks = new HashMap<>();
|
||||
private final Map<String, Object> attributes = new HashMap<>();
|
||||
private final Map<String, Function<Object,Object>> attributeDisposers = new HashMap<>();
|
||||
private WebAuthInfo authInfo;
|
||||
// Map of auth tokens. Key is authentication provdier
|
||||
private final List<WebAuthInfo> authTokens = new ArrayList<>();
|
||||
|
||||
private DBNModel navigatorModel;
|
||||
private DBRProgressMonitor progressMonitor = new SessionProgressMonitor();
|
||||
@@ -477,10 +478,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
}
|
||||
if (this.authInfo != null) {
|
||||
this.authInfo.closeAuth();
|
||||
this.authInfo = null;
|
||||
}
|
||||
clearAuthTokens();
|
||||
this.user = null;
|
||||
|
||||
if (this.sessionProject != null) {
|
||||
@@ -489,6 +487,16 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAuthTokens() {
|
||||
ArrayList<WebAuthInfo> tokensCopy;
|
||||
synchronized (authTokens) {
|
||||
tokensCopy = new ArrayList<>(this.authTokens);
|
||||
}
|
||||
for (WebAuthInfo ai : tokensCopy) {
|
||||
removeAuthInfo(ai);
|
||||
}
|
||||
}
|
||||
|
||||
public DBRProgressMonitor getProgressMonitor() {
|
||||
return progressMonitor;
|
||||
}
|
||||
@@ -622,30 +630,69 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap
|
||||
}
|
||||
}
|
||||
|
||||
public WebAuthInfo getAuthInfo() {
|
||||
return authInfo;
|
||||
public WebAuthInfo getAuthInfo(@Nullable String providerID) {
|
||||
synchronized (authTokens) {
|
||||
|
||||
if (providerID != null) {
|
||||
for (WebAuthInfo ai : authTokens) {
|
||||
if (ai.getAuthProvider().getId().equals(providerID)) {
|
||||
return ai;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return authTokens.isEmpty() ? null : authTokens.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthInfo(@Nullable WebAuthInfo authInfo) {
|
||||
WebUser newUser = authInfo == null ? null : authInfo.getUser();
|
||||
if (CommonUtils.equalObjects(this.user, newUser)) {
|
||||
return;
|
||||
public List<WebAuthInfo> getAllAuthInfo() {
|
||||
synchronized (authTokens) {
|
||||
return new ArrayList<>(authTokens);
|
||||
}
|
||||
forceUserRefresh(newUser);
|
||||
}
|
||||
|
||||
if (this.authInfo != null) {
|
||||
DBASession oldAuthSession = this.authInfo.getAuthSession();
|
||||
if (oldAuthSession != null) {
|
||||
sessionProject.getSessionContext().removeSession(oldAuthSession);
|
||||
}
|
||||
this.authInfo.closeAuth();
|
||||
public void addAuthInfo(@NotNull WebAuthInfo authInfo) throws DBException {
|
||||
WebUser newUser = authInfo.getUser();
|
||||
if (this.user == null && newUser != null) {
|
||||
forceUserRefresh(newUser);
|
||||
} else if (!CommonUtils.equalObjects(this.user, newUser)) {
|
||||
throw new DBException("Can't authorize different users in the single session");
|
||||
}
|
||||
this.authInfo = authInfo;
|
||||
if (authInfo != null) {
|
||||
DBASession authSession = authInfo.getAuthSession();
|
||||
if (authSession != null) {
|
||||
this.sessionProject.getSessionContext().addSession(authSession);
|
||||
}
|
||||
|
||||
|
||||
WebAuthInfo oldAuthInfo = getAuthInfo(authInfo.getAuthProvider().getId());
|
||||
if (oldAuthInfo != null) {
|
||||
removeAuthInfo(oldAuthInfo);
|
||||
}
|
||||
|
||||
synchronized (authTokens) {
|
||||
authTokens.add(authInfo);
|
||||
}
|
||||
DBASession authSession = authInfo.getAuthSession();
|
||||
if (authSession != null) {
|
||||
this.sessionProject.getSessionContext().addSession(authSession);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeAuthInfo(WebAuthInfo oldAuthInfo) {
|
||||
DBASession oldAuthSession = oldAuthInfo.getAuthSession();
|
||||
if (oldAuthSession != null && sessionProject != null) {
|
||||
sessionProject.getSessionContext().removeSession(oldAuthSession);
|
||||
}
|
||||
oldAuthInfo.closeAuth();
|
||||
synchronized (authTokens) {
|
||||
authTokens.remove(oldAuthInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAuthInfo(String providerId) {
|
||||
if (providerId == null) {
|
||||
clearAuthTokens();
|
||||
} else {
|
||||
removeAuthInfo(getAuthInfo(providerId));
|
||||
}
|
||||
if (authTokens.isEmpty()) {
|
||||
forceUserRefresh(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,9 +752,13 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap
|
||||
// May be called to extract auth information from session
|
||||
@Override
|
||||
public <T> T getAdapter(Class<T> adapter) {
|
||||
if (authInfo != null && authInfo.getAuthSession() != null) {
|
||||
if (adapter.isInstance(authInfo.getAuthSession())) {
|
||||
return adapter.cast(authInfo.getAuthSession());
|
||||
synchronized (authTokens) {
|
||||
for (WebAuthInfo authInfo : authTokens) {
|
||||
if (authInfo != null && authInfo.getAuthSession() != null) {
|
||||
if (adapter.isInstance(authInfo.getAuthSession())) {
|
||||
return adapter.cast(authInfo.getAuthSession());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
+9
-2
@@ -23,10 +23,12 @@ import io.cloudbeaver.WebServiceUtils;
|
||||
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
|
||||
import io.cloudbeaver.model.WebObjectOrigin;
|
||||
import io.cloudbeaver.model.WebPropertyInfo;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
|
||||
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.DBPObject;
|
||||
import org.jkiss.dbeaver.model.access.DBASession;
|
||||
@@ -91,13 +93,18 @@ public class WebUserOriginInfo implements WebObjectOrigin {
|
||||
return new WebPropertyInfo[0];
|
||||
}
|
||||
try {
|
||||
DBASession authSession = session.getAuthInfo().getAuthSession();
|
||||
WebAuthInfo authInfo = session.getAuthInfo(
|
||||
LocalAuthProvider.PROVIDER_ID.equals(authProvider.getId()) ? null : authProvider.getId());
|
||||
if (authInfo == null) {
|
||||
throw new DBException("Session not authorized in auth provider '" + authProvider.getId() + "'");
|
||||
}
|
||||
DBASession authSession = authInfo.getAuthSession();
|
||||
DBWAuthProvider<?> authProvider = this.authProvider.getInstance();
|
||||
if (authSession != null && authProvider instanceof DBWAuthProviderExternal) {
|
||||
if (!isValidSessionType(authSession, authProvider)) {
|
||||
return new WebPropertyInfo[0];
|
||||
}
|
||||
DBPObject userDetails = ((DBWAuthProviderExternal) authProvider).getUserDetails(session.getProgressMonitor(), authSession, user);
|
||||
DBPObject userDetails = ((DBWAuthProviderExternal) authProvider).getUserDetails(session.getProgressMonitor(), session, authSession, user);
|
||||
if (userDetails != null) {
|
||||
return WebServiceUtils.getObjectProperties(session, userDetails);
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public class CBApplication extends BaseApplicationImpl {
|
||||
log.debug("\tContent root: " + new File(contentRoot).getAbsolutePath());
|
||||
log.debug("\tDrivers storage: " + new File(driversLocation).getAbsolutePath());
|
||||
//log.debug("\tDrivers root: " + driversLocation);
|
||||
log.debug("\tProduct details: " + application.getInfoDetails());
|
||||
//log.debug("\tProduct details: " + application.getInfoDetails());
|
||||
log.debug("\tBase port: " + serverPort);
|
||||
log.debug("\tBase URI: " + servicesURI);
|
||||
if (develMode) {
|
||||
|
||||
+8
-1
@@ -25,6 +25,7 @@ import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.server.graphql.GraphQLEndpoint;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.utils.ArrayUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
@@ -107,8 +108,14 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
|
||||
getServletRequest(env));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static WebConnectionInfo getWebConnection(DataFetchingEnvironment env) throws DBWebException {
|
||||
return getWebSession(env).getWebConnectionInfo(env.getArgument("connectionId"));
|
||||
return getWebConnection(getWebSession(env), env.getArgument("connectionId"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebConnectionInfo getWebConnection(WebSession session, String connectionId) throws DBWebException {
|
||||
return session.getWebConnectionInfo(connectionId);
|
||||
}
|
||||
|
||||
private class ServiceInvocationHandler implements InvocationHandler {
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.cloudbeaver.service;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class WebServiceServletBase extends HttpServlet {
|
||||
|
||||
private static final Log log = Log.getLog(WebServiceServletBase.class);
|
||||
|
||||
private final CBApplication application;
|
||||
|
||||
public WebServiceServletBase(CBApplication application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public CBApplication getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
WebSession webSession = CBPlatform.getInstance().getSessionManager().findWebSession(request);
|
||||
if (webSession == null) {
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Web session not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
processServiceRequest(webSession, request, response);
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException;
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.WebServiceServletBase;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class WebSQLResultServlet extends WebServiceServletBase {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLResultServlet.class);
|
||||
|
||||
// context-id/result-id/row-number/attribute-name
|
||||
private static final Pattern URL_PATTERN = Pattern.compile("/?([\\w]+)/([0-9]+)/([0-9]+)/([0-9]+)/(.+)[/\\?]?");
|
||||
|
||||
private final DBWServiceSQL sqlService;
|
||||
|
||||
public WebSQLResultServlet(CBApplication application, DBWServiceSQL sqlService) {
|
||||
super(application);
|
||||
this.sqlService = sqlService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
|
||||
|
||||
String valuePath = request.getPathInfo();
|
||||
if (CommonUtils.isEmpty(valuePath)) {
|
||||
throw new DBWebException("Result value ID not specified");
|
||||
}
|
||||
Matcher urlMatcher = URL_PATTERN.matcher(valuePath);
|
||||
if (!urlMatcher.matches()) {
|
||||
throw new DBWebException("Invalid URI format");
|
||||
}
|
||||
|
||||
String connectionId = urlMatcher.group(1);
|
||||
String contextId = urlMatcher.group(2);
|
||||
String resultsId = urlMatcher.group(3);
|
||||
String rowNum = urlMatcher.group(4);
|
||||
String attrName = urlMatcher.group(5);
|
||||
|
||||
WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(
|
||||
WebServiceBindingSQL.getWebConnection(session, connectionId));
|
||||
WebSQLContextInfo sqlContext = WebServiceBindingSQL.getSQLContext(sqlProcessor, contextId);
|
||||
WebSQLResultsInfo sqlResults = sqlContext.getResults(resultsId);
|
||||
|
||||
|
||||
throw new DBWebException("Not implemented yet");
|
||||
/*
|
||||
response.setHeader("Content-Type", processor.getContentType());
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||
response.setHeader("Content-Length", String.valueOf(dataFile.length()));
|
||||
|
||||
try (InputStream is = new FileInputStream(dataFile)) {
|
||||
IOUtils.copyStream(is, response.getOutputStream());
|
||||
}
|
||||
|
||||
// TODO: cleanup export files ASAP?
|
||||
if (false) {
|
||||
dtConfig.removeTask(taskInfo);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
+47
-18
@@ -18,6 +18,7 @@ package io.cloudbeaver.service.sql;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.*;
|
||||
import org.jkiss.dbeaver.model.exec.DBCException;
|
||||
@@ -25,14 +26,12 @@ import org.jkiss.dbeaver.model.gis.DBGeometry;
|
||||
import org.jkiss.dbeaver.model.struct.DBSAttributeBase;
|
||||
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
|
||||
import org.jkiss.dbeaver.utils.ContentUtils;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Web SQL utils.
|
||||
@@ -41,6 +40,8 @@ public class WebSQLUtils {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLUtils.class);
|
||||
|
||||
public static final int BINARY_PREVIEW_LENGTH = 255;
|
||||
|
||||
public static Object makeWebCellValue(WebSession session, DBSTypedObject type, Object cellValue, WebDataFormat dataFormat) throws DBCException {
|
||||
if (cellValue instanceof Date) {
|
||||
return CBConstants.ISO_DATE_FORMAT.format(cellValue);
|
||||
@@ -51,11 +52,7 @@ public class WebSQLUtils {
|
||||
return null;
|
||||
}
|
||||
else if (dbValue instanceof DBDDocument) {
|
||||
if (dataFormat != WebDataFormat.document) {
|
||||
return serializeDocumentValue((DBDDocument) dbValue);
|
||||
} else {
|
||||
return serializeDocumentValue(session, (DBDDocument) dbValue);
|
||||
}
|
||||
return serializeDocumentValue(session, (DBDDocument) dbValue);
|
||||
} else if (dbValue instanceof DBDComplexValue) {
|
||||
return serializeComplexValue(session, (DBDComplexValue)dbValue, dataFormat);
|
||||
} else if (dbValue instanceof DBGeometry) {
|
||||
@@ -75,18 +72,31 @@ public class WebSQLUtils {
|
||||
for (int i = 0; i < size; i++) {
|
||||
items[i] = makeWebCellValue(session, collection.getComponentType(), collection.getItem(i), dataFormat);
|
||||
}
|
||||
return items;
|
||||
|
||||
Map<String, Object> map = createMapOfType("collection");
|
||||
map.put("value", items);
|
||||
return map;
|
||||
} else if (value instanceof DBDComposite) {
|
||||
DBDComposite composite = (DBDComposite)value;
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
Map<String, Object> struct = new LinkedHashMap<>();
|
||||
for (DBSAttributeBase attr : composite.getAttributes()) {
|
||||
map.put(attr.getName(), makeWebCellValue(session, attr, composite.getAttributeValue(attr), dataFormat));
|
||||
struct.put(attr.getName(), makeWebCellValue(session, attr, composite.getAttributeValue(attr), dataFormat));
|
||||
}
|
||||
|
||||
Map<String, Object> map = createMapOfType("map");
|
||||
map.put("value", struct);
|
||||
return map;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Object> createMapOfType(String type) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("$type", type);
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<String, Object> serializeDocumentValue(WebSession session, DBDDocument document) throws DBCException {
|
||||
String documentData;
|
||||
try {
|
||||
@@ -97,7 +107,7 @@ public class WebSQLUtils {
|
||||
throw new DBCException("Error serializing document", e);
|
||||
}
|
||||
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
Map<String, Object> map = createMapOfType("document");
|
||||
map.put("id", CommonUtils.toString(document.getDocumentId()));
|
||||
map.put("contentType", document.getDocumentContentType());
|
||||
map.put("properties", Collections.emptyMap());
|
||||
@@ -106,15 +116,34 @@ public class WebSQLUtils {
|
||||
}
|
||||
|
||||
private static Object serializeContentValue(WebSession session, DBDContent value) throws DBCException {
|
||||
return ContentUtils.getContentStringValue(session.getProgressMonitor(), value);
|
||||
}
|
||||
|
||||
private static Object serializeDocumentValue(DBDDocument value) {
|
||||
return value;
|
||||
Map<String, Object> map = createMapOfType("content");
|
||||
if (ContentUtils.isTextContent(value)) {
|
||||
String stringValue = ContentUtils.getContentStringValue(session.getProgressMonitor(), value);
|
||||
map.put("text", stringValue);
|
||||
} else {
|
||||
map.put("binary", true);
|
||||
byte[] binaryValue = ContentUtils.getContentBinaryValue(session.getProgressMonitor(), value);
|
||||
if (binaryValue != null) {
|
||||
if (binaryValue.length > BINARY_PREVIEW_LENGTH) {
|
||||
binaryValue = Arrays.copyOf(binaryValue, BINARY_PREVIEW_LENGTH);
|
||||
}
|
||||
map.put("text", GeneralUtils.convertToString(binaryValue, 0, binaryValue.length));
|
||||
} else {
|
||||
map.put("text", null);
|
||||
}
|
||||
}
|
||||
map.put("contentType", value.getContentType());
|
||||
map.put("contentLength", value.getContentLength());
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Object serializeGeometryValue(DBGeometry value) {
|
||||
return value.toString();
|
||||
Map<String, Object> map = createMapOfType("geometry");
|
||||
map.put("srid", value.getSRID());
|
||||
map.put("text", value.toString());
|
||||
map.put("properties", value.getProperties());
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+27
-1
@@ -20,9 +20,14 @@ import graphql.schema.DataFetchingEnvironment;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.WebConnectionInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.DBWBindingContext;
|
||||
import io.cloudbeaver.service.DBWServiceBindingServlet;
|
||||
import io.cloudbeaver.service.WebServiceBindingBase;
|
||||
import io.cloudbeaver.service.sql.impl.WebServiceSQL;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
@@ -34,7 +39,7 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* Web service implementation
|
||||
*/
|
||||
public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> {
|
||||
public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> implements DBWServiceBindingServlet {
|
||||
|
||||
public WebServiceBindingSQL() {
|
||||
super(DBWServiceSQL.class, new WebServiceSQL(), "schema/service.sql.graphqls");
|
||||
@@ -109,23 +114,37 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> {
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private WebDataFormat getDataFormat(DataFetchingEnvironment env) {
|
||||
String dataFormat = env.getArgument("dataFormat");
|
||||
return CommonUtils.valueOf(WebDataFormat.class, dataFormat, WebDataFormat.resultset);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLConfiguration getSQLConfiguration(WebSession webSession) {
|
||||
return webSession.getAttribute("sqlConfiguration", cfg -> new WebSQLConfiguration(), WebSQLConfiguration::dispose);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLProcessor getSQLProcessor(DataFetchingEnvironment env) throws DBWebException {
|
||||
WebConnectionInfo connectionInfo = getWebConnection(env);
|
||||
return getSQLProcessor(connectionInfo);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLProcessor getSQLProcessor(WebConnectionInfo connectionInfo) throws DBWebException {
|
||||
return getSQLConfiguration(connectionInfo.getSession()).getSQLProcessor(connectionInfo);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLContextInfo getSQLContext(DataFetchingEnvironment env) throws DBWebException {
|
||||
WebSQLProcessor processor = getSQLProcessor(env);
|
||||
String contextId = env.getArgument("contextId");
|
||||
return getSQLContext(processor, contextId);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLContextInfo getSQLContext(WebSQLProcessor processor, String contextId) throws DBWebException {
|
||||
WebSQLContextInfo context = processor.getContext(contextId);
|
||||
if (context == null) {
|
||||
throw new DBWebException("SQL context '" + contextId + "' not found");
|
||||
@@ -133,6 +152,13 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> {
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addServlets(CBApplication application, ServletContextHandler servletContextHandler) {
|
||||
servletContextHandler.addServlet(
|
||||
new ServletHolder("sqlResultValueViewer", new WebSQLResultServlet(application, getServiceImpl())),
|
||||
application.getServicesURI() + "sql-result-value/*");
|
||||
}
|
||||
|
||||
private static class WebSQLConfiguration {
|
||||
private final Map<WebConnectionInfo, WebSQLProcessor> processors = new HashMap<>();
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
|
||||
Bundle-Version: 1.0.19.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.20.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.admin</artifactId>
|
||||
<version>1.0.19-SNAPSHOT</version>
|
||||
<version>1.0.20-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+30
-43
@@ -4,15 +4,14 @@ import io.cloudbeaver.DBWConstants;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.service.WebServiceServletBase;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBConstants;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.dbeaver.utils.MimeTypes;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
@@ -20,55 +19,43 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WebAdminLogsServlet extends HttpServlet {
|
||||
public class WebAdminLogsServlet extends WebServiceServletBase {
|
||||
|
||||
private static final Log log = Log.getLog(WebAdminLogsServlet.class);
|
||||
|
||||
private final CBApplication application;
|
||||
|
||||
public WebAdminLogsServlet(CBApplication application) {
|
||||
this.application = application;
|
||||
super(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
try {
|
||||
WebSession webSession = CBPlatform.getInstance().getSessionManager().findWebSession(request);
|
||||
if (webSession == null) {
|
||||
throw new DBWebException("No active session");
|
||||
}
|
||||
if (!webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Logs accessible for admins only");
|
||||
return;
|
||||
}
|
||||
|
||||
String logId = request.getPathInfo();
|
||||
String logFileName = ".log";
|
||||
if ("debug".equals(logId)) {
|
||||
logFileName = DBConstants.DEBUG_LOG_FILE_NAME;
|
||||
}
|
||||
if (logFileName.contains("/") || logFileName.contains("\\")) {
|
||||
throw new DBWebException("Bad log file name");
|
||||
}
|
||||
File logFile = new File(GeneralUtils.getMetadataFolder(), logFileName);
|
||||
if (!logFile.exists()) {
|
||||
throw new DBWebException("Log file '" + logFileName + "' not found");
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", MimeTypes.TEXT_PLAIN);
|
||||
if (logFileName.equals(".log")) {
|
||||
logFileName = "server.log";
|
||||
}
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + logFileName + "\"");
|
||||
|
||||
try (InputStream is = new FileInputStream(logFile)) {
|
||||
IOUtils.copyStream(is, response.getOutputStream());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error reading log: " + e.getMessage());
|
||||
protected void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
|
||||
if (!session.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Logs accessible for admins only");
|
||||
return;
|
||||
}
|
||||
|
||||
String logId = request.getPathInfo();
|
||||
String logFileName = ".log";
|
||||
if ("debug".equals(logId)) {
|
||||
logFileName = DBConstants.DEBUG_LOG_FILE_NAME;
|
||||
}
|
||||
if (logFileName.contains("/") || logFileName.contains("\\")) {
|
||||
throw new DBWebException("Bad log file name");
|
||||
}
|
||||
File logFile = new File(GeneralUtils.getMetadataFolder(), logFileName);
|
||||
if (!logFile.exists()) {
|
||||
throw new DBWebException("Log file '" + logFileName + "' not found");
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", MimeTypes.TEXT_PLAIN);
|
||||
if (logFileName.equals(".log")) {
|
||||
logFileName = "server.log";
|
||||
}
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + logFileName + "\"");
|
||||
|
||||
try (InputStream is = new FileInputStream(logFile)) {
|
||||
IOUtils.copyStream(is, response.getOutputStream());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true
|
||||
Bundle-Version: 1.0.19.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.20.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.auth</artifactId>
|
||||
<version>1.0.19-SNAPSHOT</version>
|
||||
<version>1.0.20-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -30,29 +30,56 @@ type AuthProviderInfo {
|
||||
credentialParameters: [AuthCredentialInfo!]!
|
||||
}
|
||||
|
||||
type UserAuthInfo {
|
||||
# User unique identifier
|
||||
userId: String!
|
||||
# Human readable display name. May be null
|
||||
displayName: String
|
||||
# Auth provider ID
|
||||
authProvider: String!
|
||||
type UserAuthToken {
|
||||
# Auth provider used for autgorization
|
||||
provider: ID!
|
||||
|
||||
# Authorization time
|
||||
loginTime: DateTime!
|
||||
|
||||
# Optional login message
|
||||
message: String
|
||||
|
||||
# Auth origin
|
||||
origin: ObjectOrigin!
|
||||
}
|
||||
|
||||
type UserAuthInfo {
|
||||
# User unique identifier
|
||||
userId: String! @deprecated
|
||||
# Human readable display name. May be null
|
||||
displayName: String @deprecated
|
||||
# Auth provider ID
|
||||
authProvider: String! @deprecated
|
||||
|
||||
loginTime: DateTime! @deprecated
|
||||
|
||||
# Optional login message
|
||||
message: String @deprecated
|
||||
|
||||
origin: ObjectOrigin! @deprecated
|
||||
}
|
||||
|
||||
type UserInfo {
|
||||
# User unique identifier
|
||||
userId: String!
|
||||
# Human readable display name. May be null
|
||||
displayName: String
|
||||
|
||||
authTokens: [UserAuthToken!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
# Authorize user using specified auth provider
|
||||
|
||||
authLogin(provider: ID!, credentials: Object!): UserAuthInfo!
|
||||
# Logouts user. If provider not specified then all authorizations are revoked from session.
|
||||
authLogout(provider: ID): Boolean
|
||||
|
||||
authLogout: Boolean
|
||||
activeUser: UserInfo
|
||||
|
||||
sessionUser: UserAuthInfo
|
||||
sessionUser: UserAuthInfo @deprecated
|
||||
|
||||
authProviders: [AuthProviderInfo!]!
|
||||
|
||||
|
||||
+10
-4
@@ -22,6 +22,8 @@ import io.cloudbeaver.WebAction;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebAuthProviderInfo;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -31,14 +33,18 @@ import java.util.Map;
|
||||
public interface DBWServiceAuth extends DBWService {
|
||||
|
||||
@WebAction(requirePermissions = {} )
|
||||
WebAuthInfo authLogin(WebSession webSession, String providerId, Map<String, Object> credentials) throws DBWebException;
|
||||
WebAuthInfo authLogin(@NotNull WebSession webSession, @NotNull String providerId, @NotNull Map<String, Object> credentials) throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = {} )
|
||||
void authLogout(WebSession webSession) throws DBWebException;
|
||||
void authLogout(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = {})
|
||||
WebAuthInfo sessionUser(WebSession webSession) throws DBWebException;
|
||||
WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException;
|
||||
|
||||
@Deprecated
|
||||
@WebAction(requirePermissions = {})
|
||||
WebAuthInfo sessionUser(@NotNull WebSession webSession) throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = {})
|
||||
WebAuthProviderInfo[] getAuthProviders(WebSession webSession);
|
||||
WebAuthProviderInfo[] getAuthProviders(@NotNull WebSession webSession);
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,9 +40,10 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
|
||||
env.getArgument("provider"),
|
||||
env.getArgument("credentials")))
|
||||
.dataFetcher("authLogout", env -> {
|
||||
getService(env).authLogout(getWebSession(env));
|
||||
getService(env).authLogout(getWebSession(env), env.getArgument("provider"));
|
||||
return true;
|
||||
})
|
||||
.dataFetcher("activeUser", env -> getService(env).activeUser(getWebSession(env, false)))
|
||||
.dataFetcher("sessionUser", env -> getService(env).sessionUser(getWebSession(env, false)))
|
||||
.dataFetcher("authProviders", env -> getService(env).getAuthProviders(getWebSession(env)))
|
||||
;
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2021 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.service.auth;
|
||||
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebUser;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WebUserInfo
|
||||
*/
|
||||
public class WebUserInfo {
|
||||
|
||||
private static final Log log = Log.getLog(WebUserInfo.class);
|
||||
|
||||
private final WebSession session;
|
||||
private final WebUser user;
|
||||
|
||||
public WebUserInfo(WebSession session, WebUser user) {
|
||||
this.session = session;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return user.getUserId();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return user.getDisplayName();
|
||||
}
|
||||
|
||||
public List<WebAuthInfo> getAuthTokens() {
|
||||
return session.getAllAuthInfo();
|
||||
}
|
||||
|
||||
}
|
||||
+27
-9
@@ -26,7 +26,11 @@ import io.cloudbeaver.registry.WebServiceRegistry;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.service.auth.DBWServiceAuth;
|
||||
import io.cloudbeaver.service.auth.WebUserInfo;
|
||||
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.access.DBASession;
|
||||
import org.jkiss.dbeaver.model.exec.DBCException;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -40,8 +44,10 @@ import java.util.Map;
|
||||
*/
|
||||
public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
|
||||
private static final Log log = Log.getLog(WebServiceAuthImpl.class);
|
||||
|
||||
@Override
|
||||
public WebAuthInfo authLogin(WebSession webSession, String providerId, Map<String, Object> authParameters) throws DBWebException {
|
||||
public WebAuthInfo authLogin(@NotNull WebSession webSession, @NotNull String providerId, @NotNull Map<String, Object> authParameters) throws DBWebException {
|
||||
DBWSecurityController serverController = CBPlatform.getInstance().getApplication().getSecurityController();
|
||||
|
||||
if (CommonUtils.isEmpty(providerId)) {
|
||||
@@ -61,12 +67,12 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
authParameters = authProviderExternal.readExternalCredentials(webSession.getProgressMonitor(), providerConfig, authParameters);
|
||||
}
|
||||
|
||||
WebUser user = null;
|
||||
WebUser user = webSession.getUser();
|
||||
String userId = serverController.getUserByCredentials(authProvider, authParameters);
|
||||
if (userId == null) {
|
||||
// User doesn't exist. We can create new user automatically if auth provider supports this
|
||||
if (authProviderExternal != null) {
|
||||
user = authProviderExternal.registerNewUser(webSession.getProgressMonitor(), serverController, providerConfig, authParameters);
|
||||
user = authProviderExternal.registerNewUser(webSession.getProgressMonitor(), serverController, providerConfig, authParameters, user);
|
||||
userId = user.getUserId();
|
||||
}
|
||||
|
||||
@@ -74,6 +80,10 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
throw new DBCException("Invalid user credentials");
|
||||
}
|
||||
}
|
||||
if (user != null && !user.getUserId().equals(userId)) {
|
||||
log.debug("Attempt to authorize user '" + userId + "' while user '" + user.getUserId() + "' already authorized");
|
||||
throw new DBCException("You cannot authorize with different users credentials");
|
||||
}
|
||||
|
||||
// Check for auth enabled. Auth is always enabled for admins
|
||||
if (!CBApplication.getInstance().getAppConfiguration().isAuthenticationEnabled()) {
|
||||
@@ -102,7 +112,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
authInfo.setAuthProvider(authProvider);
|
||||
authInfo.setAuthSession(authSession);
|
||||
authInfo.setMessage("Authenticated with " + authProvider.getLabel() + " provider");
|
||||
webSession.setAuthInfo(authInfo);
|
||||
webSession.addAuthInfo(authInfo);
|
||||
|
||||
return authInfo;
|
||||
} catch (DBException e) {
|
||||
@@ -111,23 +121,31 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authLogout(WebSession webSession) throws DBWebException {
|
||||
public void authLogout(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException {
|
||||
if (webSession.getUser() == null) {
|
||||
throw new DBWebException("Not logged in");
|
||||
}
|
||||
webSession.setAuthInfo(null);
|
||||
webSession.removeAuthInfo(providerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthInfo sessionUser(WebSession webSession) throws DBWebException {
|
||||
public WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException {
|
||||
if (webSession.getUser() == null) {
|
||||
return null;
|
||||
}
|
||||
return webSession.getAuthInfo();
|
||||
return new WebUserInfo(webSession, webSession.getUser());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthProviderInfo[] getAuthProviders(WebSession webSession) {
|
||||
public WebAuthInfo sessionUser(@NotNull WebSession webSession) throws DBWebException {
|
||||
if (webSession.getUser() == null) {
|
||||
return null;
|
||||
}
|
||||
return webSession.getAuthInfo(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthProviderInfo[] getAuthProviders(@NotNull WebSession webSession) {
|
||||
return WebServiceRegistry.getInstance().getAuthProviders()
|
||||
.stream().map(WebAuthProviderInfo::new)
|
||||
.toArray(WebAuthProviderInfo[]::new);
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true
|
||||
Bundle-Version: 1.0.20.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.21.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.data.transfer</artifactId>
|
||||
<version>1.0.20-SNAPSHOT</version>
|
||||
<version>1.0.21-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+38
-52
@@ -1,18 +1,17 @@
|
||||
package io.cloudbeaver.service.data.transfer.impl;
|
||||
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.WebServiceServletBase;
|
||||
import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor;
|
||||
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferRegistry;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
@@ -20,69 +19,56 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WebDataTransferServlet extends HttpServlet {
|
||||
public class WebDataTransferServlet extends WebServiceServletBase {
|
||||
|
||||
private static final Log log = Log.getLog(WebDataTransferServlet.class);
|
||||
|
||||
private final CBApplication application;
|
||||
private final DBWServiceDataTransfer dtManager;
|
||||
|
||||
public WebDataTransferServlet(CBApplication application, DBWServiceDataTransfer dtManager) {
|
||||
this.application = application;
|
||||
super(application);
|
||||
this.dtManager = dtManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
try {
|
||||
String dataFileId = request.getPathInfo();
|
||||
if (CommonUtils.isEmpty(dataFileId)) {
|
||||
throw new DBWebException("Data ID not specified");
|
||||
}
|
||||
while (dataFileId.startsWith("/")) {
|
||||
dataFileId = dataFileId.substring(1);
|
||||
}
|
||||
protected void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
|
||||
String dataFileId = request.getPathInfo();
|
||||
if (CommonUtils.isEmpty(dataFileId)) {
|
||||
throw new DBWebException("Data ID not specified");
|
||||
}
|
||||
while (dataFileId.startsWith("/")) {
|
||||
dataFileId = dataFileId.substring(1);
|
||||
}
|
||||
|
||||
WebSession webSession = CBPlatform.getInstance().getSessionManager().findWebSession(request);
|
||||
if (webSession == null) {
|
||||
throw new DBWebException("No active session");
|
||||
}
|
||||
WebDataTransferSessionConfig dtConfig = WebDataTransferUtils.getSessionDataTransferConfig(webSession);
|
||||
WebDataTransferTaskConfig taskInfo = dtConfig.getTask(dataFileId);
|
||||
if (taskInfo == null) {
|
||||
throw new DBWebException("Session task '" + dataFileId + "' not found");
|
||||
}
|
||||
DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(taskInfo.getParameters().getProcessorId());
|
||||
if (processor == null) {
|
||||
throw new DBWebException("Wrong data processor '" + taskInfo.getParameters().getProcessorId() + "'");
|
||||
}
|
||||
String fileName = taskInfo.getExportFileName();
|
||||
if (!CommonUtils.isEmpty(fileName)) {
|
||||
fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor);
|
||||
} else {
|
||||
fileName = taskInfo.getDataFileId();
|
||||
}
|
||||
WebDataTransferSessionConfig dtConfig = WebDataTransferUtils.getSessionDataTransferConfig(session);
|
||||
WebDataTransferTaskConfig taskInfo = dtConfig.getTask(dataFileId);
|
||||
if (taskInfo == null) {
|
||||
throw new DBWebException("Session task '" + dataFileId + "' not found");
|
||||
}
|
||||
DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(taskInfo.getParameters().getProcessorId());
|
||||
if (processor == null) {
|
||||
throw new DBWebException("Wrong data processor '" + taskInfo.getParameters().getProcessorId() + "'");
|
||||
}
|
||||
String fileName = taskInfo.getExportFileName();
|
||||
if (!CommonUtils.isEmpty(fileName)) {
|
||||
fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor);
|
||||
} else {
|
||||
fileName = taskInfo.getDataFileId();
|
||||
}
|
||||
|
||||
File dataFile = taskInfo.getDataFile();
|
||||
File dataFile = taskInfo.getDataFile();
|
||||
|
||||
response.setHeader("Content-Type", processor.getContentType());
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||
response.setHeader("Content-Length", String.valueOf(dataFile.length()));
|
||||
response.setHeader("Content-Type", processor.getContentType());
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||
response.setHeader("Content-Length", String.valueOf(dataFile.length()));
|
||||
|
||||
try (InputStream is = new FileInputStream(dataFile)) {
|
||||
IOUtils.copyStream(is, response.getOutputStream());
|
||||
}
|
||||
try (InputStream is = new FileInputStream(dataFile)) {
|
||||
IOUtils.copyStream(is, response.getOutputStream());
|
||||
}
|
||||
|
||||
// TODO: cleanup export files ASAP?
|
||||
if (false) {
|
||||
dtConfig.removeTask(taskInfo);
|
||||
}
|
||||
|
||||
//response.ok(HttpServletResponse.SC_BAD_REQUEST, "Not implemented");
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error reading data: " + e.getMessage());
|
||||
return;
|
||||
// TODO: cleanup export files ASAP?
|
||||
if (false) {
|
||||
dtConfig.removeTask(taskInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -6,9 +6,7 @@ import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
|
||||
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
|
||||
public class WebDataTransferUtils extends HttpServlet {
|
||||
class WebDataTransferUtils {
|
||||
|
||||
private static final Log log = Log.getLog(WebDataTransferUtils.class);
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: %Bundle-Vendor
|
||||
Bundle-Name: %Bundle-Name
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true
|
||||
Bundle-Version: 1.0.23.qualifier
|
||||
Bundle-Release-Date: 20210301
|
||||
Bundle-Version: 1.0.24.qualifier
|
||||
Bundle-Release-Date: 20210322
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-ClassPath: .
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.metadata</artifactId>
|
||||
<version>1.0.23-SNAPSHOT</version>
|
||||
<version>1.0.24-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.ce.drivers.feature"
|
||||
label="Cloudbeaver CE 3rd Party Drivers"
|
||||
version="1.0.43.qualifier"
|
||||
version="1.0.44.qualifier"
|
||||
provider-name="DBeaver Corp">
|
||||
|
||||
<plugin id="io.cloudbeaver.resources.drivers.base" download-size="0" install-size="0" version="0.0.0"/>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.ce.drivers.feature</artifactId>
|
||||
<version>1.0.43-SNAPSHOT</version>
|
||||
<version>1.0.44-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.product.ce.feature"
|
||||
label="%featureName"
|
||||
version="20.0.4.qualifier"
|
||||
version="20.0.5.qualifier"
|
||||
provider-name="%providerName"
|
||||
plugin="io.cloudbeaver.product.ce">
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.product.ce.feature</artifactId>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.sample.feature"
|
||||
label="%featureName"
|
||||
version="1.0.23.qualifier"
|
||||
version="1.0.24.qualifier"
|
||||
provider-name="%providerName">
|
||||
|
||||
<description>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.sample.feature</artifactId>
|
||||
<version>1.0.23-SNAPSHOT</version>
|
||||
<version>1.0.24-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.server.feature"
|
||||
label="%featureName"
|
||||
version="20.0.4.qualifier"
|
||||
version="20.0.5.qualifier"
|
||||
provider-name="%providerName"
|
||||
plugin="io.cloudbeaver.server">
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server.feature</artifactId>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
<properties>
|
||||
<dbeaver-product>CloudBeaver CE</dbeaver-product>
|
||||
<dbeaver-version>20.0.4</dbeaver-version>
|
||||
<dbeaver-version>20.0.5</dbeaver-version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?pde version="3.5"?>
|
||||
|
||||
<product name="Cloudbeaver Provided Sample" uid="io.cloudbeaver.provided.sample.product" id="io.cloudbeaver.provided.sample.product" application="io.cloudbeaver.server.standalone" version="20.0.4.qualifier" useFeatures="true" includeLaunchers="false">
|
||||
<product name="Cloudbeaver Provided Sample" uid="io.cloudbeaver.provided.sample.product" id="io.cloudbeaver.provided.sample.product" application="io.cloudbeaver.server.standalone" version="20.0.5.qualifier" useFeatures="true" includeLaunchers="false">
|
||||
|
||||
<configIni use="default">
|
||||
</configIni>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<artifactId>provided-sample</artifactId>
|
||||
<packaging>eclipse-repository</packaging>
|
||||
<name>Cloudbeaver Provided Sample Product</name>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?pde version="3.5"?>
|
||||
|
||||
<product name="CloudBeaver ce Server" uid="io.cloudbeaver.product" id="io.cloudbeaver.product.ce.product"
|
||||
application="io.cloudbeaver.product.ce.application" version="20.0.4.qualifier" useFeatures="true"
|
||||
application="io.cloudbeaver.product.ce.application" version="20.0.5.qualifier" useFeatures="true"
|
||||
includeLaunchers="false">
|
||||
|
||||
<configIni use="default">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
<version>20.0.4-SNAPSHOT</version>
|
||||
<version>20.0.5-SNAPSHOT</version>
|
||||
<artifactId>web-server</artifactId>
|
||||
<packaging>eclipse-repository</packaging>
|
||||
<name>Cloudbeaver Server Product</name>
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
"reakit": "^1.3.4",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"reshadow": "^0.0.1-alpha.78",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"go-split": "^1.x.x",
|
||||
"go-split": "^2.x.x",
|
||||
"sanitize-html": "^1.x.x",
|
||||
"react-sanitized-html": "^2.x.x"
|
||||
},
|
||||
|
||||
@@ -13,10 +13,10 @@ import { composes } from '@cloudbeaver/core-theming';
|
||||
export const BASE_CONTAINERS_STYLES = composes(
|
||||
css`
|
||||
Group {
|
||||
composes: theme-background-surface from global;
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
ColoredContainer {
|
||||
composes: theme-background-secondary from global;
|
||||
composes: theme-background-secondary theme-text-on-secondary from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
|
||||
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ILayoutSizeProps } from '../Containers/LayoutProps';
|
||||
import { baseFormControlStylesNew } from './baseFormControlStylesNew';
|
||||
@@ -20,16 +20,33 @@ const styles = css`
|
||||
textarea {
|
||||
line-height: 19px;
|
||||
}
|
||||
field[|embedded] {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& textarea {
|
||||
border-radius: 0 !important;
|
||||
height: 100%;
|
||||
resize: none !important;
|
||||
}
|
||||
}
|
||||
field-label {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
composes: theme-typography--body1 from global;
|
||||
font-weight: 500;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type BaseProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> & ILayoutSizeProps & {
|
||||
type BaseProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'style'> & ILayoutSizeProps & {
|
||||
mod?: 'surface';
|
||||
style?: ComponentStyle;
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
type ControlledProps = BaseProps & {
|
||||
@@ -53,6 +70,7 @@ interface TextareaType {
|
||||
|
||||
export const TextareaNew: TextareaType = observer(function TextareaNew({
|
||||
name,
|
||||
style,
|
||||
value: controlledValue,
|
||||
state,
|
||||
children,
|
||||
@@ -61,6 +79,7 @@ export const TextareaNew: TextareaType = observer(function TextareaNew({
|
||||
medium,
|
||||
large,
|
||||
mod,
|
||||
embedded,
|
||||
onChange = () => {},
|
||||
...rest
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
@@ -80,8 +99,8 @@ export const TextareaNew: TextareaType = observer(function TextareaNew({
|
||||
|
||||
const value = state ? state[name] : controlledValue;
|
||||
|
||||
return styled(useStyles(baseFormControlStylesNew, styles))(
|
||||
<field as="div" className={className} {...use({ small, medium, large })}>
|
||||
return styled(useStyles(baseFormControlStylesNew, styles, style))(
|
||||
<field as="div" className={className} {...use({ small, medium, large, embedded })}>
|
||||
<field-label as='label'>{children}</field-label>
|
||||
<textarea
|
||||
{...rest}
|
||||
|
||||
@@ -25,7 +25,7 @@ const styles = composes(
|
||||
composes: theme-ripple from global;
|
||||
}
|
||||
ShadowInput {
|
||||
composes: theme-background-surface from global;
|
||||
composes: theme-background-surface theme-border-color-positive from global;
|
||||
}
|
||||
[|error] {
|
||||
composes: theme-text-error from global;
|
||||
@@ -77,14 +77,14 @@ const styles = composes(
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: solid 1px #01cca3;
|
||||
border: solid 1px;
|
||||
|
||||
&[|edited] {
|
||||
font-weight: 600;
|
||||
}
|
||||
&:global([readonly]), &:not(:focus):not([|focus]) {
|
||||
background: transparent;
|
||||
border: solid 1px transparent;
|
||||
border: solid 1px transparent !important;
|
||||
}
|
||||
}
|
||||
Icon {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useMemo } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-localization';
|
||||
import { DynamicStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { TabContext } from '../TabContext';
|
||||
import type { ITabData } from '../TabsContainer/ITabsContainer';
|
||||
@@ -25,7 +25,7 @@ interface Props<T = Record<string, any>> {
|
||||
name?: string;
|
||||
component?: React.FC<TabProps & T>;
|
||||
className?: string;
|
||||
style?: DynamicStyle[] | DynamicStyle;
|
||||
style?: ComponentStyle;
|
||||
disabled?: boolean;
|
||||
onOpen?: (tab: ITabData<any>) => void;
|
||||
onClose?: (tab: ITabData<any>) => void;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import type { DynamicStyle } from '@cloudbeaver/core-theming';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { ITabData } from '../TabsContainer/ITabsContainer';
|
||||
|
||||
@@ -16,7 +16,7 @@ export type TabProps = PropsWithChildren<{
|
||||
tabId: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
style?: DynamicStyle[] | DynamicStyle;
|
||||
style?: ComponentStyle;
|
||||
onOpen?: (tab: ITabData<any>) => void;
|
||||
onClose?: (tab: ITabData<any>) => void;
|
||||
}>;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const verticalRotatedTabStyles = css`
|
||||
}
|
||||
}
|
||||
TabList {
|
||||
outline: none;
|
||||
max-width: 32px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import { TabList as BaseTabList, TabListOptions, TabStateReturn } from 'reakit/Tab';
|
||||
|
||||
import type { DynamicStyle } from '@cloudbeaver/core-theming';
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { TabDefault } from './Tab/TabDefault';
|
||||
import { TabsContext } from './TabsContext';
|
||||
|
||||
type Props = React.PropsWithChildren<Omit<TabListOptions, keyof TabStateReturn>> & {
|
||||
style?: DynamicStyle[] | DynamicStyle;
|
||||
style?: ComponentStyle;
|
||||
childrenFirst?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { DynamicStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { TabPanel } from './TabPanel';
|
||||
import type { ITabInfo } from './TabsContainer/ITabsContainer';
|
||||
import { TabsContext } from './TabsContext';
|
||||
|
||||
interface Props {
|
||||
style?: DynamicStyle[] | DynamicStyle;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const TabPanelList: React.FC<Props> = observer(function TabPanelList({
|
||||
|
||||
@@ -44,16 +44,15 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
}: Props<T>): React.ReactElement | null {
|
||||
if (
|
||||
!selectedId
|
||||
&& !currentTabId
|
||||
&& currentTabId === undefined
|
||||
&& container
|
||||
&& container.tabInfoList.length > 0
|
||||
) {
|
||||
selectedId = container.tabInfoList[0].key;
|
||||
}
|
||||
|
||||
// TODO: according react documentation fallback local state should be placed in another useState
|
||||
// to avoid memory release from useMemo()
|
||||
const tabsState = useMemo(() => localState || new MetadataMap<string, any>(), [localState]);
|
||||
const [localTabsState] = useState(() => new MetadataMap<string, any>());
|
||||
const tabsState = localState || localTabsState;
|
||||
const [closeExecutor] = useState(() => new Executor<ITabData<T>>());
|
||||
const [openExecutor] = useState(() => new Executor<ITabData<T>>());
|
||||
|
||||
@@ -78,8 +77,10 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
state,
|
||||
});
|
||||
|
||||
if (currentTabId) {
|
||||
if (currentTabId !== undefined) {
|
||||
state.selectedId = currentTabId;
|
||||
state.setSelectedId(currentTabId);
|
||||
dynamic.selectedId = currentTabId;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,6 +106,10 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTabId !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
openExecutor.execute({
|
||||
tabId: state.selectedId!,
|
||||
props: rest as T,
|
||||
|
||||
@@ -43,6 +43,22 @@ $mdc-theme-text-colors: (
|
||||
),
|
||||
);
|
||||
|
||||
$mdc-ripple-dark-ink-opacities: (
|
||||
hover: .04,
|
||||
focus: .12,
|
||||
press: .12,
|
||||
selected: .08,
|
||||
activated: .12
|
||||
) !default;
|
||||
|
||||
$mdc-ripple-light-ink-opacities: (
|
||||
hover: .04,
|
||||
focus: .12,
|
||||
press: .12,
|
||||
selected: .08,
|
||||
activated: .12
|
||||
) !default;
|
||||
|
||||
$theme-name: "dark";
|
||||
|
||||
@import "mixins";
|
||||
|
||||
@@ -20,6 +20,7 @@ export type BaseStyles = ClassCollection | Composes;
|
||||
export type ThemeSelector = (theme: string) => Promise<undefined | BaseStyles | BaseStyles[]>;
|
||||
export type Style = BaseStyles | ThemeSelector;
|
||||
export type DynamicStyle = Style | boolean | undefined;
|
||||
export type ComponentStyle = DynamicStyle | DynamicStyle[];
|
||||
|
||||
/**
|
||||
* Changes styles depending on theme
|
||||
@@ -27,11 +28,11 @@ export type DynamicStyle = Style | boolean | undefined;
|
||||
* @param componentStyles styles array
|
||||
*/
|
||||
export function useStyles(
|
||||
...componentStyles: Array<DynamicStyle | DynamicStyle[]>
|
||||
...componentStyles: ComponentStyle[]
|
||||
): Record<string, any> {
|
||||
// todo do you understand that we store ALL STYLES in each component that uses this hook?
|
||||
|
||||
const stylesRef = useRef<Array<DynamicStyle | DynamicStyle[]>>([]);
|
||||
const stylesRef = useRef<ComponentStyle[]>([]);
|
||||
const [patch, forceUpdate] = useState(0);
|
||||
const loadedStyles = useRef<BaseStyles[]>([]);
|
||||
const themeService = useService(ThemeService);
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
|
||||
onMouseUp={handleMouseUp}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
{...props}
|
||||
row={row}
|
||||
row={[...row]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
+7
-24
@@ -9,30 +9,11 @@
|
||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import type { FormatterProps } from 'react-data-grid';
|
||||
|
||||
import { ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import { EditingContext } from '../../Editing/EditingContext';
|
||||
import { CellEditor, IEditorRef } from '../CellEditor/CellEditor';
|
||||
|
||||
function valueGetter(rawValue: any) {
|
||||
if (rawValue !== null && typeof rawValue === 'object') {
|
||||
return JSON.stringify(rawValue);
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
function formatValue(rawValue: any) {
|
||||
const value = valueGetter(rawValue);
|
||||
|
||||
if (typeof value === 'string' && value.length > 1000) {
|
||||
return value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
import { DataGridContext } from '../DataGridContext';
|
||||
|
||||
function getClasses(rawValue: any) {
|
||||
const classes = [];
|
||||
@@ -45,10 +26,12 @@ function getClasses(rawValue: any) {
|
||||
export const CellFormatter: React.FC<FormatterProps> = function CellFormatter({ rowIdx, row, column, isCellSelected }) {
|
||||
const editorRef = useRef<IEditorRef>(null);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const context = useContext(DataGridContext);
|
||||
const editingContext = useContext(EditingContext);
|
||||
const formatter = context?.model.source.getAction(context.resultIndex, ResultSetFormatAction);
|
||||
const rawValue = row[column.key];
|
||||
const classes = getClasses(rawValue);
|
||||
const value = formatValue(rawValue);
|
||||
const value = formatter?.toString(rawValue) ?? String(rawValue);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
editingContext?.closeEditor({ idx: column.idx, rowIdx });
|
||||
@@ -60,7 +43,7 @@ export const CellFormatter: React.FC<FormatterProps> = function CellFormatter({
|
||||
editorRef.current?.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [isCellSelected]);
|
||||
|
||||
if (editingContext?.isEditing({ idx: column.idx, rowIdx })) {
|
||||
return (
|
||||
|
||||
+13
-5
@@ -8,17 +8,25 @@
|
||||
|
||||
@import "@cloudbeaver/core-theming/src/styles/branding";
|
||||
|
||||
$edited-color: #ff99006b;
|
||||
|
||||
@mixin base-react-grid() {
|
||||
:global {
|
||||
.cb-react-grid-container:focus-within {
|
||||
.rdg-cell-selected {
|
||||
box-shadow: inset 0 0 0 1px #0091ea;
|
||||
}
|
||||
.rdg-cell-custom-selected {
|
||||
background-color: rgba(0, 145, 234, 0.3);
|
||||
}
|
||||
}
|
||||
.rdg-cell-selected {
|
||||
box-shadow: inset 0 0 0 1px #0091ea;
|
||||
}
|
||||
.rdg-cell-custom-editing {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.rdg-cell-custom-edited {
|
||||
background-color: $edited-color;
|
||||
}
|
||||
}
|
||||
.cb-react-grid-theme {
|
||||
@include mdc-theme-prop(color, on-surface, false);
|
||||
|
||||
@@ -63,7 +71,7 @@
|
||||
}
|
||||
|
||||
.rdg-cell-custom-edited {
|
||||
background-color: #ff99006b;
|
||||
background-color: $edited-color;
|
||||
}
|
||||
|
||||
.cell-formatter {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 757 B |
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { BASE_CONTAINERS_STYLES, TextareaNew } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSetFormatAction';
|
||||
import { IResultSetSelectKey, ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSetSelectAction';
|
||||
import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet';
|
||||
import type { DataPresentationComponent } from '../../DataPresentationService';
|
||||
|
||||
export const TextValuePresentation: DataPresentationComponent<any, IDatabaseResultSet> = observer(function TextValuePresentation({
|
||||
model,
|
||||
resultIndex,
|
||||
}) {
|
||||
const styles = useStyles(BASE_CONTAINERS_STYLES);
|
||||
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
|
||||
|
||||
const result = model.getResult(resultIndex);
|
||||
const selectedCells = selection.getSelectedElements();
|
||||
|
||||
let value: any;
|
||||
let stringValue: string | undefined;
|
||||
let firstSelectedCell: Required<IResultSetSelectKey> | undefined;
|
||||
|
||||
if (result?.data?.rows && selectedCells.length > 0) {
|
||||
firstSelectedCell = selectedCells[0];
|
||||
value = model.source
|
||||
.getEditor(resultIndex)
|
||||
.getCell(firstSelectedCell.row, firstSelectedCell.column);
|
||||
|
||||
stringValue = model.source
|
||||
.getAction(resultIndex, ResultSetFormatAction)
|
||||
.get(value);
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (firstSelectedCell) {
|
||||
model.source
|
||||
.getEditor(resultIndex)
|
||||
.setCell(firstSelectedCell.row, firstSelectedCell.column, value);
|
||||
}
|
||||
};
|
||||
|
||||
return styled(styles)(
|
||||
<TextareaNew
|
||||
name="value"
|
||||
rows={3}
|
||||
value={stringValue}
|
||||
disabled={stringValue === undefined}
|
||||
readOnly={model.isReadonly() || (value !== null && typeof value === 'object')}
|
||||
embedded
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DataPresentationService, DataPresentationType } from '../../DataPresentationService';
|
||||
import { TextValuePresentation } from './TextValuePresentation';
|
||||
|
||||
@injectable()
|
||||
export class TextValuePresentationBootstrap extends Bootstrap {
|
||||
constructor(private readonly dataPresentationService: DataPresentationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
register(): void | Promise<void> {
|
||||
this.dataPresentationService.add({
|
||||
id: 'value-text-presentation',
|
||||
dataFormat: ResultDataFormat.Resultset,
|
||||
type: DataPresentationType.value,
|
||||
title: 'data_viewer_presentation_value_title',
|
||||
icon: '/icons/text_value_presentation.png',
|
||||
getPresentationComponent: () => TextValuePresentation,
|
||||
});
|
||||
}
|
||||
|
||||
load(): void {}
|
||||
}
|
||||
@@ -11,24 +11,38 @@ import { injectable } from '@cloudbeaver/core-di';
|
||||
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel';
|
||||
import type { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult';
|
||||
|
||||
export interface IDataPresentationProps {
|
||||
model: IDatabaseDataModel<any>;
|
||||
export interface IDataPresentationProps<
|
||||
TOptions = any,
|
||||
TResult extends IDatabaseDataResult = IDatabaseDataResult
|
||||
> {
|
||||
model: IDatabaseDataModel<TOptions, TResult>;
|
||||
resultIndex: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type DataPresentationComponent = React.FunctionComponent<IDataPresentationProps>;
|
||||
export enum DataPresentationType {
|
||||
main,
|
||||
value
|
||||
}
|
||||
|
||||
export type DataPresentationComponent<
|
||||
TOptions = any,
|
||||
TResult extends IDatabaseDataResult = IDatabaseDataResult
|
||||
> = React.FunctionComponent<IDataPresentationProps<TOptions, TResult>>;
|
||||
|
||||
export type PresentationTabProps = TabProps & {
|
||||
presentation: DataPresentationOptions;
|
||||
presentation: IDataPresentationOptions;
|
||||
model: IDatabaseDataModel<any>;
|
||||
};
|
||||
|
||||
export type PresentationTabComponent = React.FunctionComponent<PresentationTabProps>;
|
||||
|
||||
export interface DataPresentationOptions {
|
||||
export interface IDataPresentationOptions {
|
||||
id: string;
|
||||
dataFormat: ResultDataFormat;
|
||||
type?: DataPresentationType;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
hidden?: () => boolean;
|
||||
@@ -37,25 +51,28 @@ export interface DataPresentationOptions {
|
||||
onActivate?: () => void;
|
||||
}
|
||||
|
||||
export interface IDataPresentation extends IDataPresentationOptions {
|
||||
type: DataPresentationType;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DataPresentationService {
|
||||
get default(): DataPresentationOptions | undefined {
|
||||
return this.dataPresentations.values().next().value;
|
||||
}
|
||||
|
||||
private dataPresentations: Map<string, DataPresentationOptions>;
|
||||
private dataPresentations: Map<string, IDataPresentation>;
|
||||
|
||||
constructor() {
|
||||
this.dataPresentations = new Map();
|
||||
}
|
||||
|
||||
get(id: string): DataPresentationOptions | undefined {
|
||||
get(id: string): IDataPresentation | undefined {
|
||||
return this.dataPresentations.get(id);
|
||||
}
|
||||
|
||||
getSupportedList(dataFormat: ResultDataFormat | ResultDataFormat[]): DataPresentationOptions[] {
|
||||
getSupportedList(
|
||||
type: DataPresentationType,
|
||||
dataFormat: ResultDataFormat | ResultDataFormat[]
|
||||
): IDataPresentation[] {
|
||||
return Array.from(this.dataPresentations.values()).filter(presentation => {
|
||||
if (presentation.hidden?.()) {
|
||||
if (presentation.type !== type || presentation.hidden?.()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,7 +80,11 @@ export class DataPresentationService {
|
||||
});
|
||||
}
|
||||
|
||||
getSupported(dataFormat: ResultDataFormat, presentationId: string | undefined): DataPresentationOptions | null {
|
||||
getSupported(
|
||||
type: DataPresentationType,
|
||||
dataFormat: ResultDataFormat,
|
||||
presentationId: string | undefined
|
||||
): IDataPresentation | null {
|
||||
if (presentationId) {
|
||||
const presentation = this.dataPresentations.get(presentationId);
|
||||
|
||||
@@ -73,7 +94,11 @@ export class DataPresentationService {
|
||||
}
|
||||
|
||||
for (const presentation of this.dataPresentations.values()) {
|
||||
if (presentation.dataFormat === dataFormat && !presentation.hidden?.()) {
|
||||
if (
|
||||
presentation.dataFormat === dataFormat
|
||||
&& presentation.type === type
|
||||
&& !presentation.hidden?.()
|
||||
) {
|
||||
return presentation;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +106,13 @@ export class DataPresentationService {
|
||||
return null;
|
||||
}
|
||||
|
||||
add(options: DataPresentationOptions): void {
|
||||
this.dataPresentations.set(options.id, options);
|
||||
add(options: IDataPresentationOptions): void {
|
||||
this.dataPresentations.set(
|
||||
options.id,
|
||||
{
|
||||
...options,
|
||||
type: options.type || DataPresentationType.main,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { DataViewerTabService } from './DataViewerTabService';
|
||||
|
||||
@injectable()
|
||||
export class DataViewerBootstrap {
|
||||
constructor(private dataViewerTabService: DataViewerTabService) { }
|
||||
export class DataViewerBootstrap extends Bootstrap {
|
||||
constructor(private dataViewerTabService: DataViewerTabService) {
|
||||
super();
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
register(): void | Promise<void> {
|
||||
this.dataViewerTabService.registerTabHandler();
|
||||
}
|
||||
|
||||
load(): void { }
|
||||
}
|
||||
|
||||
@@ -28,12 +28,27 @@ export const DataViewerPanel = observer(function DataViewerPanel({
|
||||
page.setState(tab, {
|
||||
presentationId,
|
||||
resultIndex: 0,
|
||||
valuePresentationId: null,
|
||||
});
|
||||
} else {
|
||||
pageState.presentationId = presentationId;
|
||||
}
|
||||
}, [page, tab]);
|
||||
|
||||
const handleValuePresentationChange = useCallback((valuePresentationId: string | null) => {
|
||||
const pageState = page.getState(tab);
|
||||
|
||||
if (!pageState) {
|
||||
page.setState(tab, {
|
||||
presentationId: '',
|
||||
resultIndex: 0,
|
||||
valuePresentationId,
|
||||
});
|
||||
} else {
|
||||
pageState.valuePresentationId = valuePresentationId;
|
||||
}
|
||||
}, [page, tab]);
|
||||
|
||||
if (!tab.handlerState.tableId) {
|
||||
return <TextPlaceholder>Table model not loaded</TextPlaceholder>;
|
||||
}
|
||||
@@ -43,7 +58,9 @@ export const DataViewerPanel = observer(function DataViewerPanel({
|
||||
tableId={tab.handlerState.tableId}
|
||||
resultIndex={pageState?.resultIndex}
|
||||
presentationId={pageState?.presentationId}
|
||||
valuePresentationId={pageState?.valuePresentationId}
|
||||
onPresentationChange={handlePresentationChange}
|
||||
onValuePresentationChange={handleValuePresentationChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DBObjectPageService, ObjectPage, ObjectViewerTabService, IObjectViewerTabState
|
||||
} from '@cloudbeaver/plugin-object-viewer';
|
||||
|
||||
import { DataPresentationService } from './DataPresentationService';
|
||||
import { DataViewerPanel } from './DataViewerPage/DataViewerPanel';
|
||||
import { DataViewerTab } from './DataViewerPage/DataViewerTab';
|
||||
import { DataViewerTableService } from './DataViewerTableService';
|
||||
@@ -26,14 +27,15 @@ import type { IDataViewerPageState } from './IDataViewerPageState';
|
||||
|
||||
@injectable()
|
||||
export class DataViewerTabService {
|
||||
page: ObjectPage<IDataViewerPageState>;
|
||||
readonly page: ObjectPage<IDataViewerPageState>;
|
||||
|
||||
constructor(
|
||||
private navNodeManagerService: NavNodeManagerService,
|
||||
private dataViewerTableService: DataViewerTableService,
|
||||
private objectViewerTabService: ObjectViewerTabService,
|
||||
private dbObjectPageService: DBObjectPageService,
|
||||
private notificationService: NotificationService
|
||||
private notificationService: NotificationService,
|
||||
private dataPresentationService: DataPresentationService
|
||||
) {
|
||||
this.page = this.dbObjectPageService.register({
|
||||
key: 'data_viewer_data',
|
||||
@@ -105,6 +107,14 @@ export class DataViewerTabService {
|
||||
tab.handlerState.objectId
|
||||
);
|
||||
tab.handlerState.tableId = model.id;
|
||||
|
||||
const pageState = this.page.getState(tab);
|
||||
if (pageState) {
|
||||
const presentation = this.dataPresentationService.get(pageState?.presentationId);
|
||||
if (presentation) {
|
||||
model.setDataFormat(presentation.dataFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: used for initial data fetch, but can repeat request each time data tab is selected,
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 type { IDatabaseDataAction } from '../IDatabaseDataAction';
|
||||
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
|
||||
|
||||
export interface IDatabaseDataFormatAction<TResult extends IDatabaseDataResult>
|
||||
extends IDatabaseDataAction<TResult> {
|
||||
get: (value: any) => any;
|
||||
toString: (value: any) => string;
|
||||
}
|
||||
+1
@@ -26,6 +26,7 @@ export interface IDatabaseDataSelectAction<TKey, TResult extends IDatabaseDataRe
|
||||
readonly actions: IExecutor<DatabaseDataEditorActionsData<TKey>>;
|
||||
isSelected: () => boolean;
|
||||
isElementSelected: (key: TKey) => boolean;
|
||||
getSelectedElements: () => TKey[];
|
||||
getRowSelection: (row: number) => number[];
|
||||
set: (key: TKey, selected: boolean) => void;
|
||||
clear: () => void;
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IDatabaseResultSet } from '../IDatabaseResultSet';
|
||||
import { databaseDataAction } from './DatabaseDataActionDecorator';
|
||||
import type { IDatabaseDataFormatAction } from './IDatabaseDataFormatAction';
|
||||
|
||||
@databaseDataAction()
|
||||
export class ResultSetFormatAction implements IDatabaseDataFormatAction<IDatabaseResultSet> {
|
||||
static dataFormat = ResultDataFormat.Resultset;
|
||||
result: IDatabaseResultSet;
|
||||
|
||||
constructor(result: IDatabaseResultSet) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get(value: any): any {
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if ('text' in value) {
|
||||
return value.text;
|
||||
} else if ('value' in value) {
|
||||
if (typeof value.value === 'object') {
|
||||
return JSON.stringify(value.value);
|
||||
}
|
||||
return value.value;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
toString(value: any): string {
|
||||
value = this.get(value);
|
||||
|
||||
if (typeof value === 'string' && value.length > 1000) {
|
||||
return value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
updateResult(result: IDatabaseResultSet): void {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -16,8 +16,8 @@ import { databaseDataAction } from './DatabaseDataActionDecorator';
|
||||
import type { DatabaseDataEditorActionsData, IDatabaseDataSelectAction } from './IDatabaseDataSelectAction';
|
||||
|
||||
export interface IResultSetSelectKey {
|
||||
row?: number;
|
||||
column?: number;
|
||||
readonly row?: number;
|
||||
readonly column?: number;
|
||||
}
|
||||
|
||||
@databaseDataAction()
|
||||
@@ -72,6 +72,16 @@ export class ResultSetSelectAction implements IDatabaseDataSelectAction<IResultS
|
||||
return row.length === this.result.data?.columns?.length;
|
||||
}
|
||||
|
||||
getSelectedElements(): Array<Required<IResultSetSelectKey>> {
|
||||
const selectedKeys: Array<Required<IResultSetSelectKey>> = [];
|
||||
|
||||
for (const [row, value] of this.selectedElements) {
|
||||
selectedKeys.push(...value.map(column => ({ row, column })));
|
||||
}
|
||||
|
||||
return selectedKeys;
|
||||
}
|
||||
|
||||
getRowSelection(row: number): number[] {
|
||||
return this.selectedElements.get(row) || [];
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@
|
||||
export interface IDataViewerPageState {
|
||||
resultIndex: number;
|
||||
presentationId: string;
|
||||
valuePresentationId: string | null;
|
||||
}
|
||||
|
||||
@@ -10,29 +10,25 @@ import { observer } from 'mobx-react-lite';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Button, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { DataPresentationOptions } from '../DataPresentationService';
|
||||
import type { IDataPresentationOptions } from '../DataPresentationService';
|
||||
import type { DataModelWrapper } from './DataModelWrapper';
|
||||
|
||||
type TableGridProps = PropsWithChildren<{
|
||||
model: DataModelWrapper; // TODO: change to IDatabaseDataModel<any>
|
||||
dataFormat: ResultDataFormat;
|
||||
presentation: DataPresentationOptions;
|
||||
presentation: IDataPresentationOptions;
|
||||
resultIndex: number;
|
||||
}>;
|
||||
|
||||
const styles = css`
|
||||
Presentation, error {
|
||||
Presentation {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
error {
|
||||
white-space: pre-wrap;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TableGrid = observer(function TableGrid({
|
||||
@@ -56,7 +52,7 @@ export const TableGrid = observer(function TableGrid({
|
||||
const Presentation = presentation.getPresentationComponent();
|
||||
|
||||
if (result?.loadedFully && !result.data) {
|
||||
return styled(styles)(<TextPlaceholder>{translate('data_viewer_nodata_message')}</TextPlaceholder>);
|
||||
return <TextPlaceholder>{translate('data_viewer_nodata_message')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
|
||||
@@ -24,7 +24,7 @@ const styles = composes(
|
||||
`,
|
||||
css`
|
||||
table-header {
|
||||
height: 40px;
|
||||
padding: 8px 0;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -23,16 +23,9 @@ const styles = composes(
|
||||
}
|
||||
`,
|
||||
css`
|
||||
SubmittingForm {
|
||||
height: 40px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
InlineEditor {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
+9
-6
@@ -12,16 +12,17 @@ import styled from 'reshadow';
|
||||
import {
|
||||
verticalRotatedTabStyles, Tab, TabIcon, TabTitle
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { DynamicStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
|
||||
import type { DataPresentationOptions } from '../../DataPresentationService';
|
||||
import type { IDataPresentationOptions } from '../../DataPresentationService';
|
||||
|
||||
interface Props {
|
||||
model: IDatabaseDataModel<any>;
|
||||
presentation: DataPresentationOptions;
|
||||
presentation: IDataPresentationOptions;
|
||||
className?: string;
|
||||
style?: DynamicStyle[] | DynamicStyle;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const PresentationTab = observer(function PresentationTab({
|
||||
@@ -30,6 +31,7 @@ export const PresentationTab = observer(function PresentationTab({
|
||||
className,
|
||||
style,
|
||||
}: Props) {
|
||||
const translate = useTranslate();
|
||||
const styles = useStyles(verticalRotatedTabStyles, style);
|
||||
|
||||
if (presentation.getTabComponent) {
|
||||
@@ -46,14 +48,15 @@ export const PresentationTab = observer(function PresentationTab({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<Tab
|
||||
tabId={presentation.id}
|
||||
style={[verticalRotatedTabStyles, style]}
|
||||
disabled={model.isLoading()}
|
||||
>
|
||||
<TabIcon icon={presentation.icon} />
|
||||
<TabTitle>{presentation.title}</TabTitle>
|
||||
{presentation.icon && <TabIcon icon={presentation.icon} />}
|
||||
{presentation.title && <TabTitle>{translate(presentation.title)}</TabTitle>}
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
+16
-13
@@ -8,6 +8,7 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
import { use } from 'reshadow';
|
||||
|
||||
import { ITabData, TabList, TabsState, verticalRotatedTabStyles } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
@@ -15,17 +16,9 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
|
||||
import { DataPresentationService } from '../../DataPresentationService';
|
||||
import { DataPresentationService, DataPresentationType } from '../../DataPresentationService';
|
||||
import { PresentationTab } from './PresentationTab';
|
||||
|
||||
interface Props {
|
||||
presentationId: string;
|
||||
supportedDataFormat: ResultDataFormat[];
|
||||
model: IDatabaseDataModel<any>;
|
||||
className?: string;
|
||||
onPresentationChange: (id: string) => void;
|
||||
}
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
Tab {
|
||||
@@ -48,13 +41,23 @@ const styles = composes(
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
tab-outer:only-child {
|
||||
TabList[|flexible] tab-outer:only-child {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const TableLeftBar = observer(function TableLeftBar({
|
||||
interface Props {
|
||||
type: DataPresentationType;
|
||||
presentationId: string | null | undefined;
|
||||
supportedDataFormat: ResultDataFormat[];
|
||||
model: IDatabaseDataModel<any>;
|
||||
className?: string;
|
||||
onPresentationChange: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TablePresentationBar = observer(function TablePresentationBar({
|
||||
type,
|
||||
presentationId,
|
||||
supportedDataFormat,
|
||||
model,
|
||||
@@ -62,14 +65,14 @@ export const TableLeftBar = observer(function TableLeftBar({
|
||||
onPresentationChange,
|
||||
}: Props) {
|
||||
const dataPresentationService = useService(DataPresentationService);
|
||||
const presentations = dataPresentationService.getSupportedList(supportedDataFormat);
|
||||
const presentations = dataPresentationService.getSupportedList(type, supportedDataFormat);
|
||||
const Tab = PresentationTab; // alias for styles matching
|
||||
const changePresentation = ({ tabId }: ITabData<any>) => onPresentationChange(tabId);
|
||||
|
||||
return styled(useStyles(styles, verticalRotatedTabStyles))(
|
||||
<table-left-bar as="div" className={className}>
|
||||
<TabsState currentTabId={presentationId} onChange={changePresentation}>
|
||||
<TabList>
|
||||
<TabList {...use({ flexible: type === DataPresentationType.main })}>
|
||||
{presentations.map(presentation => (
|
||||
<Tab
|
||||
key={presentation.id}
|
||||
@@ -10,24 +10,28 @@ import { observer } from 'mobx-react-lite';
|
||||
import { PropsWithChildren, useCallback } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { Button, Loader, TextPlaceholder, useErrorDetails, useStateDelay } from '@cloudbeaver/core-blocks';
|
||||
import { Button, Loader, Pane, ResizerControls, Split, splitStyles, TextPlaceholder, useErrorDetails, useStateDelay } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { DataPresentationService } from '../DataPresentationService';
|
||||
import { DataPresentationService, DataPresentationType } from '../DataPresentationService';
|
||||
import { TableFooter } from './TableFooter/TableFooter';
|
||||
import { TableGrid } from './TableGrid';
|
||||
import { TableHeader } from './TableHeader/TableHeader';
|
||||
import { TableLeftBar } from './TableLeftBar/TableLeftBar';
|
||||
import { TablePresentationBar } from './TablePresentationBar/TablePresentationBar';
|
||||
import { TableViewerStorageService } from './TableViewerStorageService';
|
||||
import { ValuePanel } from './ValuePanel';
|
||||
|
||||
const viewerStyles = composes(
|
||||
css`
|
||||
error {
|
||||
error, Split {
|
||||
composes: theme-background-surface from global;
|
||||
}
|
||||
table-viewer {
|
||||
composes: theme-background-secondary from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
table-viewer {
|
||||
@@ -36,19 +40,27 @@ const viewerStyles = composes(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
}
|
||||
table-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
table-data, table-box {
|
||||
table-data, table-box, Pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
Pane:first-child {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
Pane:last-child {
|
||||
flex: 0 0 30%;
|
||||
}
|
||||
error {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
@@ -74,18 +86,22 @@ type TableViewerProps = PropsWithChildren<{
|
||||
tableId: string;
|
||||
resultIndex: number | undefined;
|
||||
presentationId: string | undefined;
|
||||
valuePresentationId: string | null | undefined;
|
||||
className?: string;
|
||||
onPresentationChange: (id: string) => void;
|
||||
onValuePresentationChange: (id: string | null) => void;
|
||||
}>;
|
||||
|
||||
export const TableViewer = observer(function TableViewer({
|
||||
tableId,
|
||||
resultIndex = 0,
|
||||
presentationId,
|
||||
valuePresentationId,
|
||||
className,
|
||||
onPresentationChange,
|
||||
onValuePresentationChange,
|
||||
}: TableViewerProps) {
|
||||
const styles = useStyles(viewerStyles);
|
||||
const styles = useStyles(viewerStyles, splitStyles);
|
||||
const dataPresentationService = useService(DataPresentationService);
|
||||
const tableViewerStorageService = useService(TableViewerStorageService);
|
||||
const dataModel = tableViewerStorageService.get(tableId);
|
||||
@@ -105,25 +121,50 @@ export const TableViewer = observer(function TableViewer({
|
||||
}
|
||||
}, [onPresentationChange, dataModel]);
|
||||
|
||||
function handleValuePresentationChange(id: string) {
|
||||
if (id === valuePresentationId) {
|
||||
onValuePresentationChange(null);
|
||||
return;
|
||||
}
|
||||
const presentation = dataPresentationService.get(id);
|
||||
if (presentation) {
|
||||
onValuePresentationChange(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataModel) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const dataFormat = result?.dataFormat || ResultDataFormat.Resultset;
|
||||
|
||||
const presentation = dataPresentationService.getSupported(
|
||||
DataPresentationType.main,
|
||||
dataFormat,
|
||||
presentationId
|
||||
);
|
||||
|
||||
const valuePresentation = valuePresentationId
|
||||
? dataPresentationService.getSupported(
|
||||
DataPresentationType.value,
|
||||
dataFormat,
|
||||
valuePresentationId
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!presentation) {
|
||||
return <TextPlaceholder>There are no available presentation for data format: {dataFormat}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
const overlay = dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat;
|
||||
const loading = dataModel.isLoading();
|
||||
|
||||
return styled(styles)(
|
||||
<table-viewer as="div" className={className}>
|
||||
<TableHeader model={dataModel} />
|
||||
<table-content as='div'>
|
||||
<TableLeftBar
|
||||
<TablePresentationBar
|
||||
type={DataPresentationType.main}
|
||||
presentationId={presentation.id}
|
||||
supportedDataFormat={dataModel.supportedDataFormats}
|
||||
model={dataModel}
|
||||
@@ -131,35 +172,57 @@ export const TableViewer = observer(function TableViewer({
|
||||
/>
|
||||
<table-data as='div'>
|
||||
<table-box as='div'>
|
||||
{dataModel.source.hasResult(resultIndex) && (
|
||||
<TableGrid
|
||||
model={dataModel}
|
||||
dataFormat={dataFormat}
|
||||
presentation={presentation}
|
||||
resultIndex={resultIndex}
|
||||
/>
|
||||
{(overlay || !loading) && (
|
||||
<Split sticky={30} mode={valuePresentation?.dataFormat === dataFormat ? undefined : 'minimize'} keepRatio>
|
||||
<Pane>
|
||||
{dataModel.source.hasResult(resultIndex) && (
|
||||
<TableGrid
|
||||
model={dataModel}
|
||||
dataFormat={dataFormat}
|
||||
presentation={presentation}
|
||||
resultIndex={resultIndex}
|
||||
/>
|
||||
)}
|
||||
<error as="div" hidden={!error.details} {...use({ animated })}>
|
||||
{error.details?.message}
|
||||
<br /><br />
|
||||
<Button type='button' mod={['outlined']} onClick={() => dataModel.source.clearError()}>
|
||||
{translate('ui_error_close')}
|
||||
</Button>
|
||||
{error.details?.hasDetails && (
|
||||
<Button type='button' mod={['unelevated']} onClick={error.open}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
</error>
|
||||
</Pane>
|
||||
{valuePresentation?.dataFormat === dataFormat && <ResizerControls />}
|
||||
<Pane main>
|
||||
<ValuePanel
|
||||
model={dataModel}
|
||||
dataFormat={dataFormat}
|
||||
presentationId={valuePresentationId ?? null}
|
||||
resultIndex={resultIndex}
|
||||
/>
|
||||
</Pane>
|
||||
</Split>
|
||||
)}
|
||||
<error as="div" hidden={!error.details} {...use({ animated })}>
|
||||
{error.details?.message}
|
||||
<br /><br />
|
||||
<Button type='button' mod={['outlined']} onClick={() => dataModel.source.clearError()}>
|
||||
{translate('ui_error_close')}
|
||||
</Button>
|
||||
{error.details?.hasDetails && (
|
||||
<Button type='button' mod={['unelevated']} onClick={error.open}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
</error>
|
||||
<Loader
|
||||
loading={dataModel.isLoading()}
|
||||
loading={loading}
|
||||
cancelDisabled={!dataModel.source.canCancel}
|
||||
overlay={dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat}
|
||||
overlay={overlay}
|
||||
onCancel={() => dataModel.source.cancel()}
|
||||
/>
|
||||
</table-box>
|
||||
<TableFooter model={dataModel} resultIndex={resultIndex} />
|
||||
</table-data>
|
||||
<TablePresentationBar
|
||||
type={DataPresentationType.value}
|
||||
presentationId={valuePresentationId ?? null}
|
||||
supportedDataFormat={[dataFormat]}
|
||||
model={dataModel}
|
||||
onPresentationChange={handleValuePresentationChange}
|
||||
/>
|
||||
</table-content>
|
||||
</table-viewer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { DataPresentationService, DataPresentationType } from '../DataPresentationService';
|
||||
import type { DataModelWrapper } from './DataModelWrapper';
|
||||
|
||||
type TableGridProps = PropsWithChildren<{
|
||||
model: DataModelWrapper; // TODO: change to IDatabaseDataModel<any>
|
||||
dataFormat: ResultDataFormat;
|
||||
presentationId: string | null;
|
||||
resultIndex: number;
|
||||
}>;
|
||||
|
||||
const styles = css`
|
||||
Presentation {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ValuePanel = observer(function ValuePanel({
|
||||
model,
|
||||
dataFormat,
|
||||
presentationId,
|
||||
resultIndex,
|
||||
}: TableGridProps) {
|
||||
const translate = useTranslate();
|
||||
const dataPresentationService = useService(DataPresentationService);
|
||||
|
||||
if (!presentationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const presentation = dataPresentationService.getSupported(
|
||||
DataPresentationType.value,
|
||||
dataFormat,
|
||||
presentationId
|
||||
);
|
||||
const result = model.getResult(resultIndex);
|
||||
|
||||
if (!presentation || dataFormat !== presentation.dataFormat) {
|
||||
if (model.isLoading()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-unescaped-entities
|
||||
return <TextPlaceholder>Current data can't be displayed by selected presentation</TextPlaceholder>;
|
||||
}
|
||||
|
||||
const Presentation = presentation.getPresentationComponent();
|
||||
|
||||
if (result?.loadedFully && !result.data) {
|
||||
return <TextPlaceholder>{translate('data_viewer_nodata_message')}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<Presentation model={model} resultIndex={resultIndex} />
|
||||
);
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { manifest } from './manifest';
|
||||
export default manifest;
|
||||
|
||||
export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator';
|
||||
export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction';
|
||||
export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction';
|
||||
export * from './DatabaseDataModel/Actions/ResultSetFormatAction';
|
||||
export * from './DatabaseDataModel/Actions/ResultSetSelectAction';
|
||||
export * from './DatabaseDataModel/DatabaseDataActions';
|
||||
export * from './DatabaseDataModel/DatabaseDataEditor';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default [
|
||||
['table_header_sql_expression', 'Enter a SQL expression to filter results'],
|
||||
['data_viewer_nodata_message', 'No data to show'],
|
||||
['data_viewer_presentation_value_title', 'Value'],
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default [
|
||||
['table_header_sql_expression', 'Введите SQL выражение чтобы отфильтровать результаты'],
|
||||
['data_viewer_nodata_message', 'Нет данных для отображения'],
|
||||
['data_viewer_presentation_value_title', 'Значение'],
|
||||
];
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import type { IServiceInjector, PluginManifest } from '@cloudbeaver/core-di';
|
||||
import type { PluginManifest } from '@cloudbeaver/core-di';
|
||||
|
||||
import { TextValuePresentationBootstrap } from './DataPresentation/TextValuePresentation/TextValuePresentationBootstrap';
|
||||
import { DataPresentationService } from './DataPresentationService';
|
||||
import { DataViewerBootstrap } from './DataViewerBootstrap';
|
||||
import { DataViewerTableService } from './DataViewerTableService';
|
||||
@@ -23,6 +24,7 @@ export const manifest: PluginManifest = {
|
||||
},
|
||||
|
||||
providers: [
|
||||
DataViewerBootstrap,
|
||||
DataViewerTabService,
|
||||
DataViewerTableService,
|
||||
DataPresentationService,
|
||||
@@ -30,11 +32,6 @@ export const manifest: PluginManifest = {
|
||||
TableFooterMenuService,
|
||||
TableHeaderService,
|
||||
LocaleService,
|
||||
TextValuePresentationBootstrap,
|
||||
],
|
||||
|
||||
async initialize(services: IServiceInjector) {
|
||||
services
|
||||
.resolveServiceByClass(DataViewerBootstrap)
|
||||
.bootstrap();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,5 +16,5 @@ export interface IObjectViewerTabState {
|
||||
tabIcon?: string;
|
||||
tabTitle?: string;
|
||||
pageId: string;
|
||||
pagesState: Map<string, any>;
|
||||
pagesState: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DBObjectPageService {
|
||||
getPageState<T>(tab: ITab<IObjectViewerTabState>, page: ObjectPage<T>| string): T | undefined {
|
||||
const pageKey = typeof page === 'string' ? page : page.key;
|
||||
|
||||
return tab.handlerState.pagesState.get(pageKey);
|
||||
return tab.handlerState.pagesState[pageKey];
|
||||
}
|
||||
|
||||
trySwitchPage<T>(tab: ITab<IObjectViewerTabState>, page: ObjectPage<T>, state?: T): boolean {
|
||||
@@ -60,14 +60,14 @@ export class DBObjectPageService {
|
||||
selectPage = async <T>(tab: ITab<IObjectViewerTabState>, page: ObjectPage<T>, state?: T) => {
|
||||
tab.handlerState.pageId = page.key;
|
||||
if (state !== undefined) {
|
||||
tab.handlerState.pagesState.set(page.key, state);
|
||||
tab.handlerState.pagesState[page.key] = state;
|
||||
}
|
||||
await this.callHandlerCallback(tab, page => page.onSelect);
|
||||
};
|
||||
|
||||
async restorePages(tab: ITab<IObjectViewerTabState>): Promise<boolean> {
|
||||
for (const page of this.pages.values()) {
|
||||
if (page.onRestore && !page.onRestore(tab, this.getPageState(tab, page))) {
|
||||
if (page.onRestore && !(await page.onRestore(tab, this.getPageState(tab, page)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ export class ObjectPage<T = unknown> {
|
||||
}
|
||||
|
||||
getState(tab: ITab<IObjectViewerTabState>): T | undefined {
|
||||
return tab.handlerState.pagesState.get(this.key);
|
||||
return tab.handlerState.pagesState[this.key];
|
||||
}
|
||||
|
||||
setState(tab: ITab<IObjectViewerTabState>, state: T): void {
|
||||
tab.handlerState.pagesState.set(this.key, state);
|
||||
tab.handlerState.pagesState[this.key] = state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class ObjectViewerTabService {
|
||||
contexts: IExecutionContextProvider<INodeNavigationData>,
|
||||
data: INodeNavigationData
|
||||
): Promise<IObjectViewerTabContext> => {
|
||||
const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
|
||||
const tabInfo = contexts.getContext(this.navigationTabsService.navigationTabContext);
|
||||
const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext);
|
||||
|
||||
// check if tab already exist for object
|
||||
@@ -100,7 +100,7 @@ export class ObjectViewerTabService {
|
||||
parents: await nodeInfo.getParents(),
|
||||
folderId: nodeInfo.folderId,
|
||||
pageId: '',
|
||||
pagesState: new Map(),
|
||||
pagesState: {},
|
||||
tabIcon: nodeInfo.icon,
|
||||
tabTitle: nodeInfo.name,
|
||||
},
|
||||
@@ -291,7 +291,7 @@ export class ObjectViewerTabService {
|
||||
&& (!tab.handlerState.tabIcon || typeof tab.handlerState.tabIcon === 'string')
|
||||
&& (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string')
|
||||
) {
|
||||
tab.handlerState.pagesState = observable.map(tab.handlerState.pagesState);
|
||||
// tab.handlerState.pagesState = observable.map(tab.handlerState.pagesState);
|
||||
if (tab.handlerState.connectionId) {
|
||||
if (!this.connectionInfo.has(tab.handlerState.connectionId)) {
|
||||
return false;
|
||||
|
||||
@@ -29,6 +29,7 @@ interface SqlResultPanelProps {
|
||||
|
||||
export const SqlResultPanel = observer(function SqlResultPanel({ tab, panelInit }: SqlResultPanelProps) {
|
||||
const [presentationId, setPresentation] = useState('');
|
||||
const [valuePresentationId, setValuePresentation] = useState<string | null>(null);
|
||||
const group = tab.handlerState.queryTabGroups.find(group => group.groupId === panelInit.groupId)!;
|
||||
|
||||
return styled(style)(
|
||||
@@ -37,7 +38,9 @@ export const SqlResultPanel = observer(function SqlResultPanel({ tab, panelInit
|
||||
tableId={group.modelId}
|
||||
resultIndex={panelInit.indexInResultSet}
|
||||
presentationId={presentationId}
|
||||
valuePresentationId={valuePresentationId}
|
||||
onPresentationChange={setPresentation}
|
||||
onValuePresentationChange={setValuePresentation}
|
||||
/>
|
||||
</result-panel>
|
||||
);
|
||||
|
||||
+8
-8
@@ -6842,10 +6842,10 @@ globule@^1.0.0:
|
||||
lodash "~4.17.10"
|
||||
minimatch "~3.0.2"
|
||||
|
||||
go-split@^1.x.x:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/go-split/-/go-split-1.3.2.tgz#670fcd9316c6fe324d32429171e70ac31d344aae"
|
||||
integrity sha512-f/7Qz2HDjRBAOOqJi5JJzSl83k47Zz0xAmzFo6rETRDbGvp6bqJERq/YgjvoUVXUw8J4OiOuTnEHEQrwcH5f4Q==
|
||||
go-split@^2.x.x:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/go-split/-/go-split-2.0.1.tgz#a7cae7d738c6b8b72fae98056aca64dbf6ad05fc"
|
||||
integrity sha512-BeGVZ35Aez2frAE8yGUyr0R88r6im23N97ex1WzPWYpQoOJAmhcHalVuvlBTL8yfH6TUC/HFhB81fcuXjSeQbg==
|
||||
|
||||
got@^9.6.0:
|
||||
version "9.6.0"
|
||||
@@ -13079,10 +13079,10 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||
typescript@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
|
||||
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.23"
|
||||
|
||||
Reference in New Issue
Block a user