CB-4285 read full value from text cell (#2288)

* CB-4494 api for reading full value from text cell

* CB-4494 apply function fix

* CB-4285 graphql getFileFullText for files

* CB-4285 textValuePresentation refactor

* CB-4285 textValuePresentation refactor - removed unneeded fields

* CB-4285 textValuePresentation canSave in data ref

* CB-4285 paste full text button

* CB-4285 observable map for full text in textValuePresentation

* CB-4285 reset cases comment removed (all works cool)

* СB-4285 buttons in tools container is now nearby each other

* CB-4285 use content action for caching full texts

* CB-3842 use only one row in getting full value

* CB-3842 use not null at read lob value

* CB-4285 schema changes for sqlReadString and sqlReadLobValue

* CB-4285 TextValuePresentation refactor

* CB-4285 useTextValue refactor

* CB-4285 PR comments applied

* CB-4285 view full text button for ValuePanel + remove truncated warning if loaded full text

---------

Co-authored-by: s.teleshev <s.teleshev@mu.se>
Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Ainur
2024-01-17 20:02:17 +03:00
committed by GitHub
co-authored by s.teleshev Evgenia Bezborodova
parent ea0493883a
commit 5db30be61b
18 changed files with 493 additions and 208 deletions
@@ -331,7 +331,8 @@ extend type Mutation {
addedRows: [ SQLResultRow! ],
): String!
#Return BLOB name
# Returns BLOB name
@deprecated(reason: "23.3.3") # use sqlReadLobValue
readLobValue(
projectId: ID,
connectionId: ID!,
@@ -341,6 +342,27 @@ extend type Mutation {
row: [ SQLResultRow! ]!
): String!
@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!
): String!
# Returns SQLExecuteInfo
asyncSqlExecuteResults(taskId: ID!): SQLExecuteInfo !
@@ -119,10 +119,18 @@ public interface DBWServiceSQL extends DBWService {
@WebAction
String readLobValue(
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull Integer lobColumnIndex,
@Nullable List<WebSQLResultsRow> row) throws DBWebException;
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull Integer lobColumnIndex,
@NotNull WebSQLResultsRow row) throws DBWebException;
@NotNull
@WebAction
String getCellValue(
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull Integer lobColumnIndex,
@NotNull WebSQLResultsRow row) throws DBWebException;
@WebAction
String updateResultsDataBatchScript(
@@ -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<DBCAttributeMetaData> 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;
}
}
@@ -20,39 +20,29 @@ import io.cloudbeaver.server.CBApplication;
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<DBCAttributeMetaData> 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() {
}
}
@@ -54,6 +54,7 @@ import org.jkiss.utils.CommonUtils;
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 class WebSQLProcessor implements WebSessionProvider {
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<DBDAttributeConstraint> constraints = new ArrayList<>();
for (int i = 0; i < keyAttributes.length; i++) {
@@ -833,14 +847,25 @@ public class WebSQLProcessor implements WebSessionProvider {
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
@@ -131,12 +131,24 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> i
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"),
new WebSQLResultsRow(env.getArgument("row"))))
.dataFetcher("updateResultsDataBatch", env ->
getService(env).updateResultsDataBatch(
getSQLContext(env),
@@ -60,6 +60,9 @@ import java.util.ArrayList;
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,31 +322,62 @@ public class WebServiceSQL implements DBWServiceSQL {
}
}
@FunctionalInterface
private interface ThrowableFunction<T, R> {
R apply(T obj) throws Exception;
}
@Override
public String readLobValue(
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull Integer lobColumnIndex,
@Nullable List<WebSQLResultsRow> row) throws DBWebException
@NotNull WebSQLResultsRow row) throws DBWebException
{
ThrowableFunction<DBRProgressMonitor, String> function = monitor -> contextInfo.getProcessor().readLobValue(
monitor, contextInfo, resultsId, lobColumnIndex, row);
return readValue(function, contextInfo.getProcessor());
}
@NotNull
@Override
public String getCellValue(
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull Integer lobColumnIndex,
@NotNull WebSQLResultsRow row
) throws DBWebException {
if (row == null) {
throw new DBWebException("Results row is not found");
}
WebSQLProcessor processor = contextInfo.getProcessor();
ThrowableFunction<DBRProgressMonitor, String> function = monitor -> processor.readStringValue(
monitor, contextInfo, resultsId, lobColumnIndex, row);
return readValue(function, processor);
}
@NotNull
private String readValue(
@NotNull ThrowableFunction<DBRProgressMonitor, String> 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.apply(monitor));
} 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);
}
}
@@ -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
)
}
mutation getResultsetDataURL($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $lobColumnIndex: Int!, $row: SQLResultRow!) {
url: sqlReadLobValue(connectionId: $connectionId, contextId: $contextId, resultsId: $resultsId, lobColumnIndex: $lobColumnIndex, row: $row)
}
@@ -0,0 +1,3 @@
mutation sqlReadStringValue($connectionId: ID!, $contextId: ID!, $resultsId: ID!, $columnIndex: Int!, $row: SQLResultRow!) {
text: sqlReadStringValue(connectionId: $connectionId, contextId: $contextId, resultsId: $resultsId, columnIndex: $columnIndex, row: $row)
}
@@ -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<any, IDatabaseResultSet> implements IResultSetDataContentAction {
static dataFormat = [ResultDataFormat.Resultset];
private readonly cache: Map<string, string>;
private readonly cache: Map<string, Partial<ICacheEntry>>;
activeElement: IResultSetElementKey | null;
constructor(
@@ -60,6 +65,52 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(this.format.get(element));
}
private async loadFileFullText(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) {
if (!result.id) {
throw new Error("Result's id must be provided");
}
const response = await this.graphQLService.sdk.sqlReadStringValue({
resultsId: result.id,
connectionId: result.connectionId,
contextId: result.contextId,
columnIndex,
row: {
data: row,
},
});
return response.text;
}
async getFileFullText(element: IResultSetElementKey) {
const column = this.data.getColumn(element.column);
const row = this.data.getRowValue(element.row);
const cachedFullText = this.retrieveFileFullTextFromCache(element);
if (cachedFullText) {
return cachedFullText;
}
if (!row || !column) {
throw new Error('Failed to get value metadata information');
}
const fullText = await this.source.runTask(async () => {
try {
this.activeElement = element;
return await this.loadFileFullText(this.result, column.position, row);
} finally {
this.activeElement = null;
}
});
this.updateCache(element, { fullText });
return fullText;
}
async getFileDataUrl(element: IResultSetElementKey) {
const column = this.data.getColumn(element.column);
const row = this.data.getRowValue(element.row);
@@ -82,21 +133,32 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
}
async resolveFileDataUrl(element: IResultSetElementKey) {
const cache = this.retrieveFileDataUrlFromCache(element);
const cachedUrl = this.retrieveFileDataUrlFromCache(element);
if (cache) {
return cache;
if (cachedUrl) {
return cachedUrl;
}
const url = await this.getFileDataUrl(element);
this.cache.set(this.getHash(element), url);
this.updateCache(element, { url });
return url;
}
private updateCache(element: IResultSetElementKey, partialCache: Partial<ICacheEntry>) {
const hash = this.getHash(element);
const cachedElement = this.cache.get(hash) ?? {};
this.cache.set(hash, { ...cachedElement, ...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) {
@@ -0,0 +1,58 @@
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';
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<any, IDatabaseResultSet>;
}
export function useResultActions({ model, resultIndex }: IResultActionsArgs) {
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 },
);
}
@@ -0,0 +1,6 @@
.toolsContainer {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
@@ -10,25 +10,23 @@ 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, 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 } from '@cloudbeaver/core-utils';
import { bytesToSize, isNotNullDefined } from '@cloudbeaver/core-utils';
import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey';
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 { 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';
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 { useTextValue } from './useTextValue';
@@ -70,6 +68,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<IDataValuePanelProps<any, IDatabaseResultSet>> = observer(
function TextValuePresentation({ model, resultIndex, dataFormat }) {
const translate = useTranslate();
@@ -77,75 +79,58 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const quotasService = useService(QuotasService);
const textValuePresentationService = useService(TextValuePresentationService);
const style = useStyles(styles, UNDERLINE_TAB_STYLES, VALUE_PANEL_TOOLS_STYLES);
const moduleStyle = useS(moduleStyles);
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const activeElements = selection.getActiveElements();
const firstSelectedCell = activeElements?.[0];
const activeTabs = textValuePresentationService.tabs.getDisplayed({
dataFormat: dataFormat,
model: model,
resultIndex: resultIndex,
});
const { contentAction, editAction, formatAction } = useResultActions({
model,
resultIndex,
});
const contentValue = firstSelectedCell ? formatAction.get(firstSelectedCell) : null;
const state = useTabLocalState(() =>
observable({
currentContentType: 'text/plain',
lastContentType: 'text/plain',
currentContentType: TEXT_PLAIN_TYPE,
setContentType(type: string) {
this.currentContentType = type;
setContentType(contentType: string) {
if (contentType === TEXT_JSON_TYPE) {
contentType = APPLICATION_JSON_TYPE;
}
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 !== this.currentContentType) {
this.setContentType(tabId);
}
},
}),
);
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const editor = model.source.getAction(resultIndex, ResultSetEditAction);
const content = model.source.getAction(resultIndex, ResultSetDataContentAction);
const activeElements = selection.getActiveElements();
const activeTabs = textValuePresentationService.tabs.getDisplayed({ dataFormat, model, resultIndex });
let contentType = 'text/plain';
let firstSelectedCell: IResultSetElementKey | undefined;
let readonly = true;
let valueTruncated = false;
let limit: string | undefined;
let valueSize: string | undefined;
if (activeElements.length > 0) {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
firstSelectedCell = activeElements[0];
const value = format.get(firstSelectedCell);
readonly = format.isReadOnly(firstSelectedCell) || format.isBinary(firstSelectedCell);
if (isResultSetContentValue(value)) {
valueTruncated = content.isContentTruncated(value);
if (valueTruncated) {
limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength'));
valueSize = bytesToSize(value.contentLength ?? 0);
}
if (value.contentType) {
contentType = value.contentType;
if (contentType === 'text/json') {
contentType = 'application/json';
}
if (!activeTabs.some(tab => tab.key === contentType)) {
contentType = 'text/plain';
}
}
}
}
readonly = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || readonly;
if (contentType !== state.lastContentType) {
state.setDefaultContentType(contentType);
}
const { textValue, isTruncated, isTextColumn, pasteFullText } = useTextValue({
model,
resultIndex,
currentContentType: state.currentContentType,
});
const isSelectedCellReadonly = firstSelectedCell && (formatAction.isReadOnly(firstSelectedCell) || formatAction.isBinary(firstSelectedCell));
const isReadonlyByResultIndex = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || !firstSelectedCell;
const isReadonly = isSelectedCellReadonly || isReadonlyByResultIndex;
const valueSize =
isResultSetContentValue(contentValue) && isNotNullDefined(contentValue.contentLength) ? bytesToSize(contentValue.contentLength) : undefined;
const limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength'));
const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell);
const shouldShowPasteButton = isTextColumn && isTruncated;
const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]);
const extensions = useCodemirrorExtensions(undefined, typeExtension);
function handleChange(newValue: string) {
if (firstSelectedCell && !readonly) {
editor.set(firstSelectedCell, newValue);
if (firstSelectedCell && !isReadonly) {
editAction.set(firstSelectedCell, newValue);
}
}
@@ -155,33 +140,15 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
}
try {
await content.downloadFileData(firstSelectedCell);
await contentAction.downloadFileData(firstSelectedCell);
} catch (exception) {
notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error');
}
}
let currentContentType = state.currentContentType;
if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === currentContentType)) {
currentContentType = activeTabs[0].key;
}
const canSave = !!firstSelectedCell && content.isDownloadable(firstSelectedCell);
const typeExtension = useMemo(() => getTypeExtension(currentContentType) ?? [], [currentContentType]);
const extensions = useCodemirrorExtensions(undefined, typeExtension);
const value = useTextValue({
model,
resultIndex,
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);
}
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)(
@@ -191,23 +158,28 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
dataFormat={dataFormat}
resultIndex={resultIndex}
container={textValuePresentationService.tabs}
currentTabId={currentContentType}
currentTabId={state.currentContentType}
model={model}
lazy
onChange={tab => handleTabOpen(tab.tabId)}
onChange={tab => state.handleTabOpen(tab.tabId)}
>
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} />
</TabsState>
</actions>
<EditorLoader key={readonly ? '1' : '0'} value={value} readonly={readonly} extensions={extensions} onChange={value => handleChange(value)} />
{valueTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
{canSave && (
<tools-container>
<EditorLoader key={isReadonly ? '1' : '0'} value={textValue} readonly={isReadonly} extensions={extensions} onChange={handleChange} />
{isTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
<div className={s(moduleStyle, { toolsContainer: true })}>
{canSave && (
<Button disabled={model.isLoading()} onClick={save}>
{translate('ui_download')}
</Button>
</tools-container>
)}
)}
{shouldShowPasteButton && (
<Button disabled={model.isLoading()} onClick={pasteFullText}>
{translate('data_viewer_presentation_value_content_full_text_button')}
</Button>
)}
</div>
</container>,
);
},
@@ -5,12 +5,14 @@
* 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';
import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction';
import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction';
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';
@@ -21,31 +23,71 @@ interface IUseTextValueArgs {
currentContentType: string;
}
export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs) {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const editor = model.source.getAction(resultIndex, ResultSetEditAction);
interface IUseTextValue {
textValue: string;
isTruncated: boolean;
isTextColumn: boolean;
pasteFullText(): Promise<void>;
}
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 notificationService = useService(NotificationService);
const result: IUseTextValue = {
textValue: '',
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)) {
return '';
return result;
}
if (editor.isElementEdited(firstSelectedCell)) {
return format.getText(firstSelectedCell);
if (isResultSetContentValue(contentValue)) {
result.isTruncated = contentAction.isContentTruncated(contentValue);
}
const blob = format.get(firstSelectedCell);
if (isTextColumn && cachedFullText) {
result.textValue = cachedFullText;
result.isTruncated = false;
}
if (editAction.isElementEdited(firstSelectedCell)) {
result.textValue = formatAction.getText(firstSelectedCell);
}
if (isResultSetBinaryFileValue(blob)) {
const value = formatter.formatBlob(currentContentType, blob);
if (value) {
return value;
result.textValue = value;
}
}
return formatter.format(currentContentType, format.getText(firstSelectedCell));
if (!result.textValue) {
result.textValue = formatter.format(currentContentType, formatAction.getText(firstSelectedCell));
}
return result;
}
@@ -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', '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"],
@@ -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', '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'],
@@ -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', 'Не удалось получить скрипт'],
@@ -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', 'View full text'],
['data_viewer_script_preview', '脚本'],
['data_viewer_script_preview_dialog_title', '预览更改'],
['data_viewer_script_preview_error_title', '无法获取脚本'],