From 66f8f991c920655b9c4e4aef34a199b83a926e1f Mon Sep 17 00:00:00 2001 From: Ainur Date: Wed, 10 Jan 2024 17:54:31 +0100 Subject: [PATCH 01/18] CB-4494 api for reading full value from text cell --- .../schema/service.sql.graphqls | 12 ++- .../service/sql/DBWServiceSQL.java | 8 ++ .../service/sql/WebSQLCellValueReceiver.java | 85 +++++++++++++++++++ .../service/sql/WebSQLDataLOBReceiver.java | 66 +++----------- .../service/sql/WebSQLProcessor.java | 41 +++++++-- .../service/sql/WebServiceBindingSQL.java | 6 ++ .../service/sql/impl/WebServiceSQL.java | 57 ++++++++++--- 7 files changed, 200 insertions(+), 75 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 137cadbdf2..ed8b058945 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -331,7 +331,7 @@ extend type Mutation { addedRows: [ SQLResultRow! ], ): String! - #Return BLOB name + # Returns BLOB name readLobValue( projectId: ID, connectionId: ID!, @@ -341,6 +341,16 @@ extend type Mutation { row: [ SQLResultRow! ]! ): String! + # Returns full string value + sqlReadStringValue( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + columnIndex: Int!, + row: [ SQLResultRow! ]! + ): String! + # Returns SQLExecuteInfo asyncSqlExecuteResults(taskId: ID!): SQLExecuteInfo ! 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 37c72fe2d2..180f0614ee 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 @@ -124,6 +124,14 @@ String readLobValue( @NotNull Integer lobColumnIndex, @Nullable List row) throws DBWebException; + @NotNull + @WebAction + String getCellValue( + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @Nullable List row) throws DBWebException; + @WebAction String updateResultsDataBatchScript( @NotNull WebSQLContextInfo contextInfo, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java new file mode 100644 index 0000000000..8fafb7a38b --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCellValueReceiver.java @@ -0,0 +1,85 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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 org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.data.DBDAttributeBindingMeta; +import org.jkiss.dbeaver.model.data.DBDContent; +import org.jkiss.dbeaver.model.data.DBDDataReceiver; +import org.jkiss.dbeaver.model.data.DBDValue; +import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.utils.ContentUtils; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class WebSQLCellValueReceiver implements DBDDataReceiver { + protected final DBSDataContainer dataContainer; + protected int rowIndex; + protected DBDAttributeBindingMeta binding; + protected Object value; + + public WebSQLCellValueReceiver(DBSDataContainer dataContainer, int rowIndex) { + this.dataContainer = dataContainer; + this.rowIndex = rowIndex; + } + + @Override + public void fetchStart(DBCSession session, DBCResultSet resultSet, long offset, long maxRows) throws DBCException { + DBCResultSetMetaData meta = resultSet.getMeta(); + List attributes = meta.getAttributes(); + DBCAttributeMetaData attrMeta = attributes.get(rowIndex); + binding = new DBDAttributeBindingMeta(dataContainer, resultSet.getSession(), attrMeta); + } + + @Override + public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { + value = binding.getValueHandler().fetchValueObject( + resultSet.getSession(), + resultSet, + binding.getMetaAttribute(), + rowIndex); + } + + @Override + public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCException { + + } + + @Override + public void close() { + + } + + @NotNull + public byte[] getBinaryValue(DBRProgressMonitor monitor) throws DBCException { + byte[] binaryValue; + if (value instanceof DBDContent dbdContent) { + binaryValue = ContentUtils.getContentBinaryValue(monitor, dbdContent); + } else if (value instanceof DBDValue dbdValue) { + binaryValue = dbdValue.getRawValue().toString().getBytes(); + } else { + binaryValue = value.toString().getBytes(StandardCharsets.UTF_8); + } + if (binaryValue == null) { + throw new DBCException("Lob value is null"); + } + return binaryValue; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java index f08bb05147..fbfced3597 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java @@ -20,39 +20,29 @@ import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.CBPlatform; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.struct.DBSDataContainer; -import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.utils.CommonUtils; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Timestamp; import java.text.SimpleDateFormat; -import java.util.List; -public class WebSQLDataLOBReceiver implements DBDDataReceiver { +public class WebSQLDataLOBReceiver extends WebSQLCellValueReceiver { private static final Log log = Log.getLog(WebSQLDataLOBReceiver.class); public static final Path DATA_EXPORT_FOLDER = CBPlatform.getInstance().getTempFolder(new VoidProgressMonitor(), "sql-lob-files"); - private final String tableName; - private final DBSDataContainer dataContainer; - - private DBDAttributeBinding binding; - private Object lobValue; - private int rowIndex; WebSQLDataLOBReceiver(String tableName, DBSDataContainer dataContainer, int rowIndex) { + super(dataContainer, rowIndex); this.tableName = tableName; - this.dataContainer = dataContainer; - this.rowIndex = rowIndex; - if (!Files.exists(DATA_EXPORT_FOLDER)){ + if (!Files.exists(DATA_EXPORT_FOLDER)) { try { Files.createDirectories(DATA_EXPORT_FOLDER); } catch (IOException e) { @@ -62,64 +52,28 @@ public class WebSQLDataLOBReceiver implements DBDDataReceiver { } - public String createLobFile(DBCSession session) throws DBCException, IOException { + public String createLobFile(DBRProgressMonitor monitor) throws DBCException, IOException { String exportFileName = CommonUtils.truncateString(tableName, 32); StringBuilder fileName = new StringBuilder(exportFileName); fileName.append("_") - .append(binding.getName()) - .append("_"); + .append(binding.getName()) + .append("_"); Timestamp ts = new Timestamp(System.currentTimeMillis()); String s = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(ts); fileName.append(s); exportFileName = CommonUtils.escapeFileName(fileName.toString()); - byte[] binaryValue; + byte[] binaryValue = getBinaryValue(monitor); Number fileSizeLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(CBConstants.QUOTA_PROP_FILE_LIMIT); - if (lobValue instanceof DBDContent) { - binaryValue = ContentUtils.getContentBinaryValue(session.getProgressMonitor(), (DBDContent) lobValue); - } else if (lobValue instanceof DBDValue) { - binaryValue = ((DBDValue) lobValue).getRawValue().toString().getBytes(); - } else { - binaryValue = lobValue.toString().getBytes(StandardCharsets.UTF_8); - } - if (binaryValue == null) { - throw new DBCException("Lob value is null"); - } if (binaryValue.length > fileSizeLimit.longValue()) { throw new DBQuotaException( - "Data export quota exceeded \n Please increase the resourceQuotas parameter in configuration", + "Data export quota exceeded \n Please increase the resourceQuotas parameter in configuration", CBConstants.QUOTA_PROP_FILE_LIMIT, fileSizeLimit.longValue(), binaryValue.length ); } - Path file = DATA_EXPORT_FOLDER.resolve(exportFileName); + Path file = WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER.resolve(exportFileName); Files.write(file, binaryValue); return exportFileName; } - - @Override - public void fetchStart(DBCSession session, DBCResultSet resultSet, long offset, long maxRows) throws DBCException { - DBCResultSetMetaData meta = resultSet.getMeta(); - List attributes = meta.getAttributes(); - DBCAttributeMetaData attrMeta = attributes.get(rowIndex); - binding = new DBDAttributeBindingMeta(dataContainer, resultSet.getSession(), attrMeta); - } - @Override - public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { - lobValue = binding.getValueHandler().fetchValueObject( - resultSet.getSession(), - resultSet, - binding.getMetaAttribute(), - rowIndex); - } - - @Override - public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCException { - - } - - @Override - public void close() { - - } } 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 65750fc58c..be9715377c 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 @@ -54,6 +54,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -797,14 +798,27 @@ public String readLobValue( DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); checkRowIdentifier(resultsInfo, rowIdentifier); + String tableName = rowIdentifier.getEntity().getName(); + WebSQLDataLOBReceiver dataReceiver = new WebSQLDataLOBReceiver(tableName, resultsInfo.getDataContainer(), lobColumnIndex); + readCellDataValue(monitor, resultsInfo, row, dataReceiver); + try { + return dataReceiver.createLobFile(monitor); + } catch (Exception e) { + throw new DBWebException("Error creating temporary lob file ", e); + } + } + + private void readCellDataValue( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSQLResultsInfo resultsInfo, + @Nullable WebSQLResultsRow row, + @NotNull WebSQLCellValueReceiver dataReceiver) throws DBException { DBSDataContainer dataContainer = resultsInfo.getDataContainer(); DBCExecutionContext executionContext = getExecutionContext(dataContainer); - String tableName = rowIdentifier.getEntity().getName(); - WebSQLDataLOBReceiver dataReceiver = new WebSQLDataLOBReceiver(tableName, dataContainer, lobColumnIndex); try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Generate data update batches")) { WebExecutionSource executionSource = new WebExecutionSource(dataContainer, executionContext, this); DBDDataFilter dataFilter = new DBDDataFilter(); - DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]); + DBDAttributeBinding[] keyAttributes = resultsInfo.getDefaultRowIdentifier().getAttributes().toArray(new DBDAttributeBinding[0]); Object[] rowValues = new Object[keyAttributes.length]; List constraints = new ArrayList<>(); for (int i = 0; i < keyAttributes.length; i++) { @@ -833,14 +847,25 @@ public String readLobValue( DBCStatistics statistics = dataContainer.readData( executionSource, session, dataReceiver, dataFilter, 0, 1, DBSDataContainer.FLAG_NONE, 1); - try { - return dataReceiver.createLobFile(session); - } catch (Exception e) { - throw new DBWebException("Error creating temporary lob file ", e); - } } } + @NotNull + public String readStringValue( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer columnIndex, + @Nullable WebSQLResultsRow row + ) throws DBException { + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); + checkRowIdentifier(resultsInfo, rowIdentifier); + WebSQLCellValueReceiver dataReceiver = new WebSQLCellValueReceiver(resultsInfo.getDataContainer(), columnIndex); + readCellDataValue(monitor, resultsInfo, row, dataReceiver); + return new String(dataReceiver.getBinaryValue(monitor), StandardCharsets.UTF_8); + } + //////////////////////////////////////////////// // Misc 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 e24cf433b6..23b095065c 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 @@ -137,6 +137,12 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("resultsId"), env.getArgument("lobColumnIndex"), getResultsRow(env, "row"))) + .dataFetcher("sqlReadStringValue", env -> + getService(env).getCellValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("columnIndex"), + getResultsRow(env, "row"))) .dataFetcher("updateResultsDataBatch", env -> getService(env).updateResultsDataBatch( getSQLContext(env), 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 1302be8e7d..9a68a0b1b3 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 @@ -60,6 +60,9 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -319,6 +322,11 @@ public WebSQLExecuteInfo updateResultsDataBatch( } } + @FunctionalInterface + private interface ThrowableFunction { + R apply(T obj) throws Exception; + } + @Override public String readLobValue( @NotNull WebSQLContextInfo contextInfo, @@ -326,24 +334,53 @@ public String readLobValue( @NotNull Integer lobColumnIndex, @Nullable List row) throws DBWebException { + if (row == null) { + throw new DBWebException("Results row is not found"); + } + ThrowableFunction function = monitor -> contextInfo.getProcessor().readLobValue( + monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)); + return readValue(function, contextInfo.getProcessor()); + } + + @NotNull + @Override + public String getCellValue( + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @Nullable List row + ) throws DBWebException { + if (row == null) { + throw new DBWebException("Results row is not found"); + } + WebSQLProcessor processor = contextInfo.getProcessor(); + ThrowableFunction function = monitor -> processor.readStringValue( + monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)); + return readValue(function, processor); + } + + @NotNull + private String readValue( + @NotNull ThrowableFunction function, + @NotNull WebSQLProcessor processor + ) throws DBWebException { try { var result = new StringBuilder(); DBExecUtils.tryExecuteRecover( - contextInfo.getProcessor().getWebSession().getProgressMonitor(), - contextInfo.getProcessor().getConnection().getDataSource(), - monitor -> { - try { - result.append(contextInfo.getProcessor().readLobValue( - monitor, contextInfo, resultsId, lobColumnIndex, row.get(0))); - } catch (Exception e) { - throw new InvocationTargetException(e); - } + processor.getWebSession().getProgressMonitor(), + processor.getConnection().getDataSource(), + monitor -> { + try { + result.append(function); + } catch (Exception e) { + throw new InvocationTargetException(e); } + } ); return result.toString(); } catch (DBException e) { - throw new DBWebException("Error reading LOB value ", e); + throw new DBWebException("Error reading value ", e); } } From b17c9289c6450d080db67c6e9a0f1d63a2b77172 Mon Sep 17 00:00:00 2001 From: Ainur Date: Thu, 11 Jan 2024 14:16:02 +0100 Subject: [PATCH 02/18] CB-4494 apply function fix --- .../src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9a68a0b1b3..1d586bbd05 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 @@ -372,7 +372,7 @@ private String readValue( processor.getConnection().getDataSource(), monitor -> { try { - result.append(function); + result.append(function.apply(monitor)); } catch (Exception e) { throw new InvocationTargetException(e); } From 0bb3795efdafaa30df951ef476cdcf699767a423 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Thu, 11 Jan 2024 14:55:03 +0100 Subject: [PATCH 03/18] CB-4285 graphql getFileFullText for files --- .../src/queries/sqlReadStringValue.gql | 3 ++ .../ResultSet/ResultSetDataContentAction.ts | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql diff --git a/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql b/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql new file mode 100644 index 0000000000..328fd35eab --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql @@ -0,0 +1,3 @@ +mutation getSqlReadStringValue($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $columnIndex: Int!, $row: [SQLResultRow!]!) { + text: sqlReadStringValue(connectionId: $connectionId, contextId: $contextId, resultsId: $resultsId, columnIndex: $columnIndex, row: $row) +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index aa7b3900bc..1d5aed7e3a 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -60,6 +60,44 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + try { + this.activeElement = element; + return await this.loadFileFullText(this.result, column.position, row); + } finally { + this.activeElement = null; + } + }); + + return text; + } + async getFileDataUrl(element: IResultSetElementKey) { const column = this.data.getColumn(element.column); const row = this.data.getRowValue(element.row); From 6ede59b08c3bfb0990635cfdfa18ca9a5d73885b Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 11:56:15 +0100 Subject: [PATCH 04/18] CB-4285 textValuePresentation refactor --- .../TextValue/TextValuePresentation.tsx | 172 ++++++++++++------ 1 file changed, 115 insertions(+), 57 deletions(-) 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 c09c2daa82..a0a1cca168 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -5,12 +5,12 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; -import { Button, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; @@ -22,7 +22,7 @@ import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/Resul import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import { IResultSetValue, ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; @@ -93,85 +93,137 @@ export const TextValuePresentation: TabContainerPanelComponent ({ + get selectAction(): ResultSetSelectAction { + return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); + }, + get firstSelectedCell(): IResultSetElementKey | undefined { + const activeElements = this.selectAction.getActiveElements(); - const activeElements = selection.getActiveElements(); - const activeTabs = textValuePresentationService.tabs.getDisplayed({ dataFormat, model, resultIndex }); + if (activeElements.length === 0) { + return undefined; + } - let contentType = 'text/plain'; - let firstSelectedCell: IResultSetElementKey | undefined; - let readonly = true; - let valueTruncated = false; - let limit: string | undefined; - let valueSize: string | undefined; + return activeElements[0]; + }, + get editAction(): ResultSetEditAction { + return this.model.source.getAction(resultIndex, ResultSetEditAction); + }, + get contentAction(): ResultSetDataContentAction { + return this.model.source.getAction(resultIndex, ResultSetDataContentAction); + }, + get formatAction(): ResultSetFormatAction { + return this.model.source.getAction(resultIndex, ResultSetFormatAction); + }, + get isValueTruncated(): boolean { + if (!this.firstSelectedCell) { + return false; + } - if (activeElements.length > 0) { - const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const value = this.formatAction.get(this.firstSelectedCell); - firstSelectedCell = activeElements[0]; + if (isResultSetContentValue(value)) { + return this.contentAction.isContentTruncated(value); + } - const value = format.get(firstSelectedCell); - readonly = format.isReadOnly(firstSelectedCell) || format.isBinary(firstSelectedCell); + return false; + }, + get isReadonly(): boolean { + let readonly = true; + if (this.firstSelectedCell) { + readonly = this.formatAction.isReadOnly(this.firstSelectedCell) || this.formatAction.isBinary(this.firstSelectedCell); + } - if (isResultSetContentValue(value)) { - valueTruncated = content.isContentTruncated(value); + return this.model.isReadonly(this.resultIndex) || this.model.isDisabled(resultIndex) || readonly; + }, + get value(): IResultSetValue | null { + if (this.firstSelectedCell) { + return this.formatAction.get(this.firstSelectedCell); + } - if (valueTruncated) { - limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); - valueSize = bytesToSize(value.contentLength ?? 0); - } + return null; + }, + get limit(): string | undefined { + if (this.firstSelectedCell && isResultSetContentValue(this.value) && this.isValueTruncated) { + return bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); + } - if (value.contentType) { - contentType = value.contentType; + return; + }, + get valueSize(): string | undefined { + if (this.firstSelectedCell && isResultSetContentValue(this.value) && this.isValueTruncated) { + return bytesToSize(this.value.contentLength ?? 0); + } - if (contentType === 'text/json') { - contentType = 'application/json'; + return; + }, + get activeTabs() { + return textValuePresentationService.tabs.getDisplayed({ dataFormat: this.dataFormat, model: this.model, resultIndex: this.resultIndex }); + }, + handleChange(newValue: string) { + if (this.firstSelectedCell && !this.isReadonly) { + this.editAction.set(this.firstSelectedCell, newValue); + } + }, + async save() { + if (!this.firstSelectedCell) { + return; } - if (!activeTabs.some(tab => tab.key === contentType)) { - contentType = 'text/plain'; + try { + await this.contentAction.downloadFileData(this.firstSelectedCell); + } catch (exception) { + this.notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); } - } - } - } + }, + }), + { + limit: computed, + formatAction: computed, + selectAction: computed, + firstSelectedCell: computed, + editAction: computed, + isReadonly: computed, + value: computed, + activeTabs: computed, + contentAction: computed, + isValueTruncated: computed, + handleChange: action.bound, + save: action.bound, + }, + { model, resultIndex, dataFormat, notificationService }, + ); - readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || readonly; + let contentType = 'text/plain'; - if (contentType !== state.lastContentType) { - state.setDefaultContentType(contentType); - } + if (data.firstSelectedCell && isResultSetContentValue(data.value) && data.value.contentType) { + contentType = data.value.contentType; - function handleChange(newValue: string) { - if (firstSelectedCell && !readonly) { - editor.set(firstSelectedCell, newValue); + if (contentType === 'text/json') { + contentType = 'application/json'; } - } - async function save() { - if (!firstSelectedCell) { - return; + if (!data.activeTabs.some(tab => tab.key === contentType)) { + contentType = 'text/plain'; } + } - try { - await content.downloadFileData(firstSelectedCell); - } catch (exception) { - notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); - } + if (contentType !== state.lastContentType) { + state.setDefaultContentType(contentType); } let currentContentType = state.currentContentType; - if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === currentContentType)) { - currentContentType = activeTabs[0].key; + if (data.activeTabs.length > 0 && !data.activeTabs.some(tab => tab.key === currentContentType)) { + currentContentType = data.activeTabs[0].key; } - const canSave = !!firstSelectedCell && content.isDownloadable(firstSelectedCell); + const canSave = !!data.firstSelectedCell && data.contentAction.isDownloadable(data.firstSelectedCell); const typeExtension = useMemo(() => getTypeExtension(currentContentType) ?? [], [currentContentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); - const value = useTextValue({ + const textValue = useTextValue({ model, resultIndex, currentContentType, @@ -199,11 +251,17 @@ export const TextValuePresentation: TabContainerPanelComponent - handleChange(value)} /> - {valueTruncated && } + + {data.isValueTruncated && } {canSave && ( - From 9afb68d78a5ae7fa22dded6cdfa523adcfda1f53 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 12:20:53 +0100 Subject: [PATCH 05/18] CB-4285 textValuePresentation refactor - removed unneeded fields --- .../TextValue/TextValuePresentation.tsx | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) 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 a0a1cca168..bea3b2633f 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -81,14 +81,23 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ currentContentType: 'text/plain', - lastContentType: 'text/plain', - setContentType(type: string) { - this.currentContentType = type; + setContentType(contentType: string) { + if (contentType === 'text/json') { + contentType = 'application/json'; + } + + if (!data.activeTabs.some(tab => tab.key === contentType)) { + contentType = 'text/plain'; + } + + this.currentContentType = contentType; }, - setDefaultContentType(type: string) { - this.currentContentType = type; - this.lastContentType = type; + handleTabOpen(tabId: string) { + // currentContentType may be selected automatically we don't want to change state in this case + if (tabId !== state.currentContentType) { + state.setContentType(tabId); + } }, }), ); @@ -195,47 +204,19 @@ export const TextValuePresentation: TabContainerPanelComponent tab.key === contentType)) { - contentType = 'text/plain'; - } - } - - if (contentType !== state.lastContentType) { - state.setDefaultContentType(contentType); - } - - let currentContentType = state.currentContentType; - - if (data.activeTabs.length > 0 && !data.activeTabs.some(tab => tab.key === currentContentType)) { - currentContentType = data.activeTabs[0].key; + if (data.activeTabs.length > 0 && !data.activeTabs.some(tab => tab.key === state.currentContentType)) { + state.setContentType(data.activeTabs[0].key); } const canSave = !!data.firstSelectedCell && data.contentAction.isDownloadable(data.firstSelectedCell); - const typeExtension = useMemo(() => getTypeExtension(currentContentType) ?? [], [currentContentType]); + const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); - const textValue = useTextValue({ model, resultIndex, - currentContentType, + currentContentType: state.currentContentType, }); - function handleTabOpen(tabId: string) { - // currentContentType may be selected automatically we don't want to change state in this case - if (tabId !== currentContentType) { - state.setContentType(tabId); - } - } - return styled(style)( @@ -243,10 +224,10 @@ export const TextValuePresentation: TabContainerPanelComponent handleTabOpen(tab.tabId)} + onChange={tab => state.handleTabOpen(tab.tabId)} > From e560db5f646af2524eb3dea71d69b463205150cd Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 12:23:34 +0100 Subject: [PATCH 06/18] CB-4285 textValuePresentation canSave in data ref --- .../TextValue/TextValuePresentation.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 bea3b2633f..0c30d2fe63 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -104,6 +104,9 @@ export const TextValuePresentation: TabContainerPanelComponent ({ + get canSave(): boolean { + return Boolean(this.firstSelectedCell && this.contentAction.isDownloadable(this.firstSelectedCell)); + }, get selectAction(): ResultSetSelectAction { return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); }, @@ -188,6 +191,7 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); const textValue = useTextValue({ @@ -240,7 +243,7 @@ export const TextValuePresentation: TabContainerPanelComponent {data.isValueTruncated && } - {canSave && ( + {data && ( + {data.shouldShowPasteButton && ( + - - )} + )} + , ); }, diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts deleted file mode 100644 index 47997dff6d..0000000000 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { isNotNullDefined } from '@cloudbeaver/core-utils'; - -import { isResultSetBinaryFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; -import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; -import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; -import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; -import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; -import { useAutoFormat } from './useAutoFormat'; - -interface IUseTextValueArgs { - resultIndex: number; - model: IDatabaseDataModel; - currentContentType: string; -} - -export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs) { - const format = model.source.getAction(resultIndex, ResultSetFormatAction); - const editor = model.source.getAction(resultIndex, ResultSetEditAction); - const selection = model.source.getAction(resultIndex, ResultSetSelectAction); - const activeElements = selection.getActiveElements(); - const firstSelectedCell = activeElements?.[0]; - const formatter = useAutoFormat(); - - if (!isNotNullDefined(firstSelectedCell)) { - return ''; - } - - if (editor.isElementEdited(firstSelectedCell)) { - return format.getText(firstSelectedCell); - } - - const blob = format.get(firstSelectedCell); - - if (isResultSetBinaryFileValue(blob)) { - const value = formatter.formatBlob(currentContentType, blob); - - if (value) { - return value; - } - } - - return formatter.format(currentContentType, format.getText(firstSelectedCell)); -} diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index fec9e03994..fe6af086c8 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -38,6 +38,8 @@ export default [ ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], + ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], ['data_viewer_script_preview', 'Script'], ['data_viewer_script_preview_dialog_title', 'Preview changes'], ['data_viewer_script_preview_error_title', "Can't get the script"], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index c4ea762cc5..1a84cf6327 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -34,6 +34,8 @@ export default [ ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], + ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], ['data_viewer_refresh_result_set', 'Refresh result set'], ['data_viewer_total_count_tooltip', 'Get total count'], ['data_viewer_total_count_failed', 'Failed to get total count'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index 74e1d0cd72..8a79373a35 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -32,6 +32,8 @@ export default [ ['data_viewer_presentation_value_content_was_truncated', 'Значение было обрезано'], ['data_viewer_presentation_value_content_value_size', 'Размер значения'], ['data_viewer_presentation_value_content_download_error', 'Не удалось загрузить файл'], + ['data_viewer_presentation_value_content_paste_error', 'Не удалось загрузить весь текст'], + ['data_viewer_presentation_value_content_full_text_button', 'Вставить весь текст'], ['data_viewer_script_preview', 'Скрипт'], ['data_viewer_script_preview_dialog_title', 'Предпросмотр изменений'], ['data_viewer_script_preview_error_title', 'Не удалось получить скрипт'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index 2d03d261e8..d5c910db0f 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -38,6 +38,8 @@ export default [ ['data_viewer_presentation_value_content_was_truncated', 'The value was truncated'], ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], + ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], + ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], ['data_viewer_script_preview', '脚本'], ['data_viewer_script_preview_dialog_title', '预览更改'], ['data_viewer_script_preview_error_title', '无法获取脚本'], From ea2985f0e105a87094249ad4c9efcd19313a19c4 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 15:00:11 +0100 Subject: [PATCH 08/18] CB-4285 observable map for full text in textValuePresentation --- .../TextValue/TextValuePresentation.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 b4a6844e1b..a9e87c56f9 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -72,6 +72,10 @@ const styles = css` } `; +const TEXT_PLAIN_TYPE = 'text/plain'; +const TEXT_JSON_TYPE = 'text/json'; +const APPLICATION_JSON_TYPE = 'application/json'; + export const TextValuePresentation: TabContainerPanelComponent> = observer( function TextValuePresentation({ model, resultIndex, dataFormat }) { const translate = useTranslate(); @@ -83,15 +87,15 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ - currentContentType: 'text/plain', + currentContentType: TEXT_PLAIN_TYPE, setContentType(contentType: string) { - if (contentType === 'text/json') { - contentType = 'application/json'; + if (contentType === TEXT_JSON_TYPE) { + contentType = APPLICATION_JSON_TYPE; } if (!data.activeTabs.some(tab => tab.key === contentType)) { - contentType = 'text/plain'; + contentType = TEXT_PLAIN_TYPE; } this.currentContentType = contentType; @@ -105,15 +109,18 @@ export const TextValuePresentation: TabContainerPanelComponent ({ - fullTextCache: new Map(), + // TODO do all reset cases + fullTextCache: observable.map(), get fullText() { - return this.fullTextCache.get(`${this.firstSelectedCell?.row.index}:${this.firstSelectedCell?.column.index}`); + return this.fullTextCache.get(this.fullTextIndex); + }, + get fullTextIndex() { + return `${this.firstSelectedCell?.row.index}:${this.firstSelectedCell?.column.index}`; }, updateFullTextCache(value: string) { - this.fullTextCache.set(`${this.firstSelectedCell?.row.index}:${this.firstSelectedCell?.column.index}`, value); + this.fullTextCache.set(this.fullTextIndex, value); }, get shouldShowPasteButton() { return this.isTextColumn && this.isValueTruncated && !this.fullText && this.fullText !== this.textValue; @@ -270,6 +277,7 @@ export const TextValuePresentation: TabContainerPanelComponent Date: Fri, 12 Jan 2024 15:01:33 +0100 Subject: [PATCH 09/18] CB-4285 reset cases comment removed (all works cool) --- .../ValuePanelPresentation/TextValue/TextValuePresentation.tsx | 1 - 1 file changed, 1 deletion(-) 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 a9e87c56f9..d98f9ab859 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -111,7 +111,6 @@ export const TextValuePresentation: TabContainerPanelComponent ({ - // TODO do all reset cases fullTextCache: observable.map(), get fullText() { return this.fullTextCache.get(this.fullTextIndex); From 171f5a43b82ef3d9d04a1115d0cba2d9b72f77ef Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 15:22:48 +0100 Subject: [PATCH 10/18] =?UTF-8?q?=D0=A1B-4285=20buttons=20in=20tools=20con?= =?UTF-8?q?tainer=20is=20now=20nearby=20each=20other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextValue/TextValuePresentation.m.css | 6 ++++++ .../TextValue/TextValuePresentation.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.m.css diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.m.css b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.m.css new file mode 100644 index 0000000000..e909fb879a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.m.css @@ -0,0 +1,6 @@ +.toolsContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} 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 d98f9ab859..74f55856b3 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; -import { Button, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, s, useObservableRef, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; @@ -31,6 +31,7 @@ import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValu import { QuotaPlaceholder } from '../QuotaPlaceholder'; import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES'; import { getTypeExtension } from './getTypeExtension'; +import moduleStyles from './TextValuePresentation.m.css'; import { TextValuePresentationService } from './TextValuePresentationService'; import { useAutoFormat } from './useAutoFormat'; @@ -84,6 +85,7 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ @@ -326,7 +328,7 @@ export const TextValuePresentation: TabContainerPanelComponent {data.canShowTruncatedQuota && } - +
@@ -335,7 +337,7 @@ export const TextValuePresentation: TabContainerPanelComponent )} - +
, ); }, From 47031b48114691a1d39f454aedd4e9c2c77f4db3 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Fri, 12 Jan 2024 18:13:59 +0100 Subject: [PATCH 11/18] CB-4285 use content action for caching full texts --- .../ResultSet/ResultSetDataContentAction.ts | 45 +++++++++++++++---- .../TextValue/TextValuePresentation.tsx | 17 +++---- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index 1d5aed7e3a..509a273f38 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -26,11 +26,16 @@ import { ResultSetViewAction } from './ResultSetViewAction'; const RESULT_VALUE_PATH = 'sql-result-value'; +interface ICacheEntry { + url?: string; + fullText?: string; +} + @databaseDataAction() export class ResultSetDataContentAction extends DatabaseDataAction implements IResultSetDataContentAction { static dataFormat = [ResultDataFormat.Resultset]; - private readonly cache: Map; + private readonly cache: Map>; activeElement: IResultSetElementKey | null; constructor( @@ -82,11 +87,17 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + const fullText = await this.source.runTask(async () => { try { this.activeElement = element; return await this.loadFileFullText(this.result, column.position, row); @@ -95,7 +106,9 @@ export class ResultSetDataContentAction extends DatabaseDataAction) { + const hash = this.getHash(element); + const cachedElement = this.cache.get(hash); + + if (cachedElement) { + this.cache.set(hash, { ...cachedElement, ...partialCache }); + } + + this.cache.set(hash, partialCache); + } + + retrieveFileFullTextFromCache(element: IResultSetElementKey) { + const hash = this.getHash(element); + return this.cache.get(hash)?.fullText; + } + retrieveFileDataUrlFromCache(element: IResultSetElementKey) { const hash = this.getHash(element); - return this.cache.get(hash); + return this.cache.get(hash)?.url; } async downloadFileData(element: IResultSetElementKey) { 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 74f55856b3..3f6216d422 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -113,16 +113,16 @@ export const TextValuePresentation: TabContainerPanelComponent ({ - fullTextCache: observable.map(), get fullText() { - return this.fullTextCache.get(this.fullTextIndex); + if (!this.firstSelectedCell) { + return ''; + } + + return this.contentAction.retrieveFileFullTextFromCache(this.firstSelectedCell); }, get fullTextIndex() { return `${this.firstSelectedCell?.row.index}:${this.firstSelectedCell?.column.index}`; }, - updateFullTextCache(value: string) { - this.fullTextCache.set(this.fullTextIndex, value); - }, get shouldShowPasteButton() { return this.isTextColumn && this.isValueTruncated && !this.fullText && this.fullText !== this.textValue; }, @@ -165,7 +165,7 @@ export const TextValuePresentation: TabContainerPanelComponent Date: Fri, 12 Jan 2024 18:26:56 +0100 Subject: [PATCH 12/18] CB-3842 use only one row in getting full value --- .../schema/service.sql.graphqls | 16 +++++++++++++-- .../service/sql/DBWServiceSQL.java | 4 ++-- .../service/sql/WebServiceBindingSQL.java | 20 ++++++++++++------- .../service/sql/impl/WebServiceSQL.java | 11 ++++------ 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index ed8b058945..e4db4689e6 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -332,6 +332,7 @@ extend type Mutation { ): String! # Returns BLOB name + @deprecated(reason: "23.3.3") # use sqlReadLobValue readLobValue( projectId: ID, connectionId: ID!, @@ -341,14 +342,25 @@ extend type Mutation { row: [ SQLResultRow! ]! ): String! - # Returns full string value + @since(version: "23.3.3") + sqlReadLobValue( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + lobColumnIndex: Int!, + row: SQLResultRow! + ): String! + + # Returns full string value ignoring any limits + @since(version: "23.3.3") sqlReadStringValue( projectId: ID, connectionId: ID!, contextId: ID!, resultsId: ID!, columnIndex: Int!, - row: [ SQLResultRow! ]! + row: SQLResultRow! ): String! # Returns SQLExecuteInfo 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 180f0614ee..8dc2e4819e 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 @@ -122,7 +122,7 @@ String readLobValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable List row) throws DBWebException; + @Nullable WebSQLResultsRow row) throws DBWebException; @NotNull @WebAction @@ -130,7 +130,7 @@ String getCellValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable List row) throws DBWebException; + @NotNull WebSQLResultsRow row) throws DBWebException; @WebAction String updateResultsDataBatchScript( 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 23b095065c..cc266b8265 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 @@ -131,18 +131,24 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getSQLContext(env), env.getArgument("resultId")); }) - .dataFetcher("readLobValue", env -> - getService(env).readLobValue( - getSQLContext(env), - env.getArgument("resultsId"), - env.getArgument("lobColumnIndex"), - getResultsRow(env, "row"))) + .dataFetcher("readLobValue", env -> // deprecated + getService(env).readLobValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("lobColumnIndex"), + getResultsRow(env, "row").get(0))) + .dataFetcher("sqlReadLobValue", env -> + getService(env).readLobValue( + getSQLContext(env), + env.getArgument("resultsId"), + env.getArgument("lobColumnIndex"), + new WebSQLResultsRow(env.getArgument("row")))) .dataFetcher("sqlReadStringValue", env -> getService(env).getCellValue( getSQLContext(env), env.getArgument("resultsId"), env.getArgument("columnIndex"), - getResultsRow(env, "row"))) + new WebSQLResultsRow(env.getArgument("row")))) .dataFetcher("updateResultsDataBatch", env -> getService(env).updateResultsDataBatch( getSQLContext(env), 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 1d586bbd05..dc4aef0309 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 @@ -332,13 +332,10 @@ public String readLobValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable List row) throws DBWebException + @NotNull WebSQLResultsRow row) throws DBWebException { - if (row == null) { - throw new DBWebException("Results row is not found"); - } ThrowableFunction function = monitor -> contextInfo.getProcessor().readLobValue( - monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)); + monitor, contextInfo, resultsId, lobColumnIndex, row); return readValue(function, contextInfo.getProcessor()); } @@ -348,14 +345,14 @@ public String getCellValue( @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @NotNull Integer lobColumnIndex, - @Nullable List row + @NotNull WebSQLResultsRow row ) throws DBWebException { if (row == null) { throw new DBWebException("Results row is not found"); } WebSQLProcessor processor = contextInfo.getProcessor(); ThrowableFunction function = monitor -> processor.readStringValue( - monitor, contextInfo, resultsId, lobColumnIndex, row.get(0)); + monitor, contextInfo, resultsId, lobColumnIndex, row); return readValue(function, processor); } From 7caa0675cf5c9f4f309b743579ddd6bfc99d0f5f Mon Sep 17 00:00:00 2001 From: Ainur Date: Fri, 12 Jan 2024 18:29:46 +0100 Subject: [PATCH 13/18] CB-3842 use not null at read lob value --- .../src/io/cloudbeaver/service/sql/DBWServiceSQL.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 8dc2e4819e..3dcb4b5f64 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 @@ -119,10 +119,10 @@ WebSQLExecuteInfo updateResultsDataBatch( @WebAction String readLobValue( - @NotNull WebSQLContextInfo contextInfo, - @NotNull String resultsId, - @NotNull Integer lobColumnIndex, - @Nullable WebSQLResultsRow row) throws DBWebException; + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer lobColumnIndex, + @NotNull WebSQLResultsRow row) throws DBWebException; @NotNull @WebAction From 76e3d94b09cae1e3be89fc838eaade54336bf942 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Sun, 14 Jan 2024 01:05:45 +0100 Subject: [PATCH 14/18] CB-4285 schema changes for sqlReadString and sqlReadLobValue --- .../src/queries/grid/getResultsetDataURL.gql | 12 +++--------- .../core-sdk/src/queries/sqlReadStringValue.gql | 2 +- .../Actions/ResultSet/ResultSetDataContentAction.ts | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql b/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql index 91aa5c3ca2..6eec6ae8e7 100644 --- a/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql +++ b/webapp/packages/core-sdk/src/queries/grid/getResultsetDataURL.gql @@ -1,9 +1,3 @@ -mutation getResultsetDataURL($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $lobColumnIndex: Int!, $row: [ SQLResultRow! ]!) { - url: readLobValue( - connectionId: $connectionId, - contextId: $contextId, - resultsId: $resultsId, - lobColumnIndex: $lobColumnIndex, - row: $row - ) -} \ No newline at end of file +mutation getResultsetDataURL($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $lobColumnIndex: Int!, $row: SQLResultRow!) { + url: sqlReadLobValue(connectionId: $connectionId, contextId: $contextId, resultsId: $resultsId, lobColumnIndex: $lobColumnIndex, row: $row) +} diff --git a/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql b/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql index 328fd35eab..ce9b225e37 100644 --- a/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql +++ b/webapp/packages/core-sdk/src/queries/sqlReadStringValue.gql @@ -1,3 +1,3 @@ -mutation getSqlReadStringValue($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $columnIndex: Int!, $row: [SQLResultRow!]!) { +mutation sqlReadStringValue($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $columnIndex: Int!, $row: SQLResultRow!) { text: sqlReadStringValue(connectionId: $connectionId, contextId: $contextId, resultsId: $resultsId, columnIndex: $columnIndex, row: $row) } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index 509a273f38..41d04c5bbb 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -70,7 +70,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction Date: Sun, 14 Jan 2024 11:32:02 +0100 Subject: [PATCH 15/18] CB-4285 TextValuePresentation refactor --- .../Actions/ResultSet/useResultActions.ts | 40 +++ .../TextValue/TextValuePresentation.tsx | 280 +++++------------- .../TextValue/useTextValue.ts | 93 ++++++ 3 files changed, 203 insertions(+), 210 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts create mode 100644 webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts new file mode 100644 index 0000000000..2d1e5edc74 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts @@ -0,0 +1,40 @@ +import type { IDatabaseDataModel } from '../../IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { ResultSetConstraintAction } from './ResultSetConstraintAction'; +import { ResultSetDataAction } from './ResultSetDataAction'; +import { ResultSetDataContentAction } from './ResultSetDataContentAction'; +import { ResultSetEditAction } from './ResultSetEditAction'; +import { ResultSetFormatAction } from './ResultSetFormatAction'; +import { ResultSetSelectAction } from './ResultSetSelectAction'; +import { ResultSetViewAction } from './ResultSetViewAction'; + +interface IResultActionsArgs { + resultIndex: number; + model: IDatabaseDataModel; +} + +export function useResultActions({ model, resultIndex }: IResultActionsArgs) { + return { + get dataAction(): ResultSetDataAction { + return model.source.getAction(resultIndex, ResultSetDataAction); + }, + get selectAction(): ResultSetSelectAction { + return model.source.getAction(resultIndex, ResultSetSelectAction); + }, + get editAction(): ResultSetEditAction { + return model.source.getAction(resultIndex, ResultSetEditAction); + }, + get contentAction(): ResultSetDataContentAction { + return model.source.getAction(resultIndex, ResultSetDataContentAction); + }, + get formatAction(): ResultSetFormatAction { + return model.source.getAction(resultIndex, ResultSetFormatAction); + }, + get constraintAction(): ResultSetConstraintAction { + return model.source.getAction(resultIndex, ResultSetConstraintAction); + }, + get viewAction(): ResultSetViewAction { + return model.source.getAction(resultIndex, ResultSetViewAction); + }, + }; +} 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 3f6216d422..a654800762 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -5,27 +5,23 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, observable } from 'mobx'; +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; -import { Button, s, useObservableRef, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, s, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; import { BASE_TAB_STYLES, TabContainerPanelComponent, TabList, TabsState, UNDERLINE_TAB_STYLES, useTabLocalState } from '@cloudbeaver/core-ui'; -import { bytesToSize, isNotNullDefined } from '@cloudbeaver/core-utils'; +import { bytesToSize } from '@cloudbeaver/core-utils'; import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; -import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; -import { isResultSetBinaryFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; +import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import { ResultSetDataAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; -import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction'; -import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; -import { IResultSetValue, ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +import { useResultActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultActions'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { QuotaPlaceholder } from '../QuotaPlaceholder'; @@ -33,7 +29,7 @@ import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_S import { getTypeExtension } from './getTypeExtension'; import moduleStyles from './TextValuePresentation.m.css'; import { TextValuePresentationService } from './TextValuePresentationService'; -import { useAutoFormat } from './useAutoFormat'; +import { useTextValue } from './useTextValue'; const styles = css` Tab { @@ -84,9 +80,20 @@ export const TextValuePresentation: TabContainerPanelComponent observable({ currentContentType: TEXT_PLAIN_TYPE, @@ -96,7 +103,7 @@ export const TextValuePresentation: TabContainerPanelComponent tab.key === contentType)) { + if (!activeTabs.some(tab => tab.key === contentType)) { contentType = TEXT_PLAIN_TYPE; } @@ -110,197 +117,54 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); + const extensions = useCodemirrorExtensions(undefined, typeExtension); - const data = useObservableRef( - () => ({ - get fullText() { - if (!this.firstSelectedCell) { - return ''; - } - - return this.contentAction.retrieveFileFullTextFromCache(this.firstSelectedCell); - }, - get fullTextIndex() { - return `${this.firstSelectedCell?.row.index}:${this.firstSelectedCell?.column.index}`; - }, - get shouldShowPasteButton() { - return this.isTextColumn && this.isValueTruncated && !this.fullText && this.fullText !== this.textValue; - }, - get columnType(): string | undefined { - if (!this.firstSelectedCell) { - return; - } - - return this.dataAction.getColumn(this.firstSelectedCell.column)?.dataKind; - }, - get isTextColumn(): boolean { - return Boolean(this.columnType?.toLocaleLowerCase() === 'string'); - }, - get textValue() { - if (!isNotNullDefined(this.firstSelectedCell)) { - return ''; - } - - if (this.isTextColumn && this.fullText) { - return this.fullText; - } - - if (this.editAction.isElementEdited(this.firstSelectedCell)) { - return this.formatAction.getText(this.firstSelectedCell); - } - - const blob = this.formatAction.get(this.firstSelectedCell); - - if (isResultSetBinaryFileValue(blob)) { - const value = formatter.formatBlob(state.currentContentType, blob); - - if (value) { - return value; - } - } - - return formatter.format(state.currentContentType, this.formatAction.getText(this.firstSelectedCell)); - }, - get canSave(): boolean { - return Boolean(this.firstSelectedCell && this.contentAction.isDownloadable(this.firstSelectedCell)); - }, - get dataAction(): ResultSetDataAction { - return this.model.source.getAction(this.resultIndex, ResultSetDataAction); - }, - get selectAction(): ResultSetSelectAction { - return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); - }, - get firstSelectedCell(): IResultSetElementKey | undefined { - const activeElements = this.selectAction.getActiveElements(); - - if (activeElements.length === 0) { - return undefined; - } - - return activeElements[0]; - }, - get editAction(): ResultSetEditAction { - return this.model.source.getAction(resultIndex, ResultSetEditAction); - }, - get contentAction(): ResultSetDataContentAction { - return this.model.source.getAction(resultIndex, ResultSetDataContentAction); - }, - get formatAction(): ResultSetFormatAction { - return this.model.source.getAction(resultIndex, ResultSetFormatAction); - }, - get canShowTruncatedQuota(): boolean { - if (this.isValueTruncated && this.isTextColumn) { - return this.fullText !== this.textValue; - } - - return this.isValueTruncated; - }, - get isValueTruncated(): boolean { - if (!this.firstSelectedCell) { - return false; - } - - const value = this.formatAction.get(this.firstSelectedCell); - - if (isResultSetContentValue(value)) { - return this.contentAction.isContentTruncated(value); - } - - return false; - }, - get isReadonly(): boolean { - let readonly = true; - if (this.firstSelectedCell) { - readonly = this.formatAction.isReadOnly(this.firstSelectedCell) || this.formatAction.isBinary(this.firstSelectedCell); - } - - return this.model.isReadonly(this.resultIndex) || this.model.isDisabled(resultIndex) || readonly; - }, - get value(): IResultSetValue | null { - if (this.firstSelectedCell) { - return this.formatAction.get(this.firstSelectedCell); - } - - return null; - }, - get limit(): string | undefined { - if (this.firstSelectedCell && isResultSetContentValue(this.value) && this.isValueTruncated) { - return bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength')); - } - - return; - }, - get valueSize(): string | undefined { - if (this.firstSelectedCell && isResultSetContentValue(this.value) && this.isValueTruncated) { - return bytesToSize(this.value.contentLength ?? 0); - } + function handleChange(newValue: string) { + if (firstSelectedCell && !isReadonly) { + editAction.set(firstSelectedCell, newValue); + } + } - return; - }, - get activeTabs() { - return textValuePresentationService.tabs.getDisplayed({ dataFormat: this.dataFormat, model: this.model, resultIndex: this.resultIndex }); - }, - handleChange(newValue: string) { - if (this.firstSelectedCell && !this.isReadonly) { - this.editAction.set(this.firstSelectedCell, newValue); - } - }, - async save() { - if (!this.firstSelectedCell) { - return; - } + async function save() { + if (!firstSelectedCell) { + return; + } - try { - await this.contentAction.downloadFileData(this.firstSelectedCell); - } catch (exception) { - this.notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); - } - }, - async pasteFullText() { - if (!this.firstSelectedCell) { - return; - } + try { + await contentAction.downloadFileData(firstSelectedCell); + } catch (exception) { + notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error'); + } + } - try { - await this.contentAction.getFileFullText(this.firstSelectedCell); - } catch (exception) { - this.notificationService.logException(exception as any, 'data_viewer_presentation_value_content_paste_error'); - } - }, - }), - { - fullText: computed, - textValue: computed, - canSave: computed, - isTextColumn: computed, - limit: computed, - formatAction: computed, - canShowTruncatedQuota: computed, - fullTextIndex: computed, - shouldShowPasteButton: computed, - selectAction: computed, - columnType: computed, - firstSelectedCell: computed, - editAction: computed, - isReadonly: computed, - value: computed, - activeTabs: computed, - contentAction: computed, - dataAction: computed, - isValueTruncated: computed, - handleChange: action.bound, - save: action.bound, - pasteFullText: action.bound, - }, - { model, resultIndex, dataFormat, notificationService, state }, - ); + async function pasteFullText() { + if (!firstSelectedCell) { + return; + } - if (data.activeTabs.length > 0 && !data.activeTabs.some(tab => tab.key === state.currentContentType)) { - state.setContentType(data.activeTabs[0].key); + try { + await contentAction.getFileFullText(firstSelectedCell); + } catch (exception) { + notificationService.logException(exception as any, 'data_viewer_presentation_value_content_paste_error'); + } } - const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); - const extensions = useCodemirrorExtensions(undefined, typeExtension); + if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === state.currentContentType)) { + state.setContentType(activeTabs[0].key); + } return styled(style)( @@ -317,20 +181,16 @@ export const TextValuePresentation: TabContainerPanelComponent - - {data.canShowTruncatedQuota && } + + {isTruncated && }
- - {data.shouldShowPasteButton && ( - + )} + {shouldShowPasteButton && ( + )} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts new file mode 100644 index 0000000000..1d2106f979 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts @@ -0,0 +1,93 @@ +import { isNotNullDefined } from '@cloudbeaver/core-utils'; + +import { isResultSetBinaryFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; +import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +import { useResultActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultActions'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import { useAutoFormat } from './useAutoFormat'; + +interface IUseTextValueArgs { + resultIndex: number; + model: IDatabaseDataModel; + currentContentType: string; +} + +interface IUseTextValue { + textValue: string; + isFullTextValue: boolean; + isTruncated: boolean; + isTextColumn: boolean; +} + +// TODO tell Ainur about extra space bug +// TODO refactor the logic +// TODO isFullText flag instead of fullTextValue +export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs): IUseTextValue { + const { formatAction, editAction, contentAction, dataAction } = useResultActions({ model, resultIndex }); + const selection = model.source.getAction(resultIndex, ResultSetSelectAction); + const activeElements = selection.getActiveElements(); + const firstSelectedCell = activeElements?.[0]; + const formatter = useAutoFormat(); + + if (!isNotNullDefined(firstSelectedCell)) { + return { + textValue: '', + isFullTextValue: false, + isTruncated: false, + isTextColumn: false, + }; + } + + const contentValue = formatAction.get(firstSelectedCell); + let isTruncated = false; + + if (isResultSetContentValue(contentValue)) { + isTruncated = contentAction.isContentTruncated(contentValue); + } + + const cachedFullText = contentAction.retrieveFileFullTextFromCache(firstSelectedCell); + const columnType = dataAction.getColumn(firstSelectedCell.column)?.dataKind; + const isTextColumn = columnType?.toLocaleLowerCase() === 'string'; + + if (isTextColumn && cachedFullText) { + return { + textValue: cachedFullText, + isFullTextValue: true, + isTruncated, + isTextColumn, + }; + } + + if (editAction.isElementEdited(firstSelectedCell)) { + return { + textValue: formatAction.getText(firstSelectedCell), + isFullTextValue: false, + isTruncated, + isTextColumn, + }; + } + + const blob = formatAction.get(firstSelectedCell); + + if (isResultSetBinaryFileValue(blob)) { + const value = formatter.formatBlob(currentContentType, blob); + + if (value) { + return { + textValue: value, + isFullTextValue: false, + isTruncated, + isTextColumn, + }; + } + } + + return { + textValue: formatter.format(currentContentType, formatAction.getText(firstSelectedCell)), + isFullTextValue: false, + isTruncated, + isTextColumn, + }; +} From 6a78d729ef98bd0b7adecf8064582d4e12aaea0b Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Sun, 14 Jan 2024 12:52:33 +0100 Subject: [PATCH 16/18] CB-4285 useTextValue refactor --- .../TextValue/useTextValue.ts | 65 +++++++------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts index 1d2106f979..b584b6998a 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts @@ -21,73 +21,52 @@ interface IUseTextValue { isTextColumn: boolean; } -// TODO tell Ainur about extra space bug -// TODO refactor the logic -// TODO isFullText flag instead of fullTextValue export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs): IUseTextValue { const { formatAction, editAction, contentAction, dataAction } = useResultActions({ model, resultIndex }); const selection = model.source.getAction(resultIndex, ResultSetSelectAction); const activeElements = selection.getActiveElements(); const firstSelectedCell = activeElements?.[0]; const formatter = useAutoFormat(); + const columnType = firstSelectedCell ? dataAction.getColumn(firstSelectedCell.column)?.dataKind : ''; + const isTextColumn = columnType?.toLocaleLowerCase() === 'string'; + const contentValue = firstSelectedCell ? formatAction.get(firstSelectedCell) : null; + const cachedFullText = firstSelectedCell ? contentAction.retrieveFileFullTextFromCache(firstSelectedCell) : ''; + const blob = firstSelectedCell ? formatAction.get(firstSelectedCell) : null; + const result: IUseTextValue = { + textValue: '', + isFullTextValue: false, + isTruncated: false, + isTextColumn, + }; if (!isNotNullDefined(firstSelectedCell)) { - return { - textValue: '', - isFullTextValue: false, - isTruncated: false, - isTextColumn: false, - }; + return result; } - const contentValue = formatAction.get(firstSelectedCell); - let isTruncated = false; - if (isResultSetContentValue(contentValue)) { - isTruncated = contentAction.isContentTruncated(contentValue); + result.isTruncated = contentAction.isContentTruncated(contentValue); } - const cachedFullText = contentAction.retrieveFileFullTextFromCache(firstSelectedCell); - const columnType = dataAction.getColumn(firstSelectedCell.column)?.dataKind; - const isTextColumn = columnType?.toLocaleLowerCase() === 'string'; - if (isTextColumn && cachedFullText) { - return { - textValue: cachedFullText, - isFullTextValue: true, - isTruncated, - isTextColumn, - }; + result.textValue = cachedFullText; + result.isFullTextValue = true; } if (editAction.isElementEdited(firstSelectedCell)) { - return { - textValue: formatAction.getText(firstSelectedCell), - isFullTextValue: false, - isTruncated, - isTextColumn, - }; + result.textValue = formatAction.getText(firstSelectedCell); } - const blob = formatAction.get(firstSelectedCell); - if (isResultSetBinaryFileValue(blob)) { const value = formatter.formatBlob(currentContentType, blob); if (value) { - return { - textValue: value, - isFullTextValue: false, - isTruncated, - isTextColumn, - }; + result.textValue = value; } } - return { - textValue: formatter.format(currentContentType, formatAction.getText(firstSelectedCell)), - isFullTextValue: false, - isTruncated, - isTextColumn, - }; + if (!result.textValue) { + result.textValue = formatter.format(currentContentType, formatAction.getText(firstSelectedCell)); + } + + return result; } From 36bb38873e08aefb2a309517f32c987587d228a6 Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Mon, 15 Jan 2024 12:11:47 +0100 Subject: [PATCH 17/18] CB-4285 PR comments applied --- .../ResultSet/ResultSetDataContentAction.ts | 9 +-- .../Actions/ResultSet/useResultActions.ts | 62 ++++++++++++------- .../TextValue/TextValuePresentation.tsx | 33 +++------- .../TextValue/useTextValue.ts | 23 +++++++ 4 files changed, 74 insertions(+), 53 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index 41d04c5bbb..3d254d940e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -147,13 +147,8 @@ export class ResultSetDataContentAction extends DatabaseDataAction) { const hash = this.getHash(element); - const cachedElement = this.cache.get(hash); - - if (cachedElement) { - this.cache.set(hash, { ...cachedElement, ...partialCache }); - } - - this.cache.set(hash, partialCache); + const cachedElement = this.cache.get(hash) ?? {}; + this.cache.set(hash, { ...cachedElement, ...partialCache }); } retrieveFileFullTextFromCache(element: IResultSetElementKey) { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts index 2d1e5edc74..0126441793 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/useResultActions.ts @@ -1,3 +1,7 @@ +import { computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; + import type { IDatabaseDataModel } from '../../IDatabaseDataModel'; import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; import { ResultSetConstraintAction } from './ResultSetConstraintAction'; @@ -14,27 +18,41 @@ interface IResultActionsArgs { } export function useResultActions({ model, resultIndex }: IResultActionsArgs) { - return { - get dataAction(): ResultSetDataAction { - return model.source.getAction(resultIndex, ResultSetDataAction); - }, - get selectAction(): ResultSetSelectAction { - return model.source.getAction(resultIndex, ResultSetSelectAction); - }, - get editAction(): ResultSetEditAction { - return model.source.getAction(resultIndex, ResultSetEditAction); - }, - get contentAction(): ResultSetDataContentAction { - return model.source.getAction(resultIndex, ResultSetDataContentAction); - }, - get formatAction(): ResultSetFormatAction { - return model.source.getAction(resultIndex, ResultSetFormatAction); - }, - get constraintAction(): ResultSetConstraintAction { - return model.source.getAction(resultIndex, ResultSetConstraintAction); - }, - get viewAction(): ResultSetViewAction { - return model.source.getAction(resultIndex, ResultSetViewAction); + return useObservableRef( + () => ({ + get dataAction(): ResultSetDataAction { + return this.model.source.getAction(this.resultIndex, ResultSetDataAction); + }, + get selectAction(): ResultSetSelectAction { + return this.model.source.getAction(this.resultIndex, ResultSetSelectAction); + }, + get editAction(): ResultSetEditAction { + return this.model.source.getAction(this.resultIndex, ResultSetEditAction); + }, + get contentAction(): ResultSetDataContentAction { + return this.model.source.getAction(this.resultIndex, ResultSetDataContentAction); + }, + get formatAction(): ResultSetFormatAction { + return this.model.source.getAction(this.resultIndex, ResultSetFormatAction); + }, + get constraintAction(): ResultSetConstraintAction { + return this.model.source.getAction(this.resultIndex, ResultSetConstraintAction); + }, + get viewAction(): ResultSetViewAction { + return this.model.source.getAction(this.resultIndex, ResultSetViewAction); + }, + }), + { + dataAction: computed, + selectAction: computed, + editAction: computed, + contentAction: computed, + formatAction: computed, + constraintAction: computed, + viewAction: computed, + model: observable.ref, + resultIndex: observable.ref, }, - }; + { model, resultIndex }, + ); } 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 a654800762..8c3382b79a 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -15,10 +15,9 @@ import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; import { BASE_TAB_STYLES, TabContainerPanelComponent, TabList, TabsState, UNDERLINE_TAB_STYLES, useTabLocalState } from '@cloudbeaver/core-ui'; -import { bytesToSize } from '@cloudbeaver/core-utils'; +import { bytesToSize, isNotNullDefined } from '@cloudbeaver/core-utils'; import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; -import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import { useResultActions } from '../../DatabaseDataModel/Actions/ResultSet/useResultActions'; @@ -103,21 +102,17 @@ export const TextValuePresentation: TabContainerPanelComponent tab.key === contentType)) { - contentType = TEXT_PLAIN_TYPE; - } - this.currentContentType = contentType; }, handleTabOpen(tabId: string) { // currentContentType may be selected automatically we don't want to change state in this case - if (tabId !== state.currentContentType) { - state.setContentType(tabId); + if (tabId !== this.currentContentType) { + this.setContentType(tabId); } }, }), ); - const { textValue, isFullTextValue, isTruncated, isTextColumn } = useTextValue({ + const { textValue, isFullTextValue, isTruncated, isTextColumn, pasteFullText } = useTextValue({ model, resultIndex, currentContentType: state.currentContentType, @@ -125,7 +120,8 @@ export const TextValuePresentation: TabContainerPanelComponent 0 && !activeTabs.some(tab => tab.key === state.currentContentType)) { - state.setContentType(activeTabs[0].key); + if (!activeTabs.some(tab => tab.key === state.currentContentType)) { + const contentType = activeTabs.length > 0 && activeTabs[0].key ? activeTabs[0].key : TEXT_PLAIN_TYPE; + state.setContentType(contentType); } return styled(style)( diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts index b584b6998a..3ec326949d 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts @@ -1,3 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; import { isNotNullDefined } from '@cloudbeaver/core-utils'; import { isResultSetBinaryFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; @@ -19,6 +28,7 @@ interface IUseTextValue { isFullTextValue: boolean; isTruncated: boolean; isTextColumn: boolean; + pasteFullText(): Promise; } export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs): IUseTextValue { @@ -32,11 +42,24 @@ export function useTextValue({ model, resultIndex, currentContentType }: IUseTex const contentValue = firstSelectedCell ? formatAction.get(firstSelectedCell) : null; const cachedFullText = firstSelectedCell ? contentAction.retrieveFileFullTextFromCache(firstSelectedCell) : ''; const blob = firstSelectedCell ? formatAction.get(firstSelectedCell) : null; + const notificationService = useService(NotificationService); + const result: IUseTextValue = { textValue: '', isFullTextValue: false, isTruncated: false, isTextColumn, + async pasteFullText() { + if (!firstSelectedCell) { + return; + } + + try { + await contentAction.getFileFullText(firstSelectedCell); + } catch (exception) { + notificationService.logException(exception as any, 'data_viewer_presentation_value_content_paste_error'); + } + }, }; if (!isNotNullDefined(firstSelectedCell)) { From 17d98130189af33872cead53e98fae303c5acd2b Mon Sep 17 00:00:00 2001 From: "s.teleshev" Date: Wed, 17 Jan 2024 12:58:35 +0100 Subject: [PATCH 18/18] CB-4285 view full text button for ValuePanel + remove truncated warning if loaded full text --- .../TextValue/TextValuePresentation.tsx | 4 ++-- .../src/ValuePanelPresentation/TextValue/useTextValue.ts | 4 +--- webapp/packages/plugin-data-viewer/src/locales/en.ts | 2 +- webapp/packages/plugin-data-viewer/src/locales/it.ts | 2 +- webapp/packages/plugin-data-viewer/src/locales/ru.ts | 2 +- webapp/packages/plugin-data-viewer/src/locales/zh.ts | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) 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 8c3382b79a..6e96115529 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -112,7 +112,7 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts index 3ec326949d..001a8cf581 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts @@ -25,7 +25,6 @@ interface IUseTextValueArgs { interface IUseTextValue { textValue: string; - isFullTextValue: boolean; isTruncated: boolean; isTextColumn: boolean; pasteFullText(): Promise; @@ -46,7 +45,6 @@ export function useTextValue({ model, resultIndex, currentContentType }: IUseTex const result: IUseTextValue = { textValue: '', - isFullTextValue: false, isTruncated: false, isTextColumn, async pasteFullText() { @@ -72,7 +70,7 @@ export function useTextValue({ model, resultIndex, currentContentType }: IUseTex if (isTextColumn && cachedFullText) { result.textValue = cachedFullText; - result.isFullTextValue = true; + result.isTruncated = false; } if (editAction.isElementEdited(firstSelectedCell)) { diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index fe6af086c8..a6aad623d4 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -39,7 +39,7 @@ export default [ ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], - ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], + ['data_viewer_presentation_value_content_full_text_button', 'View full text'], ['data_viewer_script_preview', 'Script'], ['data_viewer_script_preview_dialog_title', 'Preview changes'], ['data_viewer_script_preview_error_title', "Can't get the script"], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index 1a84cf6327..1e4176ad7e 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -35,7 +35,7 @@ export default [ ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], - ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], + ['data_viewer_presentation_value_content_full_text_button', 'View full text'], ['data_viewer_refresh_result_set', 'Refresh result set'], ['data_viewer_total_count_tooltip', 'Get total count'], ['data_viewer_total_count_failed', 'Failed to get total count'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts index 8a79373a35..82624eea1a 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts @@ -33,7 +33,7 @@ export default [ ['data_viewer_presentation_value_content_value_size', 'Размер значения'], ['data_viewer_presentation_value_content_download_error', 'Не удалось загрузить файл'], ['data_viewer_presentation_value_content_paste_error', 'Не удалось загрузить весь текст'], - ['data_viewer_presentation_value_content_full_text_button', 'Вставить весь текст'], + ['data_viewer_presentation_value_content_full_text_button', 'Посмотреть весь текст'], ['data_viewer_script_preview', 'Скрипт'], ['data_viewer_script_preview_dialog_title', 'Предпросмотр изменений'], ['data_viewer_script_preview_error_title', 'Не удалось получить скрипт'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index d5c910db0f..9d4da4d6ac 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -39,7 +39,7 @@ export default [ ['data_viewer_presentation_value_content_value_size', 'Value size'], ['data_viewer_presentation_value_content_download_error', 'Download failed'], ['data_viewer_presentation_value_content_paste_error', 'Cannot load full text'], - ['data_viewer_presentation_value_content_full_text_button', 'Paste full text'], + ['data_viewer_presentation_value_content_full_text_button', 'View full text'], ['data_viewer_script_preview', '脚本'], ['data_viewer_script_preview_dialog_title', '预览更改'], ['data_viewer_script_preview_error_title', '无法获取脚本'],