mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-3704 feat(plugin-data-viewer): blob uploading (#2027)
* CB-3704 feat(plugin-data-viewer): blob uploading * CB-3704 fix: check truncated value correctly * CB-4051. Added upload big files * CB-4051. remove unusable files * CB-3704 feat: upload blobs * CB-4051. Refactor after discussion with front-end * CB-4051. Merge develop * CB-4051. Fixed imports * CB-3704 fix: upload blobs for new rows * CB-4051. Fixed added new row with file * fix: reset editing state * CB-4051. Fixed after review * CB-4051. revert * CB-4051. Fixed doc, imports, style * CB-4051. Fixed checkstyle * revert * CB-4051. Refactor after review, added handler for delete temp folder * CB-3704 chore: update ts project references * CB-3704 fix: rename file upload name * CB-3704. delete comments * CB-3704. Refactor after review * CB-3074. Rename job * CB-3704. Fix save for json * CB-3704 fix: boolean value representation * CB-3704 fix: downloadable data detection * CB-3704 fix: downloadable data detection * CB-3704. Fixed boolean null value * CB-3704 fix: truncate long strings for display data * CB-3704 fix: transform complex values * CB-3704. Fixed json update * CB-3704 fix: display value * CB-3704 fix: data set * CB-3704 fix: keep content type when editing content values * CB-3704 feat: add download button for * CB-3704. Rename event --------- Co-authored-by: Denis Sinelnikov <denis.sinelnikov@dbeaver.com> Co-authored-by: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
co-authored by
Denis Sinelnikov
DenisSinelnikov
EvgeniaBzzz
parent
abf3e7247c
commit
ab985673b0
+3
@@ -30,6 +30,7 @@ import org.jkiss.dbeaver.model.auth.SMSessionContext;
|
||||
import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEventDeleteTempFile;
|
||||
import org.jkiss.dbeaver.model.websocket.event.session.WSSessionExpiredEvent;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -166,6 +167,8 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
public void close() {
|
||||
super.close();
|
||||
var sessionExpiredEvent = new WSSessionExpiredEvent();
|
||||
application.getEventController().addEvent(sessionExpiredEvent);
|
||||
application.getEventController().addEvent(new WSEventDeleteTempFile(getSessionId()));
|
||||
synchronized (sessionEventHandlers) {
|
||||
for (CBWebSessionEventHandler sessionEventHandler : sessionEventHandlers) {
|
||||
try {
|
||||
|
||||
+3
-5
@@ -77,16 +77,15 @@ import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.dbeaver.runtime.jobs.DisconnectJob;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
/**
|
||||
* Web session.
|
||||
* Is the main source of data in web application
|
||||
@@ -99,7 +98,6 @@ public class WebSession extends BaseWebSession
|
||||
public static final SMSessionType CB_SESSION_TYPE = new SMSessionType("CloudBeaver");
|
||||
private static final String WEB_SESSION_AUTH_CONTEXT_TYPE = "web-session";
|
||||
private static final String ATTR_LOCALE = "locale";
|
||||
|
||||
private static final AtomicInteger TASK_ID = new AtomicInteger();
|
||||
|
||||
private final AtomicInteger taskCount = new AtomicInteger();
|
||||
|
||||
+1
@@ -23,4 +23,5 @@ public interface CBWebSessionEventHandler {
|
||||
void handleWebSessionEvent(WSEvent event) throws DBException;
|
||||
|
||||
void close();
|
||||
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSLogEventHandler">
|
||||
<topic id="cb_session_log"/>
|
||||
</eventHandler>
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSDeleteTempFileHandler">
|
||||
<topic id="cb_delete_temp_folder"/>
|
||||
</eventHandler>
|
||||
</extension>
|
||||
|
||||
</plugin>
|
||||
|
||||
@@ -51,7 +51,8 @@ enum CBEventTopic {
|
||||
cb_projects,
|
||||
cb_object_permissions,
|
||||
cb_subject_permissions,
|
||||
cb_database_output_log
|
||||
cb_database_output_log,
|
||||
cb_delete_temp_folder
|
||||
}
|
||||
|
||||
# Base server event interface
|
||||
|
||||
@@ -22,8 +22,10 @@ import io.cloudbeaver.server.jobs.SessionStateJob;
|
||||
import io.cloudbeaver.server.jobs.WebSessionMonitorJob;
|
||||
import io.cloudbeaver.service.session.WebSessionManager;
|
||||
import org.eclipse.core.resources.ResourcesPlugin;
|
||||
import org.eclipse.core.runtime.IStatus;
|
||||
import org.eclipse.core.runtime.Platform;
|
||||
import org.eclipse.core.runtime.Plugin;
|
||||
import org.eclipse.core.runtime.Status;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
@@ -39,6 +41,7 @@ import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage;
|
||||
import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore;
|
||||
import org.jkiss.dbeaver.model.qm.QMRegistry;
|
||||
import org.jkiss.dbeaver.model.qm.QMUtils;
|
||||
import org.jkiss.dbeaver.model.runtime.AbstractJob;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
|
||||
import org.jkiss.dbeaver.registry.BasePlatformImpl;
|
||||
@@ -49,6 +52,7 @@ import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter;
|
||||
import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl;
|
||||
import org.jkiss.dbeaver.utils.ContentUtils;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
import org.osgi.framework.Bundle;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -67,6 +71,7 @@ public class CBPlatform extends BasePlatformImpl {
|
||||
public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$
|
||||
|
||||
private static final Log log = Log.getLog(CBPlatform.class);
|
||||
private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files";
|
||||
|
||||
public static final String WORK_DATA_FOLDER_NAME = ".work-data";
|
||||
|
||||
@@ -159,6 +164,17 @@ public class CBPlatform extends BasePlatformImpl {
|
||||
new SessionStateJob(this)
|
||||
.scheduleMonitor();
|
||||
|
||||
new AbstractJob("Delete temp folder") {
|
||||
@Override
|
||||
protected IStatus run(DBRProgressMonitor monitor) {
|
||||
try {
|
||||
IOUtils.deleteDirectory(getTempFolder(monitor, TEMP_FILE_FOLDER));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return Status.OK_STATUS;
|
||||
}
|
||||
}.schedule();
|
||||
log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
}
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server.events;
|
||||
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEventDeleteTempFile;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class WSDeleteTempFileHandler implements WSEventHandler<WSEventDeleteTempFile> {
|
||||
|
||||
private static final Log log = Log.getLog(WSDeleteTempFileHandler.class);
|
||||
private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files";
|
||||
|
||||
public void resetTempFolder(String sessionId) {
|
||||
Path path = CBPlatform.getInstance()
|
||||
.getTempFolder(new VoidProgressMonitor(), TEMP_FILE_FOLDER)
|
||||
.resolve(sessionId);
|
||||
try {
|
||||
IOUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting temp path", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NotNull WSEventDeleteTempFile event) {
|
||||
resetTempFolder(event.getSessionId());
|
||||
}
|
||||
}
|
||||
-1
@@ -101,7 +101,6 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
|
||||
public void handleWebSessionEvent(WSEvent event) {
|
||||
super.handleEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleEventException(Exception e) {
|
||||
super.handleEventException(e);
|
||||
|
||||
+1
-1
@@ -16,12 +16,12 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import io.cloudbeaver.service.DBWService;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.WebAction;
|
||||
import io.cloudbeaver.model.WebAsyncTaskInfo;
|
||||
import io.cloudbeaver.model.WebConnectionInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.DBWService;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 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.sql;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
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.eclipse.jetty.server.Request;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.annotation.MultipartConfig;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@MultipartConfig
|
||||
public class WebSQLFileLoaderServlet extends WebServiceServletBase {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLFileLoaderServlet.class);
|
||||
|
||||
private static final Type MAP_STRING_OBJECT_TYPE = new TypeToken<Map<String, Object>>() {
|
||||
}.getType();
|
||||
private static final String REQUEST_PARAM_VARIABLES = "variables";
|
||||
|
||||
private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files";
|
||||
|
||||
private static final String FILE_ID = "fileId";
|
||||
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.serializeNulls()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
public WebSQLFileLoaderServlet(CBApplication application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processServiceRequest(
|
||||
WebSession session,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) throws DBException, IOException {
|
||||
if (!session.isAuthorizedInSecurityManager()) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Update for users only");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"POST".equalsIgnoreCase(request.getMethod())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path tempFolder = CBPlatform.getInstance()
|
||||
.getTempFolder(session.getProgressMonitor(), TEMP_FILE_FOLDER)
|
||||
.resolve(session.getSessionId());
|
||||
|
||||
MultipartConfigElement multiPartConfig = new MultipartConfigElement(tempFolder.toString());
|
||||
request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multiPartConfig);
|
||||
|
||||
Map<String, Object> variables = gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE);
|
||||
|
||||
String fileId = JSONUtils.getString(variables, FILE_ID);
|
||||
|
||||
if (fileId != null) {
|
||||
Path file = tempFolder.resolve(fileId);
|
||||
try {
|
||||
Files.write(file, request.getPart("fileData").getInputStream().readAllBytes());
|
||||
} catch (ServletException e) {
|
||||
log.error(e.getMessage());
|
||||
throw new DBWebException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-6
@@ -16,10 +16,12 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import com.google.gson.internal.LinkedTreeMap;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.WebConnectionInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.session.WebSessionProvider;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.server.jobs.SqlOutputLogReaderJob;
|
||||
import org.eclipse.jface.text.Document;
|
||||
import org.jkiss.code.NotNull;
|
||||
@@ -52,10 +54,12 @@ import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.utils.ArrayUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Web SQL processor.
|
||||
@@ -66,6 +70,9 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
|
||||
private static final int MAX_RESULTS_COUNT = 100;
|
||||
|
||||
private static final String FILE_ID = "fileId";
|
||||
private static final String TEMP_FILE_FOLDER = "temp-sql-upload-files";
|
||||
|
||||
private final WebSession webSession;
|
||||
private final WebConnectionInfo connection;
|
||||
private final SQLSyntaxManager syntaxManager;
|
||||
@@ -475,7 +482,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
for (WebSQLResultsRow row : updatedRows) {
|
||||
Map<String, Object> updateValues = row.getUpdateValues().entrySet().stream()
|
||||
.filter(x -> CommonUtils.equalObjects(allAttributes[CommonUtils.toInt(x.getKey())].getRowIdentifier(), rowIdentifier))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.collect(HashMap::new, (m,v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||
if (CommonUtils.isEmpty(row.getData()) || CommonUtils.isEmpty(updateValues)) {
|
||||
continue;
|
||||
}
|
||||
@@ -492,8 +499,8 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
Object[] rowValues = new Object[updateAttributes.length + keyAttributes.length];
|
||||
for (int i = 0; i < updateAttributes.length; i++) {
|
||||
DBDAttributeBinding updateAttribute = updateAttributes[i];
|
||||
Object realCellValue = convertInputCellValue(session, updateAttribute,
|
||||
updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition())), withoutExecution);
|
||||
Object value = updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition()));
|
||||
Object realCellValue = setCellRowValue(value, webSession, session, updateAttribute, withoutExecution);
|
||||
rowValues[i] = realCellValue;
|
||||
finalRow[updateAttribute.getOrdinalPosition()] = realCellValue;
|
||||
}
|
||||
@@ -539,8 +546,14 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
|
||||
for (int i = 0; i < allAttributes.length; i++) {
|
||||
if (addedValues.get(i) != null) {
|
||||
Object realCellValue = convertInputCellValue(session, allAttributes[i],
|
||||
addedValues.get(i), withoutExecution);
|
||||
Object realCellValue;
|
||||
if (addedValues.get(i) instanceof LinkedTreeMap) {
|
||||
LinkedTreeMap<String, Object> variables = (LinkedTreeMap<String, Object>) addedValues.get(i);
|
||||
realCellValue = setCellRowValue(variables, webSession, session, allAttributes[i], withoutExecution);
|
||||
} else {
|
||||
realCellValue = convertInputCellValue(session, allAttributes[i],
|
||||
addedValues.get(i), withoutExecution);
|
||||
}
|
||||
insertAttributes.put(allAttributes[i], realCellValue);
|
||||
finalRow[i] = realCellValue;
|
||||
}
|
||||
@@ -928,4 +941,28 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
private static DBCExecutionPurpose resolveQueryPurpose(DBDDataFilter filter) {
|
||||
return filter.hasFilters() ? DBCExecutionPurpose.USER_FILTERED : DBCExecutionPurpose.USER;
|
||||
}
|
||||
|
||||
private Object setCellRowValue(Object cellRow, WebSession webSession, DBCSession dbcSession, DBDAttributeBinding allAttributes, boolean withoutExecution) {
|
||||
if (cellRow instanceof LinkedTreeMap) {
|
||||
LinkedTreeMap<String, Object> variables = (LinkedTreeMap<String, Object>) cellRow;
|
||||
if (variables.get(FILE_ID) != null) {
|
||||
Path path = CBPlatform.getInstance()
|
||||
.getTempFolder(webSession.getProgressMonitor(), TEMP_FILE_FOLDER)
|
||||
.resolve(webSession.getSessionId())
|
||||
.resolve(variables.get(FILE_ID).toString());
|
||||
|
||||
try {
|
||||
var file = Files.newInputStream(path);
|
||||
return convertInputCellValue(dbcSession, allAttributes, file, withoutExecution);
|
||||
} catch (IOException | DBCException e) {
|
||||
return new DBException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return convertInputCellValue(dbcSession, allAttributes, cellRow, withoutExecution);
|
||||
} catch (DBCException e) {
|
||||
return new DBException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -255,6 +255,11 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> i
|
||||
new WebSQLResultServlet(application, getServiceImpl()),
|
||||
application.getServicesURI() + "sql-result-value/*"
|
||||
);
|
||||
servletContext.addServlet(
|
||||
"sqlUploadFile",
|
||||
new WebSQLFileLoaderServlet(application),
|
||||
application.getServicesURI() + "resultset/blob/*"
|
||||
);
|
||||
}
|
||||
|
||||
private static class WebSQLConfiguration {
|
||||
|
||||
+3
-3
@@ -83,7 +83,7 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
conToRead.addAll(session.getConnections());
|
||||
}
|
||||
|
||||
List<WebSQLContextInfo> contexts = new ArrayList<>();
|
||||
List<WebSQLContextInfo> contexts = new ArrayList<>();
|
||||
for (WebConnectionInfo con : conToRead) {
|
||||
WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(con, false);
|
||||
if (sqlProcessor != null) {
|
||||
@@ -334,7 +334,7 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
monitor -> {
|
||||
try {
|
||||
result.append(contextInfo.getProcessor().readLobValue(
|
||||
monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)));
|
||||
monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)));
|
||||
} catch (Exception e) {
|
||||
throw new InvocationTargetException(e);
|
||||
}
|
||||
@@ -404,7 +404,7 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
DBSDataContainer dataContainer = contextInfo.getProcessor().getDataContainerByNodePath(
|
||||
monitor, nodePath, DBSDataContainer.class);
|
||||
|
||||
WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer(
|
||||
WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer(
|
||||
contextInfo,
|
||||
monitor,
|
||||
dataContainer,
|
||||
|
||||
@@ -7,4 +7,6 @@
|
||||
height: 24px !important;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import { ReactNode, useContext, useState } from 'react';
|
||||
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
import { blobToData, bytesToSize } from '@cloudbeaver/core-utils';
|
||||
import { blobToBase64, bytesToSize } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
@@ -121,7 +121,7 @@ export const InputFileTextContent: InputFileTextContentType = observer(function
|
||||
try {
|
||||
validateFileSize(file.size);
|
||||
|
||||
const value = await blobToData(file);
|
||||
const value = await blobToBase64(file);
|
||||
|
||||
if (value) {
|
||||
setSelected(file);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './IndexedDB/IndexedDBService';
|
||||
export * from './IndexedDB/IndexedDB';
|
||||
export * from './manifest';
|
||||
export * from './selectFiles';
|
||||
export * from './ServiceWorkerService';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function selectFiles(callback: (files: FileList | null) => any): void {
|
||||
let removed = false;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = () => {
|
||||
callback(input.files);
|
||||
removed = true;
|
||||
input.remove();
|
||||
};
|
||||
input.style.position = 'fixed';
|
||||
input.style.top = '-100px';
|
||||
input.style.left = '-100px';
|
||||
input.style.opacity = '0';
|
||||
input.style.pointerEvents = 'none';
|
||||
input.style.zIndex = '-1';
|
||||
document.body.append(input);
|
||||
|
||||
input.click();
|
||||
|
||||
setTimeout(() => {
|
||||
if (!removed) {
|
||||
input.remove();
|
||||
}
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ module.exports = (env, argv) => {
|
||||
const logger = devServer.compiler.getInfrastructureLogger('webpack-dev-server');
|
||||
const port = devServer.server.address().port;
|
||||
logger.info(`Proxy from http://localhost:8080 to http://127.0.0.1:${port}`);
|
||||
httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}` }).listen(8080);
|
||||
httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}`, secure: false }).listen(8080);
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -92,6 +92,7 @@ export default [
|
||||
['ui_close_all_to_the_left', 'Close all to the Left'],
|
||||
['ui_or', 'Or'],
|
||||
['ui_download', 'Download'],
|
||||
['ui_upload', 'Upload'],
|
||||
['ui_import', 'Import'],
|
||||
['ui_view', 'View'],
|
||||
['ui_limit', 'Limit'],
|
||||
|
||||
@@ -76,6 +76,7 @@ export default [
|
||||
['ui_close_all_to_the_left', 'Close all to the Left'],
|
||||
['ui_or', 'Or'],
|
||||
['ui_download', 'Download'],
|
||||
['ui_upload', 'Upload'],
|
||||
['ui_import', 'Import'],
|
||||
['ui_view', 'View'],
|
||||
['ui_limit', 'Limit'],
|
||||
|
||||
@@ -87,7 +87,8 @@ export default [
|
||||
['ui_close_all_to_the_right', 'Закрыть все справа'],
|
||||
['ui_close_all_to_the_left', 'Закрыть все слева'],
|
||||
['ui_or', 'Или'],
|
||||
['ui_download', 'Загрузить'],
|
||||
['ui_download', 'Cкачать'],
|
||||
['ui_upload', 'Загрузить'],
|
||||
['ui_import', 'Импортировать'],
|
||||
['ui_view', 'Смотреть'],
|
||||
['ui_limit', 'Лимит'],
|
||||
|
||||
@@ -89,6 +89,7 @@ export default [
|
||||
['ui_close_all_to_the_left', 'Close all to the Left'],
|
||||
['ui_or', 'Or'],
|
||||
['ui_download', 'Download'],
|
||||
['ui_upload', 'Upload'],
|
||||
['ui_import', 'Import'],
|
||||
['ui_view', 'View'],
|
||||
['ui_limit', 'Limit'],
|
||||
|
||||
@@ -31,6 +31,19 @@ export class CustomGraphQLClient extends GraphQLClient {
|
||||
private requestsBlockedReason: Error | string | null = null;
|
||||
|
||||
async uploadFile<T = any, V extends Variables = Variables>(
|
||||
url: string,
|
||||
file: Blob,
|
||||
query?: string,
|
||||
variables?: V,
|
||||
onUploadProgress?: (event: UploadProgressEvent) => void,
|
||||
): Promise<T> {
|
||||
return this.interceptors.reduce(
|
||||
(accumulator, interceptor) => interceptor(accumulator),
|
||||
this.overrideFilesUpload<T, V>(url, file, query, variables, onUploadProgress),
|
||||
);
|
||||
}
|
||||
|
||||
async uploadFiles<T = any, V extends Variables = Variables>(
|
||||
url: string,
|
||||
files: FileList,
|
||||
query?: string,
|
||||
@@ -39,7 +52,7 @@ export class CustomGraphQLClient extends GraphQLClient {
|
||||
): Promise<T> {
|
||||
return this.interceptors.reduce(
|
||||
(accumulator, interceptor) => interceptor(accumulator),
|
||||
this.overrideFileUpload<T, V>(url, files, query, variables, onUploadProgress),
|
||||
this.overrideFilesUpload<T, V>(url, files, query, variables, onUploadProgress),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,9 +120,9 @@ export class CustomGraphQLClient extends GraphQLClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async overrideFileUpload<T, V extends Variables = Variables>(
|
||||
private async overrideFilesUpload<T, V extends Variables = Variables>(
|
||||
url: string,
|
||||
files: FileList,
|
||||
files: FileList | Blob,
|
||||
query?: string,
|
||||
variables?: V,
|
||||
onUploadProgress?: (event: UploadProgressEvent) => void,
|
||||
@@ -118,19 +131,24 @@ export class CustomGraphQLClient extends GraphQLClient {
|
||||
try {
|
||||
const { operationName } = resolveRequestDocument(query ?? '');
|
||||
// TODO: we don't support GQL response right now
|
||||
const response = await axios.postForm/*<GqlResponse>*/ <T>(
|
||||
url,
|
||||
{
|
||||
operationName,
|
||||
query,
|
||||
variables: JSON.stringify(variables),
|
||||
'files[]': files,
|
||||
},
|
||||
{
|
||||
onUploadProgress,
|
||||
responseType: 'json',
|
||||
},
|
||||
);
|
||||
const data = {
|
||||
operationName,
|
||||
query,
|
||||
variables: JSON.stringify(variables),
|
||||
'files[]': undefined as any,
|
||||
fileData: undefined as any,
|
||||
};
|
||||
|
||||
if (files instanceof FileList) {
|
||||
data['files[]'] = files;
|
||||
} else {
|
||||
data.fileData = files;
|
||||
}
|
||||
|
||||
const response = await axios.postForm/*<GqlResponse>*/ <T>(url, data, {
|
||||
onUploadProgress,
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
// TODO: we don't support GQL response right now
|
||||
// TODO: seems here can be undefined
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { GlobalConstants } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { CustomGraphQLClient, UploadProgressEvent } from '../CustomGraphQLClient';
|
||||
|
||||
export interface IUploadDriverLibraryExtension {
|
||||
uploadBlobResultSet: (fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void) => Promise<void>;
|
||||
}
|
||||
|
||||
export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension {
|
||||
return {
|
||||
uploadBlobResultSet(fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void): Promise<void> {
|
||||
// api/resultset/blob
|
||||
return client.uploadFile(GlobalConstants.absoluteServiceUrl('resultset', 'blob'), data, undefined, { fileId }, onUploadProgress);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export interface IUploadDriverLibraryExtension {
|
||||
export function uploadDriverLibraryExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension {
|
||||
return {
|
||||
uploadDriverLibrary(driverId: string, files: FileList, onUploadProgress?: (event: UploadProgressEvent) => void): Promise<void> {
|
||||
return client.uploadFile(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress);
|
||||
return client.uploadFiles(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { CustomGraphQLClient } from './CustomGraphQLClient';
|
||||
import { EnvironmentService } from './EnvironmentService';
|
||||
import { uploadBlobResultSetExtension } from './Extensions/uploadBlobResultSetExtension';
|
||||
import { uploadDriverLibraryExtension } from './Extensions/uploadDriverLibraryExtension';
|
||||
import type { IResponseInterceptor } from './IResponseInterceptor';
|
||||
import { getSdk } from './sdk';
|
||||
@@ -19,6 +20,7 @@ function extendedSDK(client: CustomGraphQLClient) {
|
||||
return {
|
||||
...sdk,
|
||||
...uploadDriverLibraryExtension(client),
|
||||
...uploadBlobResultSetExtension(client),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './AsyncTask/AsyncTask';
|
||||
export * from './AsyncTask/AsyncTaskInfoService';
|
||||
export * from './Extensions/uploadBlobResultSetExtension';
|
||||
export * from './CustomGraphQLClient';
|
||||
export * from './DetailsError';
|
||||
export * from './EnvironmentService';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function base64ToBlob(base64: string, mime = 'application/octet-stream', partSize = 512): Blob {
|
||||
const byteCharacters = atob(base64);
|
||||
const byteArrays = [];
|
||||
let slice: string;
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += partSize) {
|
||||
slice = byteCharacters.slice(offset, offset + partSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
byteArrays.push(new Uint8Array(byteNumbers));
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: mime });
|
||||
}
|
||||
+5
-1
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function blobToData(blob: Blob | File): Promise<string | null> {
|
||||
export function blobToBase64(blob: Blob | File, slice?: number): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
@@ -16,6 +16,10 @@ export function blobToData(blob: Blob | File): Promise<string | null> {
|
||||
reject(fileReader.error);
|
||||
};
|
||||
|
||||
if (slice) {
|
||||
blob = blob.slice(0, slice);
|
||||
}
|
||||
|
||||
fileReader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
@@ -24,3 +24,58 @@ export function getMIME(binary: string): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// function getMimeType(blob: Blob, callback) {
|
||||
// const fileReader = new FileReader();
|
||||
|
||||
// fileReader.onloadend = function (event) {
|
||||
// let mimeType = '';
|
||||
|
||||
// const arr = new Uint8Array(event.target.result).subarray(
|
||||
// 0,
|
||||
// 4,
|
||||
// );
|
||||
// let header = '';
|
||||
|
||||
// for (let index = 0; index < arr.length; index++) {
|
||||
// header += arr[index].toString(16);
|
||||
// }
|
||||
|
||||
// // View other byte signature patterns here:
|
||||
// // 1) https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
|
||||
// // 2) https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
// switch (header) {
|
||||
// case '89504e47': {
|
||||
// mimeType = 'image/png';
|
||||
// break;
|
||||
// }
|
||||
// case '47494638': {
|
||||
// mimeType = 'image/gif';
|
||||
// break;
|
||||
// }
|
||||
// case '52494646':
|
||||
// case '57454250':
|
||||
// mimeType = 'image/webp';
|
||||
// break;
|
||||
// case '49492A00':
|
||||
// case '4D4D002A':
|
||||
// mimeType = 'image/tiff';
|
||||
// break;
|
||||
// case 'ffd8ffe0':
|
||||
// case 'ffd8ffe1':
|
||||
// case 'ffd8ffe2':
|
||||
// case 'ffd8ffe3':
|
||||
// case 'ffd8ffe8':
|
||||
// mimeType = 'image/jpeg';
|
||||
// break;
|
||||
// default: {
|
||||
// mimeType = blob.type;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// callback(mimeType);
|
||||
// };
|
||||
|
||||
// fileReader.readAsArrayBuffer(blob.slice(0, 4));
|
||||
// }
|
||||
|
||||
@@ -8,7 +8,8 @@ export * from './Quadtree/index';
|
||||
|
||||
export * from './underscore';
|
||||
|
||||
export * from './blobToData';
|
||||
export * from './base64ToBlob';
|
||||
export * from './blobToBase64';
|
||||
export * from './bytesToSize';
|
||||
export * from './cacheValue';
|
||||
export * from './clsx';
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@cloudbeaver/core-theming": "~0.1.0",
|
||||
"@cloudbeaver/core-ui": "~0.1.0",
|
||||
"@cloudbeaver/core-utils": "~0.1.0",
|
||||
"@cloudbeaver/core-browser": "~0.1.0",
|
||||
"@cloudbeaver/plugin-data-viewer": "~0.1.0",
|
||||
"@cloudbeaver/plugin-react-data-grid": "~0.1.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -72,7 +72,7 @@ export const CellEditor = observer<Pick<RenderEditCellProps<IResultSetRowKey>, '
|
||||
|
||||
const cellKey: IResultSetElementKey = { row, column: column.columnDataIndex };
|
||||
|
||||
const value = tableDataContext.format.getText(tableDataContext.getCellValue(cellKey)!) ?? '';
|
||||
const value = tableDataContext.format.getText(cellKey);
|
||||
|
||||
const handleSave = () => onClose(false);
|
||||
const handleReject = () => {
|
||||
|
||||
@@ -90,6 +90,10 @@ export const CellRenderer = observer<CellRendererProps<IResultSetRowKey, unknown
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tableDataContext.format.isBinary(cellContext.cell)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
|
||||
const value = tableDataContext.getCellValue(cellContext.cell);
|
||||
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ export class DataGridContextMenuCellEditingService {
|
||||
const cellValue = view.getCellValue(context.data.key);
|
||||
const column = view.getColumn(context.data.key.column);
|
||||
|
||||
if (!column || cellValue === undefined || format.isReadOnly(context.data.key)) {
|
||||
if (!column || cellValue === undefined || format.isReadOnly(context.data.key) || format.isBinary(context.data.key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class DataGridContextMenuCellEditingService {
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
const cellValue = view.getCellValue(key);
|
||||
|
||||
return cellValue === undefined || format.isReadOnly(context.data.key) || view.getColumn(key.column)?.required || format.isNull(cellValue);
|
||||
return cellValue === undefined || format.isReadOnly(context.data.key) || view.getColumn(key.column)?.required || format.isNull(key);
|
||||
},
|
||||
onClick(context) {
|
||||
context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction).set(context.data.key, null);
|
||||
|
||||
+7
-10
@@ -82,7 +82,6 @@ export class DataGridContextMenuFilterService {
|
||||
): Array<IContextMenuItem<IDataGridCellMenuContext>> {
|
||||
const { model, resultIndex, key } = context.data;
|
||||
const data = model.source.getAction(resultIndex, ResultSetDataAction);
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
const supportedOperations = data.getColumnOperations(key.column);
|
||||
const columnLabel = data.getColumn(key.column)?.label || '';
|
||||
|
||||
@@ -100,8 +99,7 @@ export class DataGridContextMenuFilterService {
|
||||
},
|
||||
titleGetter() {
|
||||
const val = typeof value === 'function' ? value() : value;
|
||||
const stringifyValue = format.toDisplayString(val);
|
||||
const wrappedValue = wrapOperationArgument(operation.id, stringifyValue);
|
||||
const wrappedValue = wrapOperationArgument(operation.id, val);
|
||||
const clippedValue = replaceMiddle(wrappedValue, ' ... ', 8, 30);
|
||||
return `${columnLabel} ${operation.expression} ${clippedValue}`;
|
||||
},
|
||||
@@ -220,14 +218,14 @@ export class DataGridContextMenuFilterService {
|
||||
const supportedOperations = data.getColumnOperations(key.column);
|
||||
const value = data.getCellValue(key);
|
||||
|
||||
return value === undefined || supportedOperations.length === 0 || format.isNull(value);
|
||||
return value === undefined || supportedOperations.length === 0 || format.isNull(key);
|
||||
},
|
||||
panel: new ComputedContextMenuModel<IDataGridCellMenuContext>({
|
||||
id: 'cellValuePanel',
|
||||
menuItemsGetter: context => {
|
||||
const { model, resultIndex, key } = context.data;
|
||||
const data = model.source.getAction(resultIndex, ResultSetDataAction);
|
||||
const cellValue = data.getCellValue(key);
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
const cellValue = format.getText(key);
|
||||
const items = this.getGeneralizedMenuItems(context, cellValue, 'filter');
|
||||
return items;
|
||||
},
|
||||
@@ -253,10 +251,8 @@ export class DataGridContextMenuFilterService {
|
||||
id: 'customValuePanel',
|
||||
menuItemsGetter: context => {
|
||||
const { model, resultIndex, key } = context.data;
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
const data = model.source.getAction(resultIndex, ResultSetDataAction);
|
||||
const supportedOperations = data.getColumnOperations(key.column);
|
||||
const cellValue = data.getCellValue(key) ?? '';
|
||||
const columnLabel = data.getColumn(key.column)?.label || '';
|
||||
|
||||
return supportedOperations
|
||||
@@ -273,9 +269,10 @@ export class DataGridContextMenuFilterService {
|
||||
title: title + ' ..',
|
||||
icon: 'filter-custom',
|
||||
onClick: async () => {
|
||||
const stringifyCellValue = format.toDisplayString(cellValue);
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
const displayString = format.getText(key);
|
||||
const customValue = await this.commonDialogService.open(FilterCustomValueDialog, {
|
||||
defaultValue: stringifyCellValue,
|
||||
defaultValue: displayString,
|
||||
inputTitle: title + ':',
|
||||
});
|
||||
|
||||
|
||||
+39
-8
@@ -5,25 +5,26 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { selectFiles } from '@cloudbeaver/core-browser';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ResultSetDataContentAction, ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer';
|
||||
import {
|
||||
createResultSetBlobValue,
|
||||
ResultSetDataContentAction,
|
||||
ResultSetDataKeysUtils,
|
||||
ResultSetEditAction,
|
||||
ResultSetFormatAction,
|
||||
} from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import { DataGridContextMenuService } from './DataGridContextMenuService';
|
||||
|
||||
@injectable()
|
||||
export class DataGridContextMenuSaveContentService {
|
||||
private static readonly menuContentSaveToken = 'menuContentSave';
|
||||
|
||||
constructor(private readonly dataGridContextMenuService: DataGridContextMenuService, private readonly notificationService: NotificationService) {}
|
||||
|
||||
getMenuContentSaveToken(): string {
|
||||
return DataGridContextMenuSaveContentService.menuContentSaveToken;
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), {
|
||||
id: this.getMenuContentSaveToken(),
|
||||
id: 'menuContentDownload',
|
||||
order: 4,
|
||||
title: 'ui_download',
|
||||
icon: '/icons/export.svg',
|
||||
@@ -45,6 +46,36 @@ export class DataGridContextMenuSaveContentService {
|
||||
isDisabled: context => {
|
||||
const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction);
|
||||
|
||||
return (
|
||||
context.data.model.isLoading() ||
|
||||
(!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement))
|
||||
);
|
||||
},
|
||||
});
|
||||
this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), {
|
||||
id: 'menuContentUpload',
|
||||
order: 5,
|
||||
title: 'ui_upload',
|
||||
icon: '/icons/import.svg',
|
||||
isPresent(context) {
|
||||
return context.contextType === DataGridContextMenuService.cellContext;
|
||||
},
|
||||
onClick: async context => {
|
||||
selectFiles(files => {
|
||||
const edit = context.data.model.source.getAction(context.data.resultIndex, ResultSetEditAction);
|
||||
const file = files?.item(0) ?? undefined;
|
||||
if (file) {
|
||||
edit.set(context.data.key, createResultSetBlobValue(file));
|
||||
}
|
||||
});
|
||||
},
|
||||
isHidden: context => {
|
||||
const format = context.data.model.source.getAction(context.data.resultIndex, ResultSetFormatAction);
|
||||
return !format.isBinary(context.data.key);
|
||||
},
|
||||
isDisabled: context => {
|
||||
const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction);
|
||||
|
||||
return (
|
||||
context.data.model.isLoading() ||
|
||||
(!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement))
|
||||
|
||||
@@ -319,6 +319,7 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
|
||||
// TODO: update focus after render rows update
|
||||
if (data.type === 'focus') {
|
||||
if (!data.key?.column || !data.key.row) {
|
||||
focusSyncRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,6 +370,8 @@ export const DataGridTable = observer<IDataPresentationProps<any, IDatabaseResul
|
||||
row,
|
||||
column: { ...column.columnDataIndex },
|
||||
});
|
||||
} else {
|
||||
selectionAction.focus(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+12
-6
@@ -13,6 +13,7 @@ import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid';
|
||||
|
||||
import { CellContext } from '../CellRenderer/CellContext';
|
||||
import { TableDataContext } from '../TableDataContext';
|
||||
import { BlobFormatter } from './CellFormatters/BlobFormatter';
|
||||
import { BooleanFormatter } from './CellFormatters/BooleanFormatter';
|
||||
import { TextFormatter } from './CellFormatters/TextFormatter';
|
||||
|
||||
@@ -29,14 +30,19 @@ export const CellFormatterFactory = observer<IProps>(function CellFormatterFacto
|
||||
formatterRef.current = TextFormatter;
|
||||
|
||||
if (cellContext.cell) {
|
||||
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
|
||||
const value = tableDataContext.getCellValue(cellContext.cell);
|
||||
const isBlob = tableDataContext.format.isBinary(cellContext.cell);
|
||||
|
||||
if (value !== undefined) {
|
||||
const rawValue = tableDataContext.format.get(value);
|
||||
if (isBlob) {
|
||||
formatterRef.current = BlobFormatter;
|
||||
} else {
|
||||
const value = tableDataContext.getCellValue(cellContext.cell);
|
||||
if (value !== undefined) {
|
||||
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
|
||||
const rawValue = tableDataContext.format.get(cellContext.cell);
|
||||
|
||||
if (resultColumn && isBooleanValuePresentationAvailable(rawValue, resultColumn)) {
|
||||
formatterRef.current = BooleanFormatter;
|
||||
if (resultColumn && isBooleanValuePresentationAvailable(rawValue, resultColumn)) {
|
||||
formatterRef.current = BooleanFormatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
.blobFormatter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:not(.nullValue) {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.blobFormatterValue {
|
||||
text-transform: uppercase;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nullValue {
|
||||
composes: nullValue from './CellNullValue.m.css';
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { useContext } from 'react';
|
||||
|
||||
import { getComputed, s, useS } from '@cloudbeaver/core-blocks';
|
||||
import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer';
|
||||
import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid';
|
||||
|
||||
import { EditingContext } from '../../../Editing/EditingContext';
|
||||
import { CellContext } from '../../CellRenderer/CellContext';
|
||||
import { DataGridContext } from '../../DataGridContext';
|
||||
import { TableDataContext } from '../../TableDataContext';
|
||||
import style from './BlobFormatter.m.css';
|
||||
|
||||
export const BlobFormatter = observer<RenderCellProps<IResultSetRowKey>>(function BlobFormatter({ column, row }) {
|
||||
const context = useContext(DataGridContext);
|
||||
const tableDataContext = useContext(TableDataContext);
|
||||
const editingContext = useContext(EditingContext);
|
||||
const cellContext = useContext(CellContext);
|
||||
const cell = cellContext.cell;
|
||||
|
||||
if (!context || !tableDataContext || !editingContext || !cell) {
|
||||
throw new Error('Contexts required');
|
||||
}
|
||||
|
||||
const styles = useS(style);
|
||||
|
||||
const formatter = tableDataContext.format;
|
||||
const rawValue = getComputed(() => formatter.get(cell));
|
||||
const displayString = getComputed(() => formatter.getDisplayString(cell));
|
||||
|
||||
const nullValue = rawValue === null;
|
||||
const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cell);
|
||||
const readonly = tableDataContext.isCellReadonly(cell);
|
||||
|
||||
return (
|
||||
<span className={s(styles, { blobFormatter: true, nullValue })} title={displayString}>
|
||||
<div className={s(style, { blobFormatterValue: true })}>{displayString}</div>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
+15
-16
@@ -5,11 +5,10 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { s, useS } from '@cloudbeaver/core-blocks';
|
||||
import { getComputed, s, useS } from '@cloudbeaver/core-blocks';
|
||||
import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer';
|
||||
import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid';
|
||||
|
||||
@@ -25,35 +24,35 @@ export const BooleanFormatter = observer<RenderCellProps<IResultSetRowKey>>(func
|
||||
const editingContext = useContext(EditingContext);
|
||||
const cellContext = useContext(CellContext);
|
||||
|
||||
if (!context || !tableDataContext || !editingContext || !cellContext.cell) {
|
||||
const cell = cellContext.cell;
|
||||
|
||||
if (!context || !tableDataContext || !editingContext || !cell) {
|
||||
throw new Error('Contexts required');
|
||||
}
|
||||
|
||||
const styles = useS(style);
|
||||
|
||||
const formatter = tableDataContext.format;
|
||||
const rawValue = useMemo(
|
||||
() => computed(() => formatter.get(tableDataContext.getCellValue(cellContext!.cell!)!)),
|
||||
[tableDataContext, cellContext.cell, formatter],
|
||||
).get();
|
||||
const value = typeof rawValue === 'string' ? rawValue.toLowerCase() === 'true' : rawValue;
|
||||
const stringifiedValue = formatter.toDisplayString(value);
|
||||
const valueRepresentation = value === null ? stringifiedValue : `[${value ? 'v' : ' '}]`;
|
||||
const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cellContext.cell);
|
||||
const value = getComputed(() => formatter.get(cell));
|
||||
const textValue = getComputed(() => formatter.getText(cell));
|
||||
const booleanValue = getComputed(() => textValue.toLowerCase() === 'true');
|
||||
const stringifiedValue = getComputed(() => formatter.getDisplayString(cell));
|
||||
const valueRepresentation = value === null ? stringifiedValue : `[${booleanValue ? 'v' : ' '}]`;
|
||||
const disabled = !column.editable || editingContext.readonly || formatter.isReadOnly(cell);
|
||||
|
||||
function toggleValue() {
|
||||
if (disabled || !tableDataContext || !cellContext.cell) {
|
||||
if (disabled || !tableDataContext || !cell) {
|
||||
return;
|
||||
}
|
||||
const resultColumn = tableDataContext.getColumnInfo(cellContext.cell.column);
|
||||
const resultColumn = tableDataContext.getColumnInfo(cell.column);
|
||||
|
||||
if (!resultColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = !resultColumn.required && value === false ? null : !value;
|
||||
const nextValue = !resultColumn.required && value === false ? null : !booleanValue;
|
||||
|
||||
tableDataContext.editor.set(cellContext.cell, nextValue);
|
||||
tableDataContext.editor.set(cell, nextValue);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
-1
@@ -27,7 +27,6 @@
|
||||
|
||||
.textFormatterValue {
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -31,12 +31,12 @@ export const TextFormatter = observer<RenderCellProps<IResultSetRowKey>>(functio
|
||||
|
||||
const style = useS(styles);
|
||||
const formatter = tableDataContext.format;
|
||||
const rawValue = getComputed(() => formatter.get(tableDataContext.getCellValue(cellContext.cell!)!));
|
||||
const rawValue = getComputed(() => formatter.get(cellContext.cell!));
|
||||
const textValue = formatter.getText(cellContext.cell!);
|
||||
const displayValue = formatter.getDisplayString(cellContext.cell!);
|
||||
|
||||
const classes = s(style, { textFormatter: true, nullValue: rawValue === null });
|
||||
|
||||
const value = formatter.toDisplayString(rawValue);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
editingContext.closeEditor(cellContext.position);
|
||||
}, [cellContext]);
|
||||
@@ -58,16 +58,14 @@ export const TextFormatter = observer<RenderCellProps<IResultSetRowKey>>(functio
|
||||
);
|
||||
}
|
||||
|
||||
const isUrl = typeof rawValue === 'string' && isValidUrl(rawValue);
|
||||
|
||||
return (
|
||||
<div title={value} className={classes}>
|
||||
{isUrl && (
|
||||
<a href={rawValue as string} target="_blank" rel="noreferrer" draggable={false} className={s(style, { a: true })}>
|
||||
<div title={displayValue} className={classes}>
|
||||
{isValidUrl(textValue) && (
|
||||
<a href={textValue} target="_blank" rel="noreferrer" draggable={false} className={s(style, { a: true })}>
|
||||
<IconOrImage icon="external-link" viewBox="0 0 24 24" className={s(style, { icon: true })} />
|
||||
</a>
|
||||
)}
|
||||
<div className={s(style, { textFormatterValue: true })}>{value}</div>
|
||||
<div className={s(style, { textFormatterValue: true })}>{displayValue}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@
|
||||
* 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 { useContext } from 'react';
|
||||
|
||||
import type { IResultSetRowKey } from '@cloudbeaver/plugin-data-viewer';
|
||||
@@ -12,8 +13,8 @@ import type { RenderCellProps } from '@cloudbeaver/plugin-react-data-grid';
|
||||
|
||||
import { CellContext } from '../CellRenderer/CellContext';
|
||||
|
||||
export const IndexFormatter: React.FC<RenderCellProps<IResultSetRowKey>> = function IndexFormatter(props) {
|
||||
export const IndexFormatter: React.FC<RenderCellProps<IResultSetRowKey>> = observer(function IndexFormatter(props) {
|
||||
const context = useContext(CellContext);
|
||||
|
||||
return <div>{context.position.rowIdx + 1}</div>;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
IResultSetValue,
|
||||
ResultSetConstraintAction,
|
||||
ResultSetDataAction,
|
||||
ResultSetDataContentAction,
|
||||
ResultSetEditAction,
|
||||
ResultSetFormatAction,
|
||||
ResultSetViewAction,
|
||||
@@ -37,6 +38,7 @@ interface IColumnMetrics {
|
||||
|
||||
export interface ITableData {
|
||||
format: ResultSetFormatAction;
|
||||
dataContent: ResultSetDataContentAction;
|
||||
data: ResultSetDataAction;
|
||||
editor: ResultSetEditAction;
|
||||
view: ResultSetViewAction;
|
||||
|
||||
+1
-3
@@ -20,9 +20,7 @@ const EVENT_KEY_CODE = {
|
||||
};
|
||||
|
||||
function getCellCopyValue(tableData: ITableData, key: IResultSetElementKey): string {
|
||||
const cell = tableData.getCellValue(key);
|
||||
const cellValue = cell !== undefined ? tableData.format.getText(cell) : undefined;
|
||||
return cellValue ?? '';
|
||||
return tableData.format.getText(key);
|
||||
}
|
||||
|
||||
function getSelectedCellsValue(tableData: ITableData, selectedCells: Map<string, IResultSetElementKey[]>) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
IResultSetRowKey,
|
||||
ResultSetConstraintAction,
|
||||
ResultSetDataAction,
|
||||
ResultSetDataContentAction,
|
||||
ResultSetDataKeysUtils,
|
||||
ResultSetEditAction,
|
||||
ResultSetFormatAction,
|
||||
@@ -58,6 +59,7 @@ export function useTableData(
|
||||
const data = model.source.getAction(resultIndex, ResultSetDataAction);
|
||||
const editor = model.source.getAction(resultIndex, ResultSetEditAction);
|
||||
const view = model.source.getAction(resultIndex, ResultSetViewAction);
|
||||
const dataContent = model.source.getAction(resultIndex, ResultSetDataContentAction);
|
||||
const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction);
|
||||
|
||||
return useObservableRef<ITableData & { gridDIVElement: React.RefObject<HTMLDivElement | null> }>(
|
||||
@@ -188,6 +190,7 @@ export function useTableData(
|
||||
rows: computed,
|
||||
columnKeys: computed,
|
||||
format: observable.ref,
|
||||
dataContent: observable.ref,
|
||||
data: observable.ref,
|
||||
editor: observable.ref,
|
||||
view: observable.ref,
|
||||
@@ -196,6 +199,7 @@ export function useTableData(
|
||||
},
|
||||
{
|
||||
format,
|
||||
dataContent,
|
||||
data,
|
||||
editor,
|
||||
view,
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
{
|
||||
"path": "../core-utils/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "../core-browser/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "../plugin-data-viewer/tsconfig.json"
|
||||
},
|
||||
|
||||
@@ -18,8 +18,10 @@ import {
|
||||
SqlQueryResults,
|
||||
UpdateResultsDataBatchMutationVariables,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { DocumentEditAction } from './DatabaseDataModel/Actions/Document/DocumentEditAction';
|
||||
import type { IResultSetBlobValue } from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue';
|
||||
import { ResultSetEditAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction';
|
||||
import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource';
|
||||
import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions';
|
||||
@@ -154,31 +156,42 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
continue;
|
||||
}
|
||||
const executionContextInfo = executionContext.context!;
|
||||
const projectId = executionContextInfo.projectId;
|
||||
const connectionId = executionContextInfo.connectionId;
|
||||
const contextId = executionContextInfo.id;
|
||||
const resultsId = result.id;
|
||||
|
||||
const updateVariables: UpdateResultsDataBatchMutationVariables = {
|
||||
projectId: executionContextInfo.projectId,
|
||||
connectionId: executionContextInfo.connectionId,
|
||||
contextId: executionContextInfo.id,
|
||||
resultsId: result.id,
|
||||
projectId,
|
||||
connectionId,
|
||||
contextId,
|
||||
resultsId,
|
||||
};
|
||||
let editor: ResultSetEditAction | DocumentEditAction | undefined;
|
||||
|
||||
if (result.dataFormat === ResultDataFormat.Resultset) {
|
||||
editor = this.actions.get(result, ResultSetEditAction);
|
||||
editor.fillBatch(updateVariables);
|
||||
} else if (result.dataFormat === ResultDataFormat.Document) {
|
||||
editor = this.actions.get(result, DocumentEditAction);
|
||||
}
|
||||
|
||||
let blobs: IResultSetBlobValue[] = [];
|
||||
if (editor instanceof ResultSetEditAction) {
|
||||
blobs = editor.getBlobsToUpload();
|
||||
}
|
||||
|
||||
for (const blob of blobs) {
|
||||
const fileId = uuid();
|
||||
await this.graphQLService.sdk.uploadBlobResultSet(fileId, blob.blob);
|
||||
blob.fileId = fileId;
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.fillBatch(updateVariables);
|
||||
}
|
||||
|
||||
const response = await this.graphQLService.sdk.updateResultsDataBatch(updateVariables);
|
||||
|
||||
this.requestInfo = {
|
||||
...this.requestInfo,
|
||||
requestDuration: response.result.duration,
|
||||
requestMessage: 'Saved successfully',
|
||||
source: null,
|
||||
};
|
||||
|
||||
if (editor) {
|
||||
const responseResult = this.transformResults(executionContextInfo, response.result.results, 0).find(
|
||||
newResult => newResult.id === result.id,
|
||||
@@ -188,7 +201,15 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
editor.applyUpdate(responseResult);
|
||||
}
|
||||
}
|
||||
|
||||
this.requestInfo = {
|
||||
...this.requestInfo,
|
||||
requestDuration: response.result.duration,
|
||||
requestMessage: 'Saved successfully',
|
||||
source: null,
|
||||
};
|
||||
}
|
||||
|
||||
this.clearError();
|
||||
} catch (exception: any) {
|
||||
this.error = exception;
|
||||
|
||||
@@ -49,6 +49,7 @@ export abstract class DatabaseEditAction<TKey, TValue, TResult extends IDatabase
|
||||
abstract add(key?: TKey): void;
|
||||
abstract duplicate(...key: TKey[]): void;
|
||||
abstract delete(...key: TKey[]): void;
|
||||
abstract applyPartialUpdate(result: TResult): void;
|
||||
abstract applyUpdate(result: TResult): void;
|
||||
abstract revert(...key: TKey[]): void;
|
||||
abstract clear(): void;
|
||||
|
||||
+13
@@ -121,6 +121,19 @@ export class DocumentEditAction extends DatabaseEditAction<IDocumentElementKey,
|
||||
);
|
||||
}
|
||||
|
||||
applyPartialUpdate(result: IDatabaseResultSet): void {
|
||||
let rowIndex = 0;
|
||||
|
||||
for (const [id, document] of this.editedElements) {
|
||||
const value = result.data?.rows?.[rowIndex];
|
||||
|
||||
if (value !== undefined) {
|
||||
this.data.set(id, value[0]);
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
applyUpdate(result: IDatabaseResultSet): void {
|
||||
let rowIndex = 0;
|
||||
|
||||
|
||||
+4
-3
@@ -12,9 +12,9 @@ import type { IDatabaseDataResult } from '../IDatabaseDataResult';
|
||||
|
||||
// order is matter, used for sorting and changes diff
|
||||
export enum DatabaseEditChangeType {
|
||||
update,
|
||||
add,
|
||||
delete,
|
||||
update = 0,
|
||||
add = 1,
|
||||
delete = 2,
|
||||
}
|
||||
|
||||
export interface IDatabaseDataEditActionValue<TKey, TValue> {
|
||||
@@ -54,6 +54,7 @@ export interface IDatabaseDataEditAction<TKey, TValue, TResult extends IDatabase
|
||||
add: (key?: TKey) => void;
|
||||
duplicate: (...key: TKey[]) => void;
|
||||
delete: (key: TKey) => void;
|
||||
applyPartialUpdate(result: TResult): void;
|
||||
applyUpdate: (result: TResult) => void;
|
||||
revert: (key: TKey) => void;
|
||||
clear: () => void;
|
||||
|
||||
+5
-4
@@ -10,8 +10,9 @@ import type { IDatabaseDataResult } from '../IDatabaseDataResult';
|
||||
|
||||
export interface IDatabaseDataFormatAction<TKey, TResult extends IDatabaseDataResult> extends IDatabaseDataAction<any, TResult> {
|
||||
isReadOnly: (key: TKey) => boolean;
|
||||
get: (value: any) => any;
|
||||
getText: (value: any) => string | null;
|
||||
isNull: (value: any) => boolean;
|
||||
toDisplayString: (value: any) => string;
|
||||
isNull: (key: TKey) => boolean;
|
||||
isBinary: (key: TKey) => boolean;
|
||||
get: (key: TKey) => any;
|
||||
getText: (key: TKey) => string;
|
||||
getDisplayString: (key: TKey) => string;
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetFileValue } from './IResultSetFileValue';
|
||||
|
||||
export interface IResultSetBlobValue extends IResultSetFileValue {
|
||||
blob: Blob;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface IResultSetComplexValue {
|
||||
$type: string;
|
||||
value?: any;
|
||||
}
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
|
||||
export interface IResultSetContentValue {
|
||||
export interface IResultSetContentValue extends IResultSetComplexValue {
|
||||
$type: 'content';
|
||||
binary?: string;
|
||||
text?: string;
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export interface IResultSetColumnKey {
|
||||
|
||||
export interface IResultSetRowKey {
|
||||
index: number;
|
||||
key?: string;
|
||||
subIndex: number;
|
||||
}
|
||||
|
||||
export interface IResultSetElementKey {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
|
||||
export interface IResultSetFileValue extends IResultSetComplexValue {
|
||||
$type: 'file';
|
||||
fileId: string | null;
|
||||
contentType?: string;
|
||||
contentLength?: number;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
|
||||
export interface IResultSetGeometryValue extends IResultSetComplexValue {
|
||||
$type: 'geometry';
|
||||
srid: number;
|
||||
text: string;
|
||||
mapText: string | null;
|
||||
properties: Record<string, any> | null;
|
||||
}
|
||||
+3
-2
@@ -43,6 +43,7 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
return {
|
||||
row: {
|
||||
index: 0,
|
||||
subIndex: 0,
|
||||
},
|
||||
column: {
|
||||
index: 0,
|
||||
@@ -55,7 +56,7 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
const index = row.index + shift;
|
||||
this.result.data.rows.splice(index, 0, value);
|
||||
|
||||
return { index };
|
||||
return { index, subIndex: 0 };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -66,7 +67,7 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
const index = row.index + shift;
|
||||
this.result.data.rows.splice(index, 1);
|
||||
|
||||
return { index: index - 1 };
|
||||
return { index: index - 1, subIndex: 0 };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
+7
-19
@@ -21,7 +21,7 @@ import type { IResultSetElementKey } from './IResultSetDataKey';
|
||||
import { isResultSetContentValue } from './isResultSetContentValue';
|
||||
import { ResultSetDataAction } from './ResultSetDataAction';
|
||||
import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils';
|
||||
import type { IResultSetValue } from './ResultSetFormatAction';
|
||||
import { IResultSetValue, ResultSetFormatAction } from './ResultSetFormatAction';
|
||||
import { ResultSetViewAction } from './ResultSetViewAction';
|
||||
|
||||
const RESULT_VALUE_PATH = 'sql-result-value';
|
||||
@@ -30,30 +30,19 @@ const RESULT_VALUE_PATH = 'sql-result-value';
|
||||
export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabaseResultSet> implements IResultSetDataContentAction {
|
||||
static dataFormat = [ResultDataFormat.Resultset];
|
||||
|
||||
private readonly view: ResultSetViewAction;
|
||||
private readonly data: ResultSetDataAction;
|
||||
|
||||
private readonly graphQLService: GraphQLService;
|
||||
private readonly quotasService: QuotasService;
|
||||
|
||||
private readonly cache: Map<string, string>;
|
||||
activeElement: IResultSetElementKey | null;
|
||||
|
||||
constructor(
|
||||
source: IDatabaseDataSource<any, IDatabaseResultSet>,
|
||||
view: ResultSetViewAction,
|
||||
data: ResultSetDataAction,
|
||||
graphQLService: GraphQLService,
|
||||
quotasService: QuotasService,
|
||||
private readonly view: ResultSetViewAction,
|
||||
private readonly data: ResultSetDataAction,
|
||||
private readonly format: ResultSetFormatAction,
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly quotasService: QuotasService,
|
||||
) {
|
||||
super(source);
|
||||
|
||||
this.view = view;
|
||||
this.data = data;
|
||||
|
||||
this.graphQLService = graphQLService;
|
||||
this.quotasService = quotasService;
|
||||
|
||||
this.cache = new Map();
|
||||
this.activeElement = null;
|
||||
|
||||
@@ -68,8 +57,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
|
||||
}
|
||||
|
||||
isDownloadable(element: IResultSetElementKey) {
|
||||
const cellValue = this.view.getCellValue(element);
|
||||
return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(cellValue);
|
||||
return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(this.format.get(element));
|
||||
}
|
||||
|
||||
async getFileDataUrl(element: IResultSetElementKey) {
|
||||
|
||||
+5
-5
@@ -17,8 +17,8 @@ export const ResultSetDataKeysUtils = {
|
||||
serialize(key: IResultSetColumnKey | IResultSetRowKey): string {
|
||||
let base = `${key.index}`;
|
||||
|
||||
if ('key' in key) {
|
||||
base += `_${key.key}`;
|
||||
if ('subIndex' in key) {
|
||||
base += `.${key.subIndex}`;
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -28,14 +28,14 @@ export const ResultSetDataKeysUtils = {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keyA = 'key' in a;
|
||||
const keyB = 'key' in b;
|
||||
const keyA = 'subIndex' in a;
|
||||
const keyB = 'subIndex' in b;
|
||||
|
||||
if (keyA !== keyB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyA && (a as IResultSetRowKey).key !== (b as IResultSetRowKey).key) {
|
||||
if (keyA && (a as IResultSetRowKey).subIndex !== (b as IResultSetRowKey).subIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+165
-77
@@ -5,11 +5,10 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { ResultDataFormat, SqlResultRow, UpdateResultsDataBatchMutationVariables } from '@cloudbeaver/core-sdk';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
|
||||
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
|
||||
@@ -22,8 +21,15 @@ import {
|
||||
IDatabaseDataEditApplyActionData,
|
||||
IDatabaseDataEditApplyActionUpdate,
|
||||
} from '../IDatabaseDataEditAction';
|
||||
import { compareResultSetRowKeys } from './compareResultSetRowKeys';
|
||||
import { createResultSetContentValue } from './createResultSetContentValue';
|
||||
import { createResultSetFileValue } from './createResultSetFileValue';
|
||||
import type { IResultSetBlobValue } from './IResultSetBlobValue';
|
||||
import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey';
|
||||
import { isResultSetBlobValue } from './isResultSetBlobValue';
|
||||
import { isResultSetComplexValue } from './isResultSetComplexValue';
|
||||
import { isResultSetContentValue } from './isResultSetContentValue';
|
||||
import { isResultSetFileValue } from './isResultSetFileValue';
|
||||
import { ResultSetDataAction } from './ResultSetDataAction';
|
||||
import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils';
|
||||
import type { IResultSetValue } from './ResultSetFormatAction';
|
||||
@@ -54,8 +60,6 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
|
||||
makeObservable<this, 'editorData'>(this, {
|
||||
editorData: observable,
|
||||
addRows: computed,
|
||||
updates: computed,
|
||||
set: action,
|
||||
add: action,
|
||||
addRow: action,
|
||||
@@ -63,6 +67,7 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
deleteRow: action,
|
||||
revert: action,
|
||||
applyUpdate: action,
|
||||
applyPartialUpdate: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,14 +80,6 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
get updates(): IResultSetUpdate[] {
|
||||
return Array.from(this.editorData.values()).sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
if (a.type === DatabaseEditChangeType.update) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.type === DatabaseEditChangeType.update) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.type - b.type;
|
||||
}
|
||||
|
||||
@@ -144,13 +141,13 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
|
||||
const prevValue = update.source?.[key.column.index] as any;
|
||||
|
||||
if (isResultSetContentValue(prevValue) && value !== null) {
|
||||
if (isResultSetContentValue(prevValue) && !isResultSetComplexValue(value)) {
|
||||
if ('text' in prevValue) {
|
||||
value = {
|
||||
...prevValue,
|
||||
value = createResultSetContentValue({
|
||||
text: String(value),
|
||||
contentLength: String(value).length,
|
||||
};
|
||||
contentType: prevValue.contentType ?? 'text/plain',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,15 +176,13 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
addRow(row?: IResultSetRowKey, value?: IResultSetValue[], column?: IResultSetColumnKey): void {
|
||||
if (!row) {
|
||||
row = this.data.getDefaultKey().row;
|
||||
} else if (!('key' in row)) {
|
||||
row = { ...row, index: row.index + 1 };
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
value = this.data.columns.map(() => null);
|
||||
}
|
||||
|
||||
row = { ...row, key: uuid() };
|
||||
row = this.getNextRowAdd(row);
|
||||
|
||||
if (!column) {
|
||||
column = this.data.getDefaultKey().column;
|
||||
@@ -279,6 +274,10 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
const serializedKey = ResultSetDataKeysUtils.serialize(key);
|
||||
const update = this.editorData.get(serializedKey);
|
||||
|
||||
if (key.subIndex !== 0 && !update) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update && update.type !== DatabaseEditChangeType.delete) {
|
||||
this.editorData.delete(serializedKey);
|
||||
}
|
||||
@@ -316,73 +315,70 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
}
|
||||
}
|
||||
|
||||
applyUpdate(result: IDatabaseResultSet): void {
|
||||
const applyUpdate: Array<IDatabaseDataEditApplyActionUpdate<IResultSetRowKey>> = [];
|
||||
let rowIndex = 0;
|
||||
let addShift = 0;
|
||||
let deleteShift = 0;
|
||||
|
||||
const insertedRows: IResultSetRowKey[] = [];
|
||||
|
||||
applyPartialUpdate(result: IDatabaseResultSet): void {
|
||||
if (result.data?.rows?.length !== this.updates.length) {
|
||||
console.warn('ResultSetEditAction: returned data differs from performed update');
|
||||
}
|
||||
|
||||
for (const update of this.updates) {
|
||||
switch (update.type) {
|
||||
const applyUpdate: Array<IDatabaseDataEditApplyActionUpdate<IResultSetRowKey>> = [];
|
||||
|
||||
const tempUpdates = this.updates
|
||||
.map((update, i) => ({
|
||||
rowIndex: update.type === DatabaseEditChangeType.delete ? -1 : i,
|
||||
update,
|
||||
}))
|
||||
.sort((a, b) => compareResultSetRowKeys(b.update.row, a.update.row));
|
||||
|
||||
let offset = tempUpdates.reduce((offset, { update }) => {
|
||||
if (update.type === DatabaseEditChangeType.add) {
|
||||
return offset + 1;
|
||||
}
|
||||
if (update.type === DatabaseEditChangeType.delete) {
|
||||
return offset - 1;
|
||||
}
|
||||
return offset;
|
||||
}, 0);
|
||||
|
||||
for (const update of tempUpdates) {
|
||||
const value = result.data?.rows?.[update.rowIndex];
|
||||
const row = update.update.row;
|
||||
const type = update.update.type;
|
||||
|
||||
switch (update.update.type) {
|
||||
case DatabaseEditChangeType.update: {
|
||||
const value = result.data?.rows?.[rowIndex];
|
||||
|
||||
if (value !== undefined) {
|
||||
this.data.setRowValue(update.row, value);
|
||||
applyUpdate.push({
|
||||
type: DatabaseEditChangeType.update,
|
||||
row: update.row,
|
||||
newRow: update.row,
|
||||
});
|
||||
if (value) {
|
||||
this.data.setRowValue(update.update.row, value);
|
||||
}
|
||||
|
||||
rowIndex++;
|
||||
applyResultToUpdate(update.update, value);
|
||||
this.shiftRow(update.update.row, offset);
|
||||
this.removeEmptyUpdate(update.update);
|
||||
break;
|
||||
}
|
||||
|
||||
case DatabaseEditChangeType.add: {
|
||||
const value = result.data?.rows?.[rowIndex];
|
||||
|
||||
if (value !== undefined) {
|
||||
const newRow = this.data.insertRow(update.row, value, addShift);
|
||||
|
||||
if (newRow) {
|
||||
applyUpdate.push({
|
||||
type: DatabaseEditChangeType.add,
|
||||
row: update.row,
|
||||
newRow,
|
||||
});
|
||||
}
|
||||
if (value) {
|
||||
this.data.insertRow(update.update.row, value, 1);
|
||||
}
|
||||
|
||||
insertedRows.push(update.row);
|
||||
rowIndex++;
|
||||
addShift++;
|
||||
applyResultToUpdate(update.update, value);
|
||||
this.shiftRow(update.update.row, offset);
|
||||
this.removeEmptyUpdate(update.update);
|
||||
offset--;
|
||||
break;
|
||||
}
|
||||
|
||||
case DatabaseEditChangeType.delete: {
|
||||
const insertShift = insertedRows.filter(row => row.index <= update.row.index).length;
|
||||
const newRow = this.data.removeRow(update.row, deleteShift + insertShift);
|
||||
|
||||
if (newRow) {
|
||||
applyUpdate.push({
|
||||
type: DatabaseEditChangeType.delete,
|
||||
row: update.row,
|
||||
newRow,
|
||||
});
|
||||
}
|
||||
|
||||
deleteShift--;
|
||||
this.revert({ row: update.update.row, column: { index: 0 } });
|
||||
this.data.removeRow(update.update.row);
|
||||
offset++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
applyUpdate.push({
|
||||
type,
|
||||
row,
|
||||
newRow: update.update.row,
|
||||
});
|
||||
}
|
||||
|
||||
if (applyUpdate.length > 0) {
|
||||
@@ -391,6 +387,10 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
updates: applyUpdate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyUpdate(result: IDatabaseResultSet): void {
|
||||
this.applyPartialUpdate(result);
|
||||
|
||||
this.clear();
|
||||
}
|
||||
@@ -457,13 +457,23 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.editorData.clear();
|
||||
getBlobsToUpload(): Array<IResultSetBlobValue> {
|
||||
const blobs: Array<IResultSetBlobValue> = [];
|
||||
|
||||
this.action.execute({
|
||||
resultId: this.result.id,
|
||||
revert: true,
|
||||
});
|
||||
for (const update of this.updates) {
|
||||
if (update.type === DatabaseEditChangeType.delete) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < update.update.length; i++) {
|
||||
const value = update.update[i];
|
||||
if (isResultSetBlobValue(value) && value.fileId === null) {
|
||||
blobs.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blobs;
|
||||
}
|
||||
|
||||
fillBatch(batch: UpdateResultsDataBatchMutationVariables): void {
|
||||
@@ -479,7 +489,11 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
updatedRows.push({
|
||||
data: update.source,
|
||||
updateValues: update.update.reduce<Record<number, IResultSetValue>>((obj, value, index) => {
|
||||
if (value !== update.source![index]) {
|
||||
if (isResultSetBlobValue(value)) {
|
||||
if (value.fileId !== null) {
|
||||
obj[index] = createResultSetFileValue(value.fileId, value.contentType, value.contentLength);
|
||||
}
|
||||
} else if (value !== update.source![index]) {
|
||||
obj[index] = value;
|
||||
}
|
||||
return obj;
|
||||
@@ -495,7 +509,7 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
}
|
||||
const addedRows = batch.addedRows as SqlResultRow[];
|
||||
|
||||
addedRows.push({ data: update.update });
|
||||
addedRows.push({ data: replaceUploadBlobs(update.update) });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -505,7 +519,7 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
}
|
||||
const deletedRows = batch.deletedRows as SqlResultRow[];
|
||||
|
||||
deletedRows.push({ data: update.update });
|
||||
deletedRows.push({ data: replaceBlobsWithNull(update.update) });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -520,6 +534,38 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.editorData.clear();
|
||||
|
||||
this.action.execute({
|
||||
resultId: this.result.id,
|
||||
revert: true,
|
||||
});
|
||||
}
|
||||
|
||||
private getNextRowAdd(row: IResultSetRowKey): IResultSetRowKey {
|
||||
let i = row.subIndex + 1;
|
||||
while (this.editorData.has(ResultSetDataKeysUtils.serialize({ ...row, subIndex: i }))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return { ...row, subIndex: i };
|
||||
}
|
||||
|
||||
private shiftRow(row: IResultSetRowKey, shift: number) {
|
||||
const key = ResultSetDataKeysUtils.serialize(row);
|
||||
const update = this.editorData.get(ResultSetDataKeysUtils.serialize(row));
|
||||
|
||||
if (update) {
|
||||
update.row = {
|
||||
index: update.row.index + shift,
|
||||
subIndex: 0,
|
||||
};
|
||||
this.editorData.delete(key);
|
||||
this.editorData.set(ResultSetDataKeysUtils.serialize(update.row), update);
|
||||
}
|
||||
}
|
||||
|
||||
private removeEmptyUpdate(update: IResultSetUpdate) {
|
||||
if (update.type === DatabaseEditChangeType.add) {
|
||||
return;
|
||||
@@ -576,3 +622,45 @@ export class ResultSetEditAction extends DatabaseEditAction<IResultSetElementKey
|
||||
return valueA === valueB;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBlobsWithNull(values: IResultSetValue[]) {
|
||||
return values.map(value => {
|
||||
if (isResultSetBlobValue(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function replaceUploadBlobs(values: IResultSetValue[]) {
|
||||
return values.map(value => {
|
||||
if (isResultSetBlobValue(value)) {
|
||||
if (value.fileId !== null) {
|
||||
return createResultSetFileValue(value.fileId, value.contentType, value.contentLength);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function applyResultToUpdate(update: IResultSetUpdate, result?: IResultSetValue[]): void {
|
||||
if (result) {
|
||||
update.source = result;
|
||||
|
||||
update.update = update.update.map((value, i) => {
|
||||
const source = update.source![i];
|
||||
if (isResultSetContentValue(source) && isResultSetFileValue(value)) {
|
||||
if (value.fileId && value.contentLength === source.contentLength) {
|
||||
return JSON.parse(JSON.stringify(source));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
if (update.type === DatabaseEditChangeType.add) {
|
||||
update.type = DatabaseEditChangeType.update;
|
||||
}
|
||||
}
|
||||
|
||||
+140
-49
@@ -6,7 +6,6 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { removeLineBreak } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { DatabaseDataAction } from '../../DatabaseDataAction';
|
||||
import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
|
||||
@@ -14,12 +13,25 @@ import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
|
||||
import { databaseDataAction } from '../DatabaseDataActionDecorator';
|
||||
import { DatabaseEditChangeType } from '../IDatabaseDataEditAction';
|
||||
import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction';
|
||||
import type { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
import type { IResultSetElementKey, IResultSetPartialKey } from './IResultSetDataKey';
|
||||
import { isResultSetBlobValue } from './isResultSetBlobValue';
|
||||
import { isResultSetComplexValue } from './isResultSetComplexValue';
|
||||
import { isResultSetContentValue } from './isResultSetContentValue';
|
||||
import { isResultSetFileValue } from './isResultSetFileValue';
|
||||
import { isResultSetGeometryValue } from './isResultSetGeometryValue';
|
||||
import { ResultSetEditAction } from './ResultSetEditAction';
|
||||
import { ResultSetViewAction } from './ResultSetViewAction';
|
||||
|
||||
export type IResultSetValue = string | number | boolean | Record<string, string | number | Record<string, any> | null> | null;
|
||||
export type IResultSetValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Record<string, string | number | Record<string, any> | null>
|
||||
| IResultSetComplexValue
|
||||
| null;
|
||||
|
||||
const DISPLAY_STRING_LENGTH = 200;
|
||||
|
||||
@databaseDataAction()
|
||||
export class ResultSetFormatAction
|
||||
@@ -37,29 +49,6 @@ export class ResultSetFormatAction
|
||||
this.edit = edit;
|
||||
}
|
||||
|
||||
getHeaders(): string[] {
|
||||
return this.view.columns.map(column => column.name!).filter(name => name !== undefined);
|
||||
}
|
||||
|
||||
getLongestCells(offset = 0, count?: number): string[] {
|
||||
const rows = this.view.rows.slice(offset, count);
|
||||
const cells: string[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const value = this.toDisplayString(row[i]);
|
||||
const columnIndex = this.view.columnIndex({ index: i });
|
||||
const current = cells[columnIndex] ?? '';
|
||||
|
||||
if (value.length > current.length) {
|
||||
cells[columnIndex] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
isReadOnly(key: IResultSetPartialKey): boolean {
|
||||
let readonly = false;
|
||||
|
||||
@@ -85,26 +74,99 @@ export class ResultSetFormatAction
|
||||
|
||||
return readonly;
|
||||
}
|
||||
|
||||
isNull(value: IResultSetValue): boolean {
|
||||
return this.get(value) === null;
|
||||
isNull(key: IResultSetElementKey): boolean {
|
||||
return this.get(key) === null;
|
||||
}
|
||||
|
||||
get(value: IResultSetValue): IResultSetValue {
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if ('text' in value) {
|
||||
return value.text;
|
||||
} else if ('value' in value) {
|
||||
return value.value;
|
||||
}
|
||||
return value;
|
||||
isBinary(key: IResultSetPartialKey): boolean {
|
||||
if (!key.column) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value;
|
||||
const column = this.view.getColumn(key.column);
|
||||
if (column?.dataKind?.toLocaleLowerCase() === 'binary') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.row) {
|
||||
const value = this.get(key as IResultSetElementKey);
|
||||
|
||||
if (isResultSetFileValue(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isResultSetContentValue(value)) {
|
||||
return value.binary !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getText(value: IResultSetValue): string | null {
|
||||
value = this.get(value);
|
||||
getHeaders(): string[] {
|
||||
return this.view.columns.map(column => column.name!).filter(name => name !== undefined);
|
||||
}
|
||||
|
||||
getLongestCells(offset = 0, count?: number): string[] {
|
||||
const cells: string[] = [];
|
||||
const columnsCount = this.view.columnKeys.length;
|
||||
count ??= this.view.rowKeys.length;
|
||||
|
||||
for (let rowIndex = offset; rowIndex < offset + count; rowIndex++) {
|
||||
for (let columnIndex = 0; columnIndex < columnsCount; columnIndex++) {
|
||||
const key = { row: this.view.rowKeys[rowIndex], column: this.view.columnKeys[columnIndex] };
|
||||
const displayString = this.getDisplayString(key);
|
||||
const current = cells[columnIndex] ?? '';
|
||||
|
||||
if (displayString.length > current.length) {
|
||||
cells[columnIndex] = displayString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
get(key: IResultSetElementKey): IResultSetValue {
|
||||
return this.view.getCellValue(key);
|
||||
}
|
||||
|
||||
getText(key: IResultSetElementKey): string {
|
||||
const value = this.get(key);
|
||||
|
||||
if (value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isResultSetContentValue(value)) {
|
||||
if (value.text !== undefined) {
|
||||
return value.text;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isResultSetGeometryValue(value)) {
|
||||
if (value.text !== undefined) {
|
||||
return value.text;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isResultSetComplexValue(value)) {
|
||||
if (value.value !== undefined) {
|
||||
if (typeof value.value === 'object' && value.value !== null) {
|
||||
return JSON.stringify(value.value);
|
||||
}
|
||||
return String(value.value);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.isBinary(key)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
@@ -117,22 +179,51 @@ export class ResultSetFormatAction
|
||||
return value;
|
||||
}
|
||||
|
||||
toDisplayString(value: IResultSetValue): string {
|
||||
value = this.getText(value);
|
||||
getDisplayString(key: IResultSetElementKey): string {
|
||||
const value = this.get(key);
|
||||
|
||||
if (value === null) {
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.length > 1000) {
|
||||
return removeLineBreak(
|
||||
value
|
||||
.split('')
|
||||
.map(v => (v.charCodeAt(0) < 32 ? ' ' : v))
|
||||
.join(''),
|
||||
);
|
||||
if (isResultSetGeometryValue(value)) {
|
||||
if (value.text !== undefined) {
|
||||
return this.truncateText(String(value.text), DISPLAY_STRING_LENGTH);
|
||||
}
|
||||
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
return removeLineBreak(String(value));
|
||||
if (this.isBinary(key)) {
|
||||
return '[blob]';
|
||||
}
|
||||
|
||||
if (isResultSetContentValue(value)) {
|
||||
if (value.text !== undefined) {
|
||||
return this.truncateText(String(value.text), DISPLAY_STRING_LENGTH);
|
||||
}
|
||||
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
if (isResultSetComplexValue(value)) {
|
||||
if (value.value !== undefined) {
|
||||
if (typeof value.value === 'object' && value.value !== null) {
|
||||
return JSON.stringify(value.value);
|
||||
}
|
||||
return String(value.value);
|
||||
}
|
||||
return '[null]';
|
||||
}
|
||||
|
||||
return this.truncateText(String(value), DISPLAY_STRING_LENGTH);
|
||||
}
|
||||
|
||||
truncateText(text: string, length: number): string {
|
||||
return text
|
||||
.slice(0, length)
|
||||
.split('')
|
||||
.map(v => (v.charCodeAt(0) < 32 ? ' ' : v))
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -267,7 +267,11 @@ export class ResultSetSelectAction extends DatabaseSelectAction<any, IDatabaseRe
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusedElement = toJS(key);
|
||||
if (key) {
|
||||
key = JSON.parse(JSON.stringify(toJS(key)));
|
||||
}
|
||||
|
||||
this.focusedElement = key;
|
||||
this.actions.execute({
|
||||
type: 'focus',
|
||||
resultId: this.result.id,
|
||||
|
||||
+7
-10
@@ -5,7 +5,7 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { DataTypeLogicalOperation, ResultDataFormat, SqlResultColumn } from '@cloudbeaver/core-sdk';
|
||||
|
||||
@@ -14,7 +14,8 @@ import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
|
||||
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
|
||||
import { databaseDataAction } from '../DatabaseDataActionDecorator';
|
||||
import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction';
|
||||
import type { IResultSetContentValue } from './IResultSetContentValue';
|
||||
import { compareResultSetRowKeys } from './compareResultSetRowKeys';
|
||||
import type { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey';
|
||||
import { isResultSetContentValue } from './isResultSetContentValue';
|
||||
import { ResultSetDataAction } from './ResultSetDataAction';
|
||||
@@ -27,7 +28,7 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
static dataFormat = [ResultDataFormat.Resultset];
|
||||
|
||||
get rowKeys(): IResultSetRowKey[] {
|
||||
return [...this.editor.addRows, ...this.data.rows.map((c, index) => ({ index }))].sort((a, b) => a.index - b.index);
|
||||
return [...this.editor.addRows, ...this.data.rows.map((c, index) => ({ index, subIndex: 0 }))].sort(compareResultSetRowKeys);
|
||||
}
|
||||
|
||||
get columnKeys(): IResultSetColumnKey[] {
|
||||
@@ -52,10 +53,6 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
this.editor = editor;
|
||||
|
||||
makeObservable<this, 'columnsOrder'>(this, {
|
||||
rowKeys: computed,
|
||||
columnKeys: computed,
|
||||
rows: computed,
|
||||
columns: computed,
|
||||
columnsOrder: observable,
|
||||
setColumnOrder: action,
|
||||
});
|
||||
@@ -129,7 +126,7 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
return { row, column };
|
||||
}
|
||||
|
||||
getCellValue(cell: IResultSetElementKey): IResultSetValue | undefined {
|
||||
getCellValue(cell: IResultSetElementKey): IResultSetValue {
|
||||
const edited = this.editor.get(cell);
|
||||
|
||||
if (edited !== undefined) {
|
||||
@@ -137,13 +134,13 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
}
|
||||
|
||||
if (cell.row.index >= this.rows.length || cell.column.index >= this.columns.length) {
|
||||
return undefined;
|
||||
throw new Error('Cell is out of range');
|
||||
}
|
||||
|
||||
return this.rows[cell.row.index][cell.column.index];
|
||||
}
|
||||
|
||||
getContent(cell: IResultSetElementKey): IResultSetContentValue | null {
|
||||
getContent(cell: IResultSetElementKey): IResultSetComplexValue | null {
|
||||
const value = this.getCellValue(cell);
|
||||
|
||||
if (isResultSetContentValue(value)) {
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetRowKey } from './IResultSetDataKey';
|
||||
|
||||
export function compareResultSetRowKeys(a: IResultSetRowKey, b: IResultSetRowKey): number {
|
||||
// subIndex is used to sort rows with the same index
|
||||
return a.index + a.subIndex / 10 - b.index - b.subIndex / 10;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { createResultSetFileValue } from './createResultSetFileValue';
|
||||
import type { IResultSetBlobValue } from './IResultSetBlobValue';
|
||||
|
||||
export function createResultSetBlobValue(blob: Blob, fileId?: string): IResultSetBlobValue {
|
||||
return {
|
||||
...createResultSetFileValue(fileId ?? null, blob.type, blob.size),
|
||||
blob,
|
||||
};
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetContentValue } from './IResultSetContentValue';
|
||||
|
||||
export function createResultSetContentValue(data: Omit<IResultSetContentValue, '$type'>): IResultSetContentValue {
|
||||
return {
|
||||
$type: 'content',
|
||||
...data,
|
||||
};
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetFileValue } from './IResultSetFileValue';
|
||||
|
||||
export function createResultSetFileValue(fileId: string | null, contentType?: string, contentLength?: number): IResultSetFileValue {
|
||||
return {
|
||||
$type: 'file',
|
||||
fileId,
|
||||
contentType,
|
||||
contentLength,
|
||||
};
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetBlobValue } from './IResultSetBlobValue';
|
||||
import { isResultSetFileValue } from './isResultSetFileValue';
|
||||
|
||||
export function isResultSetBlobValue(value: any): value is IResultSetBlobValue {
|
||||
return isResultSetFileValue(value) && 'blob' in value && value.blob instanceof Blob;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetComplexValue } from './IResultSetComplexValue';
|
||||
|
||||
export function isResultSetComplexValue(value: any): value is IResultSetComplexValue {
|
||||
return value !== null && typeof value === 'object' && '$type' in value && typeof value.$type === 'string';
|
||||
}
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { IResultSetContentValue } from './IResultSetContentValue';
|
||||
import { isResultSetComplexValue } from './isResultSetComplexValue';
|
||||
|
||||
export function isResultSetContentValue(value: any): value is IResultSetContentValue {
|
||||
return value !== null && typeof value === 'object' && '$type' in value && value.$type === 'content';
|
||||
return isResultSetComplexValue(value) && value.$type === 'content';
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetFileValue } from './IResultSetFileValue';
|
||||
import { isResultSetComplexValue } from './isResultSetComplexValue';
|
||||
|
||||
export function isResultSetFileValue(value: any): value is IResultSetFileValue {
|
||||
return isResultSetComplexValue(value) && value.$type === 'file';
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { IResultSetGeometryValue } from './IResultSetGeometryValue';
|
||||
import { isResultSetComplexValue } from './isResultSetComplexValue';
|
||||
|
||||
export function isResultSetGeometryValue(value: any): value is IResultSetGeometryValue {
|
||||
return isResultSetComplexValue(value) && value.$type === 'geometry';
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { action, makeObservable, runInAction } from 'mobx';
|
||||
|
||||
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { getDependingDataActions } from './Actions/DatabaseDataActionDecorator';
|
||||
@@ -40,13 +41,13 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
}
|
||||
|
||||
get<T extends IDatabaseDataAction<TOptions, TResult>>(result: TResult, Action: IDatabaseDataActionClass<TOptions, TResult, T>): T {
|
||||
if (Action.dataFormat && !Action.dataFormat.includes(result.dataFormat)) {
|
||||
if (!isActionSupportsFormat(Action, result.dataFormat)) {
|
||||
throw new Error('DataFormat unsupported');
|
||||
}
|
||||
|
||||
const actions = this.actions.get(result.uniqueResultId);
|
||||
|
||||
let action = actions.find(action => action instanceof Action);
|
||||
let action = actions.find(action => action instanceof Action && isActionSupportsFormat(action, result.dataFormat));
|
||||
|
||||
if (!action) {
|
||||
runInAction(() => {
|
||||
@@ -56,7 +57,7 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
|
||||
for (const dependency of allDeps) {
|
||||
if (isDatabaseDataAction(dependency)) {
|
||||
depends.push(this.get<IDatabaseDataAction<TOptions, TResult>>(result, dependency));
|
||||
depends.push(this.get(result, dependency));
|
||||
} else {
|
||||
depends.push(this.source.serviceInjector.getServiceByClass(dependency as any));
|
||||
}
|
||||
@@ -68,7 +69,7 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
|
||||
action = new Action(this.source, ...depends);
|
||||
action.updateResult(result, this.source.results.indexOf(result));
|
||||
this.actions.set(result.uniqueResultId, [...actions, action]);
|
||||
this.actions.set(result.uniqueResultId, [...this.actions.get(result.uniqueResultId), action]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
Action: IDatabaseDataActionInterface<TOptions, TResult, T>,
|
||||
): T | undefined {
|
||||
const actions = this.actions.get(result.uniqueResultId);
|
||||
const action = actions?.find(action => action instanceof Action);
|
||||
const action = actions?.find(action => action instanceof Action && isActionSupportsFormat(action, result.dataFormat));
|
||||
|
||||
return action as T | undefined;
|
||||
}
|
||||
@@ -92,11 +93,6 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
const result = results.find(result => result.uniqueResultId === key);
|
||||
|
||||
for (const action of actions) {
|
||||
if (!(action.constructor as any).dataFormat.includes(result?.dataFormat)) {
|
||||
this.actions.delete(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
action.updateResults(results);
|
||||
|
||||
if (!result) {
|
||||
@@ -120,3 +116,14 @@ export class DatabaseDataActions<TOptions, TResult extends IDatabaseDataResult>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isActionSupportsFormat<TOptions, TResult extends IDatabaseDataResult>(
|
||||
action: IDatabaseDataActionClass<TOptions, TResult, IDatabaseDataAction<TOptions, TResult>> | IDatabaseDataAction<TOptions, TResult>,
|
||||
format: ResultDataFormat,
|
||||
): boolean {
|
||||
if ('dataFormat' in action) {
|
||||
return !action.dataFormat || action.dataFormat.includes(format);
|
||||
}
|
||||
const constructor = action.constructor as IDatabaseDataActionClass<TOptions, TResult, IDatabaseDataAction<TOptions, TResult>>;
|
||||
return !constructor.dataFormat || constructor.dataFormat.includes(format);
|
||||
}
|
||||
|
||||
+1
-1
@@ -230,7 +230,7 @@ export class TableFooterMenuService {
|
||||
|
||||
return !editor?.isEdited();
|
||||
},
|
||||
onClick: context => context.data.model.save(),
|
||||
onClick: context => context.data.model.save().catch(() => {}),
|
||||
});
|
||||
|
||||
this.registerMenuItem({
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
.img {
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
&:not(.stretch) {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
+124
-91
@@ -7,85 +7,76 @@
|
||||
*/
|
||||
import { action, computed, observable } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { Button, IconOrImage, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { ActionIconButtonStyles, Button, Container, Fill, IconButton, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { selectFiles } from '@cloudbeaver/core-browser';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { QuotasService } from '@cloudbeaver/core-root';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { bytesToSize, download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue';
|
||||
import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue';
|
||||
import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue';
|
||||
import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
|
||||
import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue';
|
||||
import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction';
|
||||
import { ResultSetDataKeysUtils } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils';
|
||||
import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction';
|
||||
import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction';
|
||||
import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction';
|
||||
import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction';
|
||||
import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet';
|
||||
import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService';
|
||||
import { QuotaPlaceholder } from '../QuotaPlaceholder';
|
||||
import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES';
|
||||
|
||||
const styles = css`
|
||||
img {
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
&[|stretch] {
|
||||
margin: unset;
|
||||
}
|
||||
}
|
||||
|
||||
container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
image {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
import styles from './ImageValuePresentation.m.css';
|
||||
|
||||
interface IToolsProps {
|
||||
loading?: boolean;
|
||||
stretch?: boolean;
|
||||
onToggleStretch?: () => void;
|
||||
onSave?: () => void;
|
||||
onUpload?: () => void;
|
||||
}
|
||||
|
||||
const Tools = observer<IToolsProps>(function Tools({ loading, stretch, onToggleStretch, onSave }) {
|
||||
const Tools = observer<IToolsProps>(function Tools({ loading, stretch, onToggleStretch, onSave, onUpload }) {
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(VALUE_PANEL_TOOLS_STYLES)(
|
||||
<tools-container>
|
||||
{onSave && (
|
||||
<Button disabled={loading} onClick={onSave}>
|
||||
{translate('ui_download')}
|
||||
</Button>
|
||||
)}
|
||||
return (
|
||||
<Container gap dense keepSize>
|
||||
<Container keepSize flexStart center>
|
||||
{onSave && (
|
||||
<IconButton
|
||||
title={translate('ui_download')}
|
||||
className={ActionIconButtonStyles.actionIconButton}
|
||||
name="/icons/export.svg"
|
||||
disabled={loading}
|
||||
img
|
||||
onClick={onSave}
|
||||
/>
|
||||
)}
|
||||
{onUpload && (
|
||||
<IconButton
|
||||
title={translate('ui_upload')}
|
||||
className={ActionIconButtonStyles.actionIconButton}
|
||||
name="/icons/import.svg"
|
||||
disabled={loading}
|
||||
img
|
||||
onClick={onUpload}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
<Fill />
|
||||
{onToggleStretch && (
|
||||
<tools>
|
||||
<tools-action as="button" title={translate('data_viewer_presentation_value_image_fit')} disabled={stretch} onClick={onToggleStretch}>
|
||||
<IconOrImage icon="img-fit-size" />
|
||||
</tools-action>
|
||||
<tools-action
|
||||
as="button"
|
||||
title={translate('data_viewer_presentation_value_image_original_size')}
|
||||
disabled={!stretch}
|
||||
<Container keepSize flexEnd center>
|
||||
<IconButton
|
||||
title={translate(stretch ? 'data_viewer_presentation_value_image_original_size' : 'data_viewer_presentation_value_image_fit')}
|
||||
className={ActionIconButtonStyles.actionIconButton}
|
||||
name={stretch ? 'img-original-size' : 'img-fit-size'}
|
||||
onClick={onToggleStretch}
|
||||
>
|
||||
<IconOrImage icon="img-original-size" />
|
||||
</tools-action>
|
||||
</tools>
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</tools-container>,
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,29 +85,39 @@ export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelP
|
||||
const translate = useTranslate();
|
||||
const notificationService = useService(NotificationService);
|
||||
const quotasService = useService(QuotasService);
|
||||
const style = useStyles(styles);
|
||||
|
||||
const content = model.source.getAction(resultIndex, ResultSetDataContentAction);
|
||||
const style = useS(styles);
|
||||
|
||||
const state = useObservableRef(
|
||||
() => ({
|
||||
get editAction(): ResultSetEditAction {
|
||||
return this.model.source.getAction(this.resultIndex, ResultSetEditAction);
|
||||
},
|
||||
get contentAction(): ResultSetDataContentAction {
|
||||
return this.model.source.getAction(this.resultIndex, ResultSetDataContentAction);
|
||||
},
|
||||
get selectAction(): ResultSetSelectAction {
|
||||
return this.model.source.getAction(this.resultIndex, ResultSetSelectAction);
|
||||
},
|
||||
get formatAction(): ResultSetFormatAction {
|
||||
return this.model.source.getAction(this.resultIndex, ResultSetFormatAction);
|
||||
},
|
||||
get selectedCell() {
|
||||
const selection = this.model.source.getAction(this.resultIndex, ResultSetSelectAction);
|
||||
const focusCell = selection.getFocusedElement();
|
||||
const focusCell = this.selectAction.getFocusedElement();
|
||||
|
||||
return selection.elements[0] || focusCell;
|
||||
return this.selectAction.elements[0] || focusCell;
|
||||
},
|
||||
get cellValue() {
|
||||
const view = this.model.source.getAction(this.resultIndex, ResultSetViewAction);
|
||||
const cellValue = view.getCellValue(this.selectedCell);
|
||||
|
||||
return cellValue;
|
||||
return this.formatAction.get(this.selectedCell);
|
||||
},
|
||||
get src() {
|
||||
if (this.savedSrc) {
|
||||
return this.savedSrc;
|
||||
}
|
||||
|
||||
if (isResultSetBlobValue(this.cellValue)) {
|
||||
return URL.createObjectURL(this.cellValue.blob);
|
||||
}
|
||||
|
||||
if (isResultSetContentValue(this.cellValue) && this.cellValue.binary) {
|
||||
return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`;
|
||||
} else if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) {
|
||||
@@ -126,17 +127,28 @@ export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelP
|
||||
return '';
|
||||
},
|
||||
get savedSrc() {
|
||||
return content.retrieveFileDataUrlFromCache(this.selectedCell);
|
||||
return this.contentAction.retrieveFileDataUrlFromCache(this.selectedCell);
|
||||
},
|
||||
get canSave() {
|
||||
if (this.truncated) {
|
||||
return content.isDownloadable(this.selectedCell);
|
||||
return this.contentAction.isDownloadable(this.selectedCell);
|
||||
}
|
||||
|
||||
return !!this.src;
|
||||
},
|
||||
get canUpload() {
|
||||
return this.formatAction.isBinary(this.selectedCell);
|
||||
},
|
||||
get truncated() {
|
||||
return isResultSetContentValue(this.cellValue) && content.isContentTruncated(this.cellValue);
|
||||
if (isResultSetFileValue(this.cellValue)) {
|
||||
return false;
|
||||
}
|
||||
if (isResultSetContentValue(this.cellValue)) {
|
||||
if (this.cellValue.binary) {
|
||||
return this.contentAction.isContentTruncated(this.cellValue);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
stretch: false,
|
||||
toggleStretch() {
|
||||
@@ -145,7 +157,7 @@ export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelP
|
||||
async save() {
|
||||
try {
|
||||
if (this.truncated) {
|
||||
await content.downloadFileData(this.selectedCell);
|
||||
await this.contentAction.downloadFileData(this.selectedCell);
|
||||
} else {
|
||||
download(this.src, '', true);
|
||||
}
|
||||
@@ -153,10 +165,23 @@ export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelP
|
||||
this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error');
|
||||
}
|
||||
},
|
||||
async upload() {
|
||||
selectFiles(files => {
|
||||
const file = files?.item(0) ?? undefined;
|
||||
if (file) {
|
||||
this.editAction.set(this.selectedCell, createResultSetBlobValue(file));
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
editAction: computed,
|
||||
contentAction: computed,
|
||||
selectAction: computed,
|
||||
formatAction: computed,
|
||||
selectedCell: computed,
|
||||
cellValue: computed,
|
||||
canUpload: computed,
|
||||
src: computed,
|
||||
savedSrc: computed,
|
||||
canSave: computed,
|
||||
@@ -166,50 +191,58 @@ export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelP
|
||||
resultIndex: observable.ref,
|
||||
toggleStretch: action.bound,
|
||||
save: action.bound,
|
||||
upload: action.bound,
|
||||
},
|
||||
{ model, resultIndex, notificationService },
|
||||
);
|
||||
|
||||
const save = state.canSave ? state.save : undefined;
|
||||
const upload = state.canUpload ? state.upload : undefined;
|
||||
const loading = model.isLoading();
|
||||
const value = state.cellValue;
|
||||
|
||||
if (state.truncated && !state.savedSrc) {
|
||||
if (state.truncated && !state.savedSrc && isResultSetContentValue(value)) {
|
||||
const limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength'));
|
||||
const valueSize = bytesToSize((state.cellValue as unknown as IResultSetContentValue).contentLength ?? 0);
|
||||
const valueSize = bytesToSize(value.contentLength ?? 0);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
await content.resolveFileDataUrl(state.selectedCell);
|
||||
await state.contentAction.resolveFileDataUrl(state.selectedCell);
|
||||
} catch (exception: any) {
|
||||
notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error');
|
||||
}
|
||||
};
|
||||
|
||||
return styled(style)(
|
||||
<container>
|
||||
<QuotaPlaceholder limit={limit} size={valueSize}>
|
||||
{content.isDownloadable(state.selectedCell) && (
|
||||
<Button
|
||||
disabled={loading}
|
||||
loading={!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(content.activeElement, state.selectedCell)}
|
||||
onClick={load}
|
||||
>
|
||||
{translate('ui_view')}
|
||||
</Button>
|
||||
)}
|
||||
</QuotaPlaceholder>
|
||||
<Tools loading={loading} onSave={save} />
|
||||
</container>,
|
||||
return (
|
||||
<Container vertical>
|
||||
<Container fill overflow center>
|
||||
<QuotaPlaceholder limit={limit} size={valueSize}>
|
||||
{state.contentAction.isDownloadable(state.selectedCell) && (
|
||||
<Button
|
||||
disabled={loading}
|
||||
loading={
|
||||
!!state.contentAction.activeElement &&
|
||||
ResultSetDataKeysUtils.isElementsKeyEqual(state.contentAction.activeElement, state.selectedCell)
|
||||
}
|
||||
onClick={load}
|
||||
>
|
||||
{translate('ui_view')}
|
||||
</Button>
|
||||
)}
|
||||
</QuotaPlaceholder>
|
||||
</Container>
|
||||
<Tools loading={loading} onSave={save} onUpload={upload} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<container>
|
||||
<image>
|
||||
<img src={state.src} {...use({ stretch: state.stretch })} />
|
||||
</image>
|
||||
<Tools loading={loading} stretch={state.stretch} onToggleStretch={state.toggleStretch} onSave={save} />
|
||||
</container>,
|
||||
return (
|
||||
<Container vertical>
|
||||
<Container fill overflow center>
|
||||
<img src={state.src} className={s(style, { img: true, stretch: state.stretch })} />
|
||||
</Container>
|
||||
<Tools loading={loading} stretch={state.stretch} onToggleStretch={state.toggleStretch} onSave={save} onUpload={upload} />
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+7
-8
@@ -9,7 +9,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue';
|
||||
import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue';
|
||||
import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
|
||||
import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction';
|
||||
import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction';
|
||||
@@ -46,7 +46,7 @@ export class ImageValuePresentationBootstrap extends Bootstrap {
|
||||
|
||||
const cellValue = view.getCellValue(firstSelectedCell);
|
||||
|
||||
return !(this.isImageUrl(cellValue) || (isResultSetContentValue(cellValue) && this.isImage(cellValue)));
|
||||
return !this.isImage(cellValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -56,15 +56,14 @@ export class ImageValuePresentationBootstrap extends Bootstrap {
|
||||
|
||||
load(): void {}
|
||||
|
||||
private isImage(value: IResultSetContentValue | null) {
|
||||
if (value !== null && 'binary' in value) {
|
||||
private isImage(value: IResultSetValue) {
|
||||
if (isResultSetContentValue(value) && value?.binary) {
|
||||
return getMIME(value.binary || '') !== null;
|
||||
}
|
||||
if (isResultSetContentValue(value) || isResultSetBlobValue(value)) {
|
||||
return value?.contentType?.startsWith('image/') ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isImageUrl(value: IResultSetValue | undefined) {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
+3
-5
@@ -115,15 +115,13 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
let valueSize: string | undefined;
|
||||
|
||||
if (selection.elements.length > 0 || focusCell) {
|
||||
const view = model.source.getAction(resultIndex, ResultSetViewAction);
|
||||
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
|
||||
|
||||
firstSelectedCell = selection.elements[0] || focusCell;
|
||||
|
||||
const value = view.getCellValue(firstSelectedCell) ?? '';
|
||||
|
||||
stringValue = format.getText(value) ?? '';
|
||||
readonly = format.isReadOnly(firstSelectedCell);
|
||||
const value = format.get(firstSelectedCell);
|
||||
stringValue = format.getText(firstSelectedCell);
|
||||
readonly = format.isReadOnly(firstSelectedCell) || format.isBinary(firstSelectedCell);
|
||||
|
||||
if (isResultSetContentValue(value)) {
|
||||
valueTruncated = content.isContentTruncated(value);
|
||||
|
||||
@@ -7,9 +7,21 @@ export * from './DatabaseDataModel/Actions/Document/IDocumentElementKey';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/DataContext/DATA_CONTEXT_DV_DDM_RS_COLUMN_KEY';
|
||||
export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM';
|
||||
export * from './DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM_RESULT_INDEX';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/createResultSetContentValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/createResultSetFileValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetDataKey';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetFileValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetContentValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/isResultSetFileValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction';
|
||||
export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils';
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { IDatabaseDataAction, IDatabaseDataResult, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import type { IGISType } from './ResultSetGISAction';
|
||||
import type { IDatabaseDataAction, IDatabaseDataResult, IResultSetElementKey, IResultSetGeometryValue } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
export interface IDatabaseDataGISAction<TKey, TResult extends IDatabaseDataResult> extends IDatabaseDataAction<any, TResult> {
|
||||
getGISDataFor: (selectedCells: IResultSetElementKey[]) => IResultSetElementKey[];
|
||||
getCellValue: (cell: IResultSetElementKey) => IGISType | undefined;
|
||||
getCellValue: (cell: IResultSetElementKey) => IResultSetGeometryValue | undefined;
|
||||
isGISFormat: (cell: IResultSetElementKey) => boolean;
|
||||
}
|
||||
|
||||
@@ -12,25 +12,18 @@ import {
|
||||
type IDatabaseDataSource,
|
||||
type IDatabaseResultSet,
|
||||
IResultSetElementKey,
|
||||
IResultSetGeometryValue,
|
||||
isResultSetGeometryValue,
|
||||
ResultSetViewAction,
|
||||
} from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import type { IDatabaseDataGISAction } from './IDatabaseDataGISAction';
|
||||
|
||||
export interface IGISType {
|
||||
$type: string;
|
||||
srid: number;
|
||||
text: string;
|
||||
mapText: string | null;
|
||||
properties: Record<string, any> | null;
|
||||
}
|
||||
@databaseDataAction()
|
||||
export class ResultSetGISAction
|
||||
extends DatabaseDataAction<any, IDatabaseResultSet>
|
||||
implements IDatabaseDataGISAction<IResultSetElementKey, IDatabaseResultSet>
|
||||
{
|
||||
private readonly GISValueType = 'geometry';
|
||||
|
||||
static dataFormat = [ResultDataFormat.Resultset];
|
||||
|
||||
private readonly view: ResultSetViewAction;
|
||||
@@ -43,22 +36,20 @@ export class ResultSetGISAction
|
||||
isGISFormat(cell: IResultSetElementKey): boolean {
|
||||
const value = this.view.getCellValue(cell);
|
||||
|
||||
if (value !== null && typeof value === 'object' && '$type' in value) {
|
||||
return value.$type === this.GISValueType;
|
||||
}
|
||||
|
||||
return false;
|
||||
return isResultSetGeometryValue(value);
|
||||
}
|
||||
|
||||
getGISDataFor(cells: IResultSetElementKey[]): IResultSetElementKey[] {
|
||||
return cells.filter(cell => this.isGISFormat(cell));
|
||||
}
|
||||
|
||||
getCellValue(cell: IResultSetElementKey): IGISType | undefined {
|
||||
if (!this.isGISFormat(cell)) {
|
||||
getCellValue(cell: IResultSetElementKey): IResultSetGeometryValue | undefined {
|
||||
const value = this.view.getCellValue(cell);
|
||||
|
||||
if (!isResultSetGeometryValue(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.view.getCellValue(cell) as any as IGISType;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ import {
|
||||
SqlQueryResults,
|
||||
UpdateResultsDataBatchMutationVariables,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
import {
|
||||
DatabaseDataSource,
|
||||
DocumentEditAction,
|
||||
IDatabaseDataOptions,
|
||||
IDatabaseResultSet,
|
||||
IRequestInfo,
|
||||
IResultSetBlobValue,
|
||||
ResultSetEditAction,
|
||||
} from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
@@ -99,31 +101,42 @@ export class QueryDataSource<TOptions extends IDataQueryOptions = IDataQueryOpti
|
||||
}
|
||||
|
||||
const executionContextInfo = this.executionContext.context;
|
||||
const projectId = this.options.connectionKey.projectId;
|
||||
const connectionId = this.options.connectionKey.connectionId;
|
||||
const contextId = executionContextInfo.id;
|
||||
const resultsId = result.id;
|
||||
|
||||
const updateVariables: UpdateResultsDataBatchMutationVariables = {
|
||||
projectId: this.options.connectionKey.projectId,
|
||||
connectionId: this.options.connectionKey.connectionId,
|
||||
contextId: executionContextInfo.id,
|
||||
resultsId: result.id,
|
||||
projectId,
|
||||
connectionId,
|
||||
contextId,
|
||||
resultsId,
|
||||
};
|
||||
let editor: ResultSetEditAction | DocumentEditAction | undefined;
|
||||
|
||||
if (result.dataFormat === ResultDataFormat.Resultset) {
|
||||
editor = this.actions.get(result, ResultSetEditAction);
|
||||
editor.fillBatch(updateVariables);
|
||||
} else if (result.dataFormat === ResultDataFormat.Document) {
|
||||
editor = this.actions.get(result, DocumentEditAction);
|
||||
}
|
||||
|
||||
let blobs: IResultSetBlobValue[] = [];
|
||||
if (editor instanceof ResultSetEditAction) {
|
||||
blobs = editor.getBlobsToUpload();
|
||||
}
|
||||
|
||||
for (const blob of blobs) {
|
||||
const fileId = uuid();
|
||||
await this.graphQLService.sdk.uploadBlobResultSet(fileId, blob.blob!);
|
||||
blob.fileId = fileId;
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.fillBatch(updateVariables);
|
||||
}
|
||||
|
||||
const response = await this.graphQLService.sdk.updateResultsDataBatch(updateVariables);
|
||||
|
||||
this.requestInfo = {
|
||||
...this.requestInfo,
|
||||
requestDuration: response.result.duration,
|
||||
requestMessage: 'Saved successfully',
|
||||
source: this.options.query,
|
||||
};
|
||||
|
||||
if (editor) {
|
||||
const responseResult = this.transformResults(executionContextInfo, response.result.results, 0).find(
|
||||
newResult => newResult.id === result.id,
|
||||
@@ -133,6 +146,13 @@ export class QueryDataSource<TOptions extends IDataQueryOptions = IDataQueryOpti
|
||||
editor.applyUpdate(responseResult);
|
||||
}
|
||||
}
|
||||
|
||||
this.requestInfo = {
|
||||
...this.requestInfo,
|
||||
requestDuration: response.result.duration,
|
||||
requestMessage: 'Saved successfully',
|
||||
source: this.options.query,
|
||||
};
|
||||
}
|
||||
this.clearError();
|
||||
} catch (exception: any) {
|
||||
|
||||
@@ -52,8 +52,10 @@ export const ScriptPreviewDialog = observer<DialogComponentProps<Payload>>(funct
|
||||
extensions.set(...sqlDialect);
|
||||
|
||||
const apply = async () => {
|
||||
await payload.model.save();
|
||||
rejectDialog();
|
||||
try {
|
||||
await payload.model.save();
|
||||
rejectDialog();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user