From ab985673b0cce1ea071e2069c2f0353aff9ae489 Mon Sep 17 00:00:00 2001 From: Alexey Date: Mon, 16 Oct 2023 15:47:11 +0200 Subject: [PATCH] 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 Co-authored-by: DenisSinelnikov <142215442+DenisSinelnikov@users.noreply.github.com> Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../model/session/BaseWebSession.java | 3 + .../cloudbeaver/model/session/WebSession.java | 8 +- .../websocket/CBWebSessionEventHandler.java | 1 + .../bundles/io.cloudbeaver.server/plugin.xml | 3 + .../schema/service.events.graphqls | 3 +- .../src/io/cloudbeaver/server/CBPlatform.java | 16 ++ .../events/WSDeleteTempFileHandler.java | 50 ++++ .../server/websockets/CBEventsWebSocket.java | 1 - .../service/sql/DBWServiceSQL.java | 2 +- .../service/sql/WebSQLFileLoaderServlet.java | 101 ++++++++ .../service/sql/WebSQLProcessor.java | 49 +++- .../service/sql/WebServiceBindingSQL.java | 5 + .../service/sql/impl/WebServiceSQL.java | 6 +- .../core-blocks/src/ActionIconButton.m.css | 2 + .../src/FormControls/InputFileTextContent.tsx | 4 +- webapp/packages/core-browser/src/index.ts | 1 + .../packages/core-browser/src/selectFiles.ts | 33 +++ .../configs/webpack.product.dev.config.js | 2 +- .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 3 +- .../core-localization/src/locales/zh.ts | 1 + .../core-sdk/src/CustomGraphQLClient.ts | 50 ++-- .../uploadBlobResultSetExtension.ts | 23 ++ .../uploadDriverLibraryExtension.ts | 2 +- .../packages/core-sdk/src/GraphQLService.ts | 2 + webapp/packages/core-sdk/src/index.ts | 1 + .../packages/core-utils/src/base64ToBlob.ts | 27 ++ .../src/{blobToData.ts => blobToBase64.ts} | 6 +- webapp/packages/core-utils/src/getMIME.ts | 55 ++++ webapp/packages/core-utils/src/index.ts | 3 +- .../plugin-data-spreadsheet-new/package.json | 1 + .../src/DataGrid/CellEditor.tsx | 2 +- .../DataGrid/CellRenderer/CellRenderer.tsx | 4 + .../DataGridContextMenuCellEditingService.ts | 4 +- .../DataGridContextMenuFilterService.ts | 17 +- .../DataGridContextMenuSaveContentService.ts | 47 +++- .../src/DataGrid/DataGridTable.tsx | 3 + .../Formatters/CellFormatterFactory.tsx | 18 +- .../CellFormatters/BlobFormatter.m.css | 26 ++ .../CellFormatters/BlobFormatter.tsx | 47 ++++ .../CellFormatters/BooleanFormatter.tsx | 31 ++- .../CellFormatters/TextFormatter.m.css | 1 - .../CellFormatters/TextFormatter.tsx | 16 +- .../DataGrid/Formatters/IndexFormatter.tsx | 5 +- .../src/DataGrid/TableDataContext.ts | 2 + .../src/DataGrid/useGridSelectedCellsCopy.ts | 4 +- .../src/DataGrid/useTableData.tsx | 4 + .../plugin-data-spreadsheet-new/tsconfig.json | 3 + .../src/ContainerDataSource.ts | 45 +++- .../Actions/DatabaseEditAction.ts | 1 + .../Actions/Document/DocumentEditAction.ts | 13 + .../Actions/IDatabaseDataEditAction.ts | 7 +- .../Actions/IDatabaseDataFormatAction.ts | 9 +- .../Actions/ResultSet/IResultSetBlobValue.ts | 12 + .../ResultSet/IResultSetComplexValue.ts | 12 + .../ResultSet/IResultSetContentValue.ts | 3 +- .../Actions/ResultSet/IResultSetDataKey.ts | 2 +- .../Actions/ResultSet/IResultSetFileValue.ts | 15 ++ .../ResultSet/IResultSetGeometryValue.ts | 16 ++ .../Actions/ResultSet/ResultSetDataAction.ts | 5 +- .../ResultSet/ResultSetDataContentAction.ts | 26 +- .../ResultSet/ResultSetDataKeysUtils.ts | 10 +- .../Actions/ResultSet/ResultSetEditAction.ts | 242 ++++++++++++------ .../ResultSet/ResultSetFormatAction.ts | 189 ++++++++++---- .../ResultSet/ResultSetSelectAction.ts | 6 +- .../Actions/ResultSet/ResultSetViewAction.ts | 17 +- .../ResultSet/compareResultSetRowKeys.ts | 13 + .../ResultSet/createResultSetBlobValue.ts | 16 ++ .../ResultSet/createResultSetContentValue.ts | 15 ++ .../ResultSet/createResultSetFileValue.ts | 17 ++ .../Actions/ResultSet/isResultSetBlobValue.ts | 13 + .../ResultSet/isResultSetComplexValue.ts | 12 + .../ResultSet/isResultSetContentValue.ts | 3 +- .../Actions/ResultSet/isResultSetFileValue.ts | 13 + .../ResultSet/isResultSetGeometryValue.ts | 13 + .../DatabaseDataModel/DatabaseDataActions.ts | 27 +- .../TableFooterMenu/TableFooterMenuService.ts | 2 +- .../ImageValue/ImageValuePresentation.m.css | 18 ++ .../ImageValue/ImageValuePresentation.tsx | 215 +++++++++------- .../ImageValuePresentationBootstrap.ts | 15 +- .../TextValue/TextValuePresentation.tsx | 8 +- .../packages/plugin-data-viewer/src/index.ts | 12 + .../src/IDatabaseDataGISAction.ts | 6 +- .../src/ResultSetGISAction.ts | 25 +- .../plugin-sql-editor/src/QueryDataSource.ts | 44 +++- .../src/ScriptPreview/ScriptPreviewDialog.tsx | 6 +- 87 files changed, 1390 insertions(+), 433 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java create mode 100644 webapp/packages/core-browser/src/selectFiles.ts create mode 100644 webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts create mode 100644 webapp/packages/core-utils/src/base64ToBlob.ts rename webapp/packages/core-utils/src/{blobToData.ts => blobToBase64.ts} (77%) create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index d96429c4e6..a049a6667b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -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 { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index 3dbe3c3b6e..ff50e74320 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -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(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java index fb9e22f078..ba68ce02b2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java @@ -23,4 +23,5 @@ public interface CBWebSessionEventHandler { void handleWebSessionEvent(WSEvent event) throws DBException; void close(); + } diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index eb75b6ef1d..522dfe5427 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -73,6 +73,9 @@ + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 5d21aaee78..515b89b30d 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -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 diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 4c53999c7f..98cf78bfed 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -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)"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java new file mode 100644 index 0000000000..40e44c9e3a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java @@ -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 { + + 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()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index c927564af0..401124d84c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -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); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 163076b5a5..b14adc1e22 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -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; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java new file mode 100644 index 0000000000..f05434a483 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -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>() { + }.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 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()); + } + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 28b3b20261..3ee8dffdd3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -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 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 variables = (LinkedTreeMap) 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 variables = (LinkedTreeMap) 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()); + } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index fa8d1ed3cf..9b63060156 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -255,6 +255,11 @@ public class WebServiceBindingSQL extends WebServiceBindingBase i new WebSQLResultServlet(application, getServiceImpl()), application.getServicesURI() + "sql-result-value/*" ); + servletContext.addServlet( + "sqlUploadFile", + new WebSQLFileLoaderServlet(application), + application.getServicesURI() + "resultset/blob/*" + ); } private static class WebSQLConfiguration { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 2e1f5f6de0..94229ea4ea 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -83,7 +83,7 @@ public class WebServiceSQL implements DBWServiceSQL { conToRead.addAll(session.getConnections()); } - List contexts = new ArrayList<>(); + List 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, diff --git a/webapp/packages/core-blocks/src/ActionIconButton.m.css b/webapp/packages/core-blocks/src/ActionIconButton.m.css index 62d354bcc9..5dce1ceef1 100644 --- a/webapp/packages/core-blocks/src/ActionIconButton.m.css +++ b/webapp/packages/core-blocks/src/ActionIconButton.m.css @@ -7,4 +7,6 @@ height: 24px !important; overflow: hidden; flex-shrink: 0; + flex-grow: 0; + flex-basis: auto; } diff --git a/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx index 258436c234..e734d071ad 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx @@ -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); diff --git a/webapp/packages/core-browser/src/index.ts b/webapp/packages/core-browser/src/index.ts index 231b868af1..4afcee8d0c 100644 --- a/webapp/packages/core-browser/src/index.ts +++ b/webapp/packages/core-browser/src/index.ts @@ -1,4 +1,5 @@ export * from './IndexedDB/IndexedDBService'; export * from './IndexedDB/IndexedDB'; export * from './manifest'; +export * from './selectFiles'; export * from './ServiceWorkerService'; diff --git a/webapp/packages/core-browser/src/selectFiles.ts b/webapp/packages/core-browser/src/selectFiles.ts new file mode 100644 index 0000000000..3c15074e7e --- /dev/null +++ b/webapp/packages/core-browser/src/selectFiles.ts @@ -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); +} diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index c6cafab724..102370cd7b 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -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: [ diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 763f0dc9f6..14498d138d 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index d0763e2198..2c8a057faf 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -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'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 73252113e5..a61d30fd4f 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -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', 'Лимит'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index 02bc0e4c7c..dcd8b9e5a2 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -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'], diff --git a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts index 0714a8448d..5ce68ed447 100644 --- a/webapp/packages/core-sdk/src/CustomGraphQLClient.ts +++ b/webapp/packages/core-sdk/src/CustomGraphQLClient.ts @@ -31,6 +31,19 @@ export class CustomGraphQLClient extends GraphQLClient { private requestsBlockedReason: Error | string | null = null; async uploadFile( + url: string, + file: Blob, + query?: string, + variables?: V, + onUploadProgress?: (event: UploadProgressEvent) => void, + ): Promise { + return this.interceptors.reduce( + (accumulator, interceptor) => interceptor(accumulator), + this.overrideFilesUpload(url, file, query, variables, onUploadProgress), + ); + } + + async uploadFiles( url: string, files: FileList, query?: string, @@ -39,7 +52,7 @@ export class CustomGraphQLClient extends GraphQLClient { ): Promise { return this.interceptors.reduce( (accumulator, interceptor) => interceptor(accumulator), - this.overrideFileUpload(url, files, query, variables, onUploadProgress), + this.overrideFilesUpload(url, files, query, variables, onUploadProgress), ); } @@ -107,9 +120,9 @@ export class CustomGraphQLClient extends GraphQLClient { } } - private async overrideFileUpload( + private async overrideFilesUpload( 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/**/ ( - 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/**/ (url, data, { + onUploadProgress, + responseType: 'json', + }); // TODO: we don't support GQL response right now // TODO: seems here can be undefined diff --git a/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts new file mode 100644 index 0000000000..c80271ed8c --- /dev/null +++ b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts @@ -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; +} + +export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension { + return { + uploadBlobResultSet(fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void): Promise { + // api/resultset/blob + return client.uploadFile(GlobalConstants.absoluteServiceUrl('resultset', 'blob'), data, undefined, { fileId }, onUploadProgress); + }, + }; +} diff --git a/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts b/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts index 7a239f6bf8..3f088bdce5 100644 --- a/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts +++ b/webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts @@ -16,7 +16,7 @@ export interface IUploadDriverLibraryExtension { export function uploadDriverLibraryExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension { return { uploadDriverLibrary(driverId: string, files: FileList, onUploadProgress?: (event: UploadProgressEvent) => void): Promise { - return client.uploadFile(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress); + return client.uploadFiles(GlobalConstants.absoluteServiceUrl('drivers', 'library'), files, undefined, { driverId }, onUploadProgress); }, }; } diff --git a/webapp/packages/core-sdk/src/GraphQLService.ts b/webapp/packages/core-sdk/src/GraphQLService.ts index 07cdc795ae..b8eb8e9d7e 100644 --- a/webapp/packages/core-sdk/src/GraphQLService.ts +++ b/webapp/packages/core-sdk/src/GraphQLService.ts @@ -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), }; } diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts index 399400d300..de2ffb2c59 100644 --- a/webapp/packages/core-sdk/src/index.ts +++ b/webapp/packages/core-sdk/src/index.ts @@ -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'; diff --git a/webapp/packages/core-utils/src/base64ToBlob.ts b/webapp/packages/core-utils/src/base64ToBlob.ts new file mode 100644 index 0000000000..54bd599203 --- /dev/null +++ b/webapp/packages/core-utils/src/base64ToBlob.ts @@ -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 }); +} diff --git a/webapp/packages/core-utils/src/blobToData.ts b/webapp/packages/core-utils/src/blobToBase64.ts similarity index 77% rename from webapp/packages/core-utils/src/blobToData.ts rename to webapp/packages/core-utils/src/blobToBase64.ts index 480c9bce7a..36e47575a5 100644 --- a/webapp/packages/core-utils/src/blobToData.ts +++ b/webapp/packages/core-utils/src/blobToBase64.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -export function blobToData(blob: Blob | File): Promise { +export function blobToBase64(blob: Blob | File, slice?: number): Promise { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => { @@ -16,6 +16,10 @@ export function blobToData(blob: Blob | File): Promise { reject(fileReader.error); }; + if (slice) { + blob = blob.slice(0, slice); + } + fileReader.readAsDataURL(blob); }); } diff --git a/webapp/packages/core-utils/src/getMIME.ts b/webapp/packages/core-utils/src/getMIME.ts index 64a3727cf5..dd8bc12444 100644 --- a/webapp/packages/core-utils/src/getMIME.ts +++ b/webapp/packages/core-utils/src/getMIME.ts @@ -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)); +// } diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index d5646f9b6f..ca2a5a0ccc 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -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'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 056373066d..eeefbbc709 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -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", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx index ffcc6095ec..a7f0f01721 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor.tsx @@ -72,7 +72,7 @@ export const CellEditor = observer, ' 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 = () => { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index e86adf0b1b..a6ff25b0ca 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -90,6 +90,10 @@ export const CellRenderer = observer> { 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({ 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 + ':', }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts index 616af6741b..9766f1ae80 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts @@ -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)) diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index e3dd643458..b4ce46fe97 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -319,6 +319,7 @@ export const DataGridTable = observer(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; + } } } } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css new file mode 100644 index 0000000000..696c9aaac1 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.m.css @@ -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'; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx new file mode 100644 index 0000000000..91de76315e --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BlobFormatter.tsx @@ -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>(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 ( + +
{displayString}
+
+ ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx index 61d805ce2a..5cb32cca7e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx @@ -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>(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 ( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css index 9bc33bd293..aa2b666fe8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.m.css @@ -27,7 +27,6 @@ .textFormatterValue { overflow: hidden; - white-space: pre; text-overflow: ellipsis; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx index 2b3ce9d003..5095124c29 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/TextFormatter.tsx @@ -31,12 +31,12 @@ export const TextFormatter = observer>(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>(functio ); } - const isUrl = typeof rawValue === 'string' && isValidUrl(rawValue); - return ( -
- {isUrl && ( - +
+ {isValidUrl(textValue) && ( + )} -
{value}
+
{displayValue}
); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx index 70248aa345..5e539bbcbc 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/IndexFormatter.tsx @@ -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> = function IndexFormatter(props) { +export const IndexFormatter: React.FC> = observer(function IndexFormatter(props) { const context = useContext(CellContext); return
{context.position.rowIdx + 1}
; -}; +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts index c1d6558471..f56f2def1b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts @@ -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; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 1dd5ad7aaa..eb1c486d01 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -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) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx index 3167d1e2a4..a33991a9f5 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.tsx @@ -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 }>( @@ -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, diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index 2764c2ed85..6a2934cf7e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -42,6 +42,9 @@ { "path": "../core-utils/tsconfig.json" }, + { + "path": "../core-browser/tsconfig.json" + }, { "path": "../plugin-data-viewer/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 04adc6951a..1c98fba0b0 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -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 newResult.id === result.id, @@ -188,7 +201,15 @@ export class ContainerDataSource extends DatabaseDataSource { @@ -54,6 +54,7 @@ export interface IDatabaseDataEditAction void; duplicate: (...key: TKey[]) => void; delete: (key: TKey) => void; + applyPartialUpdate(result: TResult): void; applyUpdate: (result: TResult) => void; revert: (key: TKey) => void; clear: () => void; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts index 997cd7eebc..790f553ca7 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -10,8 +10,9 @@ import type { IDatabaseDataResult } from '../IDatabaseDataResult'; export interface IDatabaseDataFormatAction extends IDatabaseDataAction { 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; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts new file mode 100644 index 0000000000..a99369c92d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBlobValue.ts @@ -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; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts new file mode 100644 index 0000000000..04695b7496 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue.ts @@ -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; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts index 8f32f4d009..31fe64404b 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetContentValue.ts @@ -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; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts index 2a06c2ed4c..645863f396 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataKey.ts @@ -12,7 +12,7 @@ export interface IResultSetColumnKey { export interface IResultSetRowKey { index: number; - key?: string; + subIndex: number; } export interface IResultSetElementKey { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts new file mode 100644 index 0000000000..e1321f2cb5 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetFileValue.ts @@ -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; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts new file mode 100644 index 0000000000..20190e8b72 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue.ts @@ -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 | null; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index d1bf79cb51..328ca9d696 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -43,6 +43,7 @@ export class ResultSetDataAction extends DatabaseDataAction 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; activeElement: IResultSetElementKey | null; constructor( source: IDatabaseDataSource, - 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(this, { editorData: observable, - addRows: computed, - updates: computed, set: action, add: action, addRow: action, @@ -63,6 +67,7 @@ export class ResultSetEditAction extends DatabaseEditAction { 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 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> = []; - 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> = []; + + 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 { + const blobs: Array = []; - 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>((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 { + 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; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts index 5ec3c69ac3..412ccd4867 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts @@ -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 | null> | null; +export type IResultSetValue = + | string + | number + | boolean + | Record | 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(''); } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts index edf87178d5..e03c572cea 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts @@ -267,7 +267,11 @@ export class ResultSetSelectAction extends DatabaseSelectAction ({ 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(this, { - rowKeys: computed, - columnKeys: computed, - rows: computed, - columns: computed, columnsOrder: observable, setColumnOrder: action, }); @@ -129,7 +126,7 @@ export class ResultSetViewAction extends DatabaseDataAction= 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)) { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts new file mode 100644 index 0000000000..b4cdb97318 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/compareResultSetRowKeys.ts @@ -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; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts new file mode 100644 index 0000000000..b79fb4df77 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue.ts @@ -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, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts new file mode 100644 index 0000000000..27461c4df3 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetContentValue.ts @@ -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 { + return { + $type: 'content', + ...data, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts new file mode 100644 index 0000000000..8fa5fb8eda --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/createResultSetFileValue.ts @@ -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, + }; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts new file mode 100644 index 0000000000..c44c186315 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.ts @@ -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; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts new file mode 100644 index 0000000000..c305b7e20b --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue.ts @@ -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'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts index 38303410d7..bd0d1455c4 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts @@ -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'; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts new file mode 100644 index 0000000000..699ef27923 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetFileValue.ts @@ -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'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts new file mode 100644 index 0000000000..9593b50ae9 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetGeometryValue.ts @@ -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'; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index 2de6937126..a991001d02 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -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 } get>(result: TResult, Action: IDatabaseDataActionClass): 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 for (const dependency of allDeps) { if (isDatabaseDataAction(dependency)) { - depends.push(this.get>(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 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 Action: IDatabaseDataActionInterface, ): 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 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 } } } + +function isActionSupportsFormat( + action: IDatabaseDataActionClass> | IDatabaseDataAction, + format: ResultDataFormat, +): boolean { + if ('dataFormat' in action) { + return !action.dataFormat || action.dataFormat.includes(format); + } + const constructor = action.constructor as IDatabaseDataActionClass>; + return !constructor.dataFormat || constructor.dataFormat.includes(format); +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index ea1ad375cc..3fe63db3be 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -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({ diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css new file mode 100644 index 0000000000..26ae7b051a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.m.css @@ -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; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index 223fb7c16e..f2d40524a8 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -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(function Tools({ loading, stretch, onToggleStretch, onSave }) { +const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave, onUpload }) { const translate = useTranslate(); - return styled(VALUE_PANEL_TOOLS_STYLES)( - - {onSave && ( - - )} + return ( + + + {onSave && ( + + )} + {onUpload && ( + + )} + + {onToggleStretch && ( - - - - - + - - - + /> + )} - , + ); }); @@ -94,29 +85,39 @@ export const ImageValuePresentation: TabContainerPanelComponent ({ + 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 { + 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 { 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)( - - - {content.isDownloadable(state.selectedCell) && ( - - )} - - - , + return ( + + + + {state.contentAction.isDownloadable(state.selectedCell) && ( + + )} + + + + ); } - return styled(style)( - - - - - - , + return ( + + + + + + ); }, ); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts index eccdc1a70d..3f7658f7cc 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts @@ -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; } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx index 1cbed86c21..a7bc46d9c6 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -115,15 +115,13 @@ export const TextValuePresentation: TabContainerPanelComponent 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); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index cdc7eff691..4ddeac3423 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -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'; diff --git a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts index a7cd711f43..ad9e8839df 100644 --- a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts +++ b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts @@ -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 extends IDatabaseDataAction { getGISDataFor: (selectedCells: IResultSetElementKey[]) => IResultSetElementKey[]; - getCellValue: (cell: IResultSetElementKey) => IGISType | undefined; + getCellValue: (cell: IResultSetElementKey) => IResultSetGeometryValue | undefined; isGISFormat: (cell: IResultSetElementKey) => boolean; } diff --git a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts index 610e3303fd..2fbbe59d49 100644 --- a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts +++ b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts @@ -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 | null; -} @databaseDataAction() export class ResultSetGISAction extends DatabaseDataAction implements IDatabaseDataGISAction { - 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; } } diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 95355bff36..1c9b5ef903 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -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 newResult.id === result.id, @@ -133,6 +146,13 @@ export class QueryDataSource>(funct extensions.set(...sqlDialect); const apply = async () => { - await payload.model.save(); - rejectDialog(); + try { + await payload.model.save(); + rejectDialog(); + } catch {} }; return (