Merge branch 'devel' into feat/cloud-explorer-filter

This commit is contained in:
Naumov Alexey
2021-03-17 11:37:14 +03:00
91 changed files with 1108 additions and 379 deletions
@@ -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
+1 -1
View File
@@ -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>
@@ -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;
}
@@ -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;
@@ -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) {
@@ -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 {
@@ -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;
}
@@ -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);
}
*/
}
}
@@ -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;
}
}
@@ -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>
@@ -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!]!
@@ -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);
}
@@ -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)))
;
@@ -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();
}
}
@@ -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>
@@ -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);
}
}
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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": {}
}
+1 -1
View File
@@ -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);
@@ -85,7 +85,7 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
onMouseUp={handleMouseUp}
onDoubleClick={handleDoubleClick}
{...props}
row={row}
row={[...row]}
/>
);
});
@@ -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 (
@@ -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

@@ -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}
/>
);
});
@@ -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,
@@ -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;
}
@@ -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;
@@ -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;
}
}
@@ -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;
}
`
);
@@ -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>
);
});
@@ -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
View File
@@ -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"