diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg
index 58492acbfe..01304442a5 100644
--- a/webapp/packages/core-app/public/icons/icons.svg
+++ b/webapp/packages/core-app/public/icons/icons.svg
@@ -358,4 +358,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts
index addaf932c7..0c2cd2d2e3 100644
--- a/webapp/packages/core-localization/src/locales/en.ts
+++ b/webapp/packages/core-localization/src/locales/en.ts
@@ -69,6 +69,7 @@ export const defaultENLocale = [
['ui_close_all_to_the_right', 'Close all to the Right'],
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
+ ['ui_download', 'Download'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts
index bc90074f26..1a4290666c 100644
--- a/webapp/packages/core-localization/src/locales/it.ts
+++ b/webapp/packages/core-localization/src/locales/it.ts
@@ -48,6 +48,7 @@ export const defaultITLocale = [
['ui_close_all_to_the_right', 'Close all to the Right'],
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
+ ['ui_download', 'Download'],
['root_permission_denied', 'Non hai i permessi'],
['app_root_session_expire_warning_title', 'La sessione sta per scadere'],
diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts
index e3711fb411..de80dba9ed 100644
--- a/webapp/packages/core-localization/src/locales/ru.ts
+++ b/webapp/packages/core-localization/src/locales/ru.ts
@@ -67,6 +67,7 @@ export const defaultRULocale = [
['ui_close_all_to_the_right', 'Закрыть все справа'],
['ui_close_all_to_the_left', 'Закрыть все слева'],
['ui_or', 'Или'],
+ ['ui_download', 'Загрузить'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts
index 055f284e9a..0bec5655ee 100644
--- a/webapp/packages/core-localization/src/locales/zh.ts
+++ b/webapp/packages/core-localization/src/locales/zh.ts
@@ -69,6 +69,7 @@ export const defaultZHLocale = [
['ui_close_all_to_the_right', 'Close all to the Right'],
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
+ ['ui_download', 'Download'],
['root_permission_denied', '您没有权限'],
['root_permission_no_permission', '您没有权限执行此操作'],
diff --git a/webapp/packages/core-utils/src/download.ts b/webapp/packages/core-utils/src/download.ts
index 0cd0dee02c..e5f452b990 100644
--- a/webapp/packages/core-utils/src/download.ts
+++ b/webapp/packages/core-utils/src/download.ts
@@ -6,11 +6,15 @@
* you may not use this file except in compliance with the License.
*/
-export function download(content: Blob | string, fileName = ''): void {
+export function download(content: Blob | string, fileName = '', blank?: boolean): void {
const saveLink = document.createElement('a');
saveLink.tabIndex = -1;
saveLink.download = fileName;
+ if (blank) {
+ saveLink.target = '_blank';
+ saveLink.rel = 'noopener';
+ }
saveLink.style.display = 'none';
document.body.appendChild(saveLink);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts
index cbf7bccdee..2933995c41 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.ts
@@ -7,8 +7,8 @@
*/
import { injectable } from '@cloudbeaver/core-di';
-import { download } from '@cloudbeaver/core-utils';
-import { DataViewerContentSaverService } from '@cloudbeaver/plugin-data-viewer';
+import { NotificationService } from '@cloudbeaver/core-events';
+import { ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContextMenuService } from './DataGridContextMenuService';
@@ -18,7 +18,7 @@ export class DataGridContextMenuSaveContentService {
constructor(
private readonly dataGridContextMenuService: DataGridContextMenuService,
- private readonly dataViewerContentSaverService: DataViewerContentSaverService
+ private readonly notificationService: NotificationService
) { }
getMenuContentSaveToken(): string {
@@ -37,20 +37,20 @@ export class DataGridContextMenuSaveContentService {
return context.contextType === DataGridContextMenuService.cellContext;
},
onClick: async context => {
- const url = await this.dataViewerContentSaverService.getElementValueURL(
- context.data.model,
- context.data.resultIndex,
- context.data.key
- );
-
- if (url) {
- download(url);
+ try {
+ await context.data.model.source.dataManager.downloadFileData(context.data.key, context.data.resultIndex);
+ } catch (exception: any) {
+ this.notificationService.logException(exception, 'data_grid_table_context_menu_save_value_error');
}
},
- isHidden: context => !this.dataViewerContentSaverService.canSaveElementValue(
- context.data.model,
- context.data.resultIndex,
- context.data.key
+ isHidden: context => !context.data.model.source.dataManager.isContent(
+ context.data.key,
+ context.data.resultIndex
+ ),
+ isDisabled: context => context.data.model.isLoading() || (
+ !!context.data.model.source.dataManager.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(
+ context.data.key, context.data.model.source.dataManager.activeElement
+ )
),
}
);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx
index cd86c37f1f..0a07679c66 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx
@@ -52,7 +52,7 @@ export function useGridSelectionContext(
let selected = true;
const rowsSelection: IResultSetElementKey[][] = [];
- const columnsToSelect: Array = columns.length > 0 ? columns : [undefined];
+ const columnsToSelect: Array = columns.length > 0 ? columns : [undefined];
for (let rowIndex = firstRowIndex; rowIndex <= lastRowIndex; rowIndex++) {
const row = props.tableData.getRow(rowIndex);
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
index edb5ea5183..adaad50dad 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts
@@ -170,7 +170,7 @@ export function useTableData(
return (
!column.editable
- || this.format.isReadOnly(key)
+ || this.format.isReadOnly(key)
);
},
}), {
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts
index f835fca55f..d2b854a353 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts
@@ -19,6 +19,7 @@ export default [
['data_grid_table_tooltip_column_header_order', 'Sort by column'],
['data_grid_table_context_menu_filter_dialog_title', 'Edit value'],
['data_grid_table_context_menu_filter_clipboard_permission', 'Give access to clipboard'],
+ ['data_grid_table_context_menu_save_value_error', 'Failed to save value'],
['data_grid_table_index_column_tooltip', 'Select whole table'],
['data_grid_table_readonly_tooltip', 'Read-only'],
];
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts
index eaac0a29d1..6c82540d8b 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts
@@ -14,6 +14,7 @@ export default [
['data_grid_table_tooltip_column_header_order', 'Ordina per colonna'],
['data_grid_table_context_menu_filter_dialog_title', 'Modifica valore'],
['data_grid_table_context_menu_filter_clipboard_permission', 'Dai accesso agli appunti'],
+ ['data_grid_table_context_menu_save_value_error', 'Failed to save value'],
['data_grid_table_index_column_tooltip', 'Seleziona tutta la tabella'],
['data_grid_table_readonly_tooltip', 'In sola lettura'],
];
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts
index 43576cac2c..360fd1267c 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts
@@ -19,6 +19,7 @@ export default [
['data_grid_table_tooltip_column_header_order', 'Сортировка по колонке'],
['data_grid_table_context_menu_filter_dialog_title', 'Редактировать значение'],
['data_grid_table_context_menu_filter_clipboard_permission', 'Дать доступ к буферу обмена'],
+ ['data_grid_table_context_menu_save_value_error', 'Не удалось сохранить значение'],
['data_grid_table_index_column_tooltip', 'Выбрать всю таблицу'],
['data_grid_table_readonly_tooltip', 'Доступно только для чтения'],
];
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts
index 4bdb95606e..37bb8b2298 100644
--- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts
+++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts
@@ -19,6 +19,7 @@ export default [
['data_grid_table_tooltip_column_header_order', '按列排序'],
['data_grid_table_context_menu_filter_dialog_title', '编辑值'],
['data_grid_table_context_menu_filter_clipboard_permission', '授予访问剪贴板的权限'],
+ ['data_grid_table_context_menu_save_value_error', 'Failed to save value'],
['data_grid_table_index_column_tooltip', '选择整个表'],
['data_grid_table_readonly_tooltip', '只读'],
];
diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts
index b75d304504..c8831bd25c 100644
--- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts
+++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts
@@ -14,7 +14,9 @@ import { AsyncTaskInfoService, GraphQLService, ResultDataFormat, SqlExecuteInfo,
import { DocumentEditAction } from './DatabaseDataModel/Actions/Document/DocumentEditAction';
import { ResultSetEditAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetEditAction';
+import { DatabaseDataManager } from './DatabaseDataModel/DatabaseDataManager';
import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource';
+import type { IDatabaseDataManager } from './DatabaseDataModel/IDatabaseDataManager';
import type { IDatabaseDataOptions } from './DatabaseDataModel/IDatabaseDataOptions';
import type { IDatabaseResultSet } from './DatabaseDataModel/IDatabaseResultSet';
@@ -24,6 +26,7 @@ export interface IDataContainerOptions extends IDatabaseDataOptions {
export class ContainerDataSource extends DatabaseDataSource {
currentTask: ITask | null;
+ dataManager: IDatabaseDataManager;
get canCancel(): boolean {
return this.currentTask?.cancellable || false;
@@ -36,6 +39,7 @@ export class ContainerDataSource extends DatabaseDataSource {
static dataFormat = [ResultDataFormat.Resultset];
- private view: ResultSetViewAction;
- private edit: ResultSetEditAction;
+ private readonly view: ResultSetViewAction;
+ private readonly edit: ResultSetEditAction;
constructor(
source: IDatabaseDataSource,
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
index d8b5a02410..42ab68ca90 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts
@@ -36,10 +36,10 @@ export class ResultSetSelectAction extends DatabaseSelectAction;
private focusedElement: IResultSetElementKey | null;
- private view: ResultSetViewAction;
- private edit: ResultSetEditAction;
- private data: ResultSetDataAction;
- private validationDisposer: IReactionDisposer;
+ private readonly view: ResultSetViewAction;
+ private readonly edit: ResultSetEditAction;
+ private readonly data: ResultSetDataAction;
+ private readonly validationDisposer: IReactionDisposer;
constructor(
source: IDatabaseDataSource,
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts
index bfff2aa6b3..c7a2450498 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts
@@ -47,8 +47,8 @@ export class ResultSetViewAction extends DatabaseDataAction,
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts
index bdd3d715ab..b813ec0940 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetContentValue.ts
@@ -8,9 +8,15 @@
import type { IResultSetContentValue } from './IResultSetContentValue';
+const TRUNCATED_LIMIT = 1048576;
+
export function isResultSetContentValue(value: any): value is IResultSetContentValue {
return value !== null
&& typeof value === 'object'
&& '$type' in value
&& value.$type === 'content';
}
+
+export function isResultSetContentValueTruncated(value: IResultSetContentValue) {
+ return (value.contentLength ?? 0) > TRUNCATED_LIMIT;
+}
\ No newline at end of file
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataManager.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataManager.ts
new file mode 100644
index 0000000000..5213354983
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataManager.ts
@@ -0,0 +1,127 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2022 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 { makeObservable, observable } from 'mobx';
+
+import type { GraphQLService } from '@cloudbeaver/core-sdk';
+import { download, GlobalConstants } from '@cloudbeaver/core-utils';
+
+import type { IResultSetElementKey } from './Actions/ResultSet/IResultSetDataKey';
+import { isResultSetContentValue } from './Actions/ResultSet/isResultSetContentValue';
+import { ResultSetDataAction } from './Actions/ResultSet/ResultSetDataAction';
+import { ResultSetDataKeysUtils } from './Actions/ResultSet/ResultSetDataKeysUtils';
+import type { IResultSetValue } from './Actions/ResultSet/ResultSetFormatAction';
+import { ResultSetViewAction } from './Actions/ResultSet/ResultSetViewAction';
+import type { IDatabaseDataManager } from './IDatabaseDataManager';
+import type { IDatabaseDataSource } from './IDatabaseDataSource';
+import type { IDatabaseResultSet } from './IDatabaseResultSet';
+
+const RESULT_VALUE_PATH = 'sql-result-value';
+
+export class DatabaseDataManager implements IDatabaseDataManager {
+ private readonly cache: Map;
+ activeElement: IResultSetElementKey | null;
+
+ constructor(
+ private readonly graphQLService: GraphQLService,
+ private readonly source: IDatabaseDataSource
+ ) {
+ this.cache = new Map();
+ this.activeElement = null;
+
+ makeObservable(this, {
+ cache: observable,
+ activeElement: observable.ref,
+ });
+ }
+
+ isContent(element: IResultSetElementKey, resultIndex: number) {
+ const view = this.source.getAction(resultIndex, ResultSetViewAction);
+ const cellValue = view.getCellValue(element);
+
+ return isResultSetContentValue(cellValue);
+ }
+
+ async getFileDataUrl(element: IResultSetElementKey, resultIndex: number) {
+ const result = this.source.getResult(resultIndex);
+ const data = this.source.getAction(resultIndex, ResultSetDataAction);
+ const column = data.getColumn(element.column);
+ const row = data.getRowValue(element.row);
+
+ if (!result || !row || !column) {
+ throw new Error('Failed to get value metadata information');
+ }
+
+ const url = await this.source.runTask(
+ async () => {
+ try {
+ this.activeElement = element;
+ const fileName = await this.loadFileName(result, column.position, row);
+ return this.generateFileDataUrl(fileName);
+ } finally {
+ this.activeElement = null;
+ }
+ }
+ );
+
+ return url;
+ }
+
+ async resolveFileDataUrl(element: IResultSetElementKey, resultIndex: number) {
+ const cache = this.retrieveFileDataUrlFromCache(element, resultIndex);
+
+ if (cache) {
+ return cache;
+ }
+
+ const url = await this.getFileDataUrl(element, resultIndex);
+ this.cache.set(this.getHash(element, resultIndex), url);
+
+ return url;
+ }
+
+ retrieveFileDataUrlFromCache(element: IResultSetElementKey, resultIndex: number) {
+ const hash = this.getHash(element, resultIndex);
+ return this.cache.get(hash);
+ }
+
+ async downloadFileData(element: IResultSetElementKey, resultIndex: number) {
+ const url = await this.getFileDataUrl(element, resultIndex);
+ download(url);
+ }
+
+ clearCache() {
+ this.cache.clear();
+ }
+
+ private generateFileDataUrl(fileName: string) {
+ return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${fileName}`;
+ }
+
+ private getHash(element: IResultSetElementKey, resultIndex: number) {
+ return `${ResultSetDataKeysUtils.serializeElementKey(element)}_${resultIndex}`;
+ }
+
+ private async loadFileName(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) {
+ if (!result.id) {
+ throw new Error("Result's id must be provided");
+ }
+
+ const response = await this.graphQLService.sdk.getResultsetDataURL({
+ resultsId: result.id,
+ connectionId: result.connectionId,
+ contextId: result.contextId,
+ lobColumnIndex: columnIndex,
+ row: {
+ data: row,
+ },
+ });
+
+ return response.url;
+ }
+}
\ No newline at end of file
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
index 1428c1793d..1cd85897be 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts
@@ -14,11 +14,13 @@ import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DatabaseDataActions } from './DatabaseDataActions';
import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction';
import type { IDatabaseDataActions } from './IDatabaseDataActions';
+import type { IDatabaseDataManager } from './IDatabaseDataManager';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource';
export abstract class DatabaseDataSource
implements IDatabaseDataSource {
+ abstract readonly dataManager: IDatabaseDataManager;
access: DatabaseDataAccessMode;
dataFormat: ResultDataFormat;
supportedDataFormats: ResultDataFormat[];
@@ -171,6 +173,7 @@ export abstract class DatabaseDataSource boolean;
+ getFileDataUrl: (element: IResultSetElementKey, resultIndex: number) => Promise;
+ resolveFileDataUrl: (element: IResultSetElementKey, resultIndex: number) => Promise;
+ retrieveFileDataUrlFromCache: (element: IResultSetElementKey, resultIndex: number) => string | undefined;
+ downloadFileData: (element: IResultSetElementKey, resultIndex: number) => Promise;
+ clearCache: () => void;
+}
\ No newline at end of file
diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
index df6142c9fc..9380587968 100644
--- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
+++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts
@@ -9,8 +9,10 @@
import type { IConnectionExecutionContext } from '@cloudbeaver/core-connections';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
+import type { DatabaseDataManager } from './DatabaseDataManager';
import type { IDatabaseDataAction, IDatabaseDataActionClass, IDatabaseDataActionInterface } from './IDatabaseDataAction';
import type { IDatabaseDataActions } from './IDatabaseDataActions';
+import type { IDatabaseDataManager } from './IDatabaseDataManager';
import type { IDatabaseDataResult } from './IDatabaseDataResult';
export interface IRequestInfo {
@@ -26,6 +28,7 @@ export enum DatabaseDataAccessMode {
}
export interface IDatabaseDataSource {
+ readonly dataManager: IDatabaseDataManager;
readonly access: DatabaseDataAccessMode;
readonly dataFormat: ResultDataFormat;
readonly supportedDataFormats: ResultDataFormat[];
diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ContentLoader.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ContentLoader.tsx
new file mode 100644
index 0000000000..6dc8445bad
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ContentLoader.tsx
@@ -0,0 +1,49 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2022 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+import { observer } from 'mobx-react-lite';
+import styled, { css } from 'reshadow';
+
+import { Button } from '@cloudbeaver/core-blocks';
+import { useTranslate } from '@cloudbeaver/core-localization';
+
+interface Props {
+ disabled?: boolean;
+ loading?: boolean;
+ className?: string;
+ onLoad: () => void;
+}
+
+const style = css`
+ loader {
+ margin: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ }
+ loader-label {
+ text-align: center;
+ }
+`;
+
+export const ContentLoader = observer(function ContentLoader({ disabled, loading, className, onLoad, children }) {
+ const translate = useTranslate();
+ return styled(style)(
+
+ {children}
+
+
+ );
+});
\ No newline at end of file
diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx
index 955f7feba5..3d2522a1d1 100644
--- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx
+++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx
@@ -6,21 +6,26 @@
* you may not use this file except in compliance with the License.
*/
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
-import { useState } from 'react';
import styled, { css, use } from 'reshadow';
-import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
-import { Button } from '@cloudbeaver/core-blocks';
+import { IconOrImage, useObservableRef } from '@cloudbeaver/core-blocks';
+import { useService } from '@cloudbeaver/core-di';
+import { NotificationService } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
-import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
+import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
+import { download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils';
-import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
+import { isResultSetContentValue, isResultSetContentValueTruncated } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
+import { ResultSetDataKeysUtils } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataKeysUtils';
import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction';
import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction';
import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet';
import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService';
+import { ContentLoader } from '../ContentLoader';
+import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES';
const styles = css`
img {
@@ -36,17 +41,11 @@ const styles = css`
container {
display: flex;
+ gap: 16px;
flex: 1;
flex-direction: column;
}
-
- tools {
- display: flex;
- flex: 0;
- justify-content: flex-end;
- padding-bottom: 16px;
- }
-
+
image {
flex: 1;
display: flex;
@@ -54,40 +53,166 @@ const styles = css`
}
`;
+interface IToolsProps {
+ loading?: boolean;
+ stretch?: boolean;
+ onToggleStretch?: () => void;
+ onSave?: () => void;
+}
+
+const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave }) {
+ const translate = useTranslate();
+
+ return styled(VALUE_PANEL_TOOLS_STYLES)(
+
+ {onSave && (
+
+
+
+
+
+ )}
+ {onToggleStretch && (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+});
+
export const ImageValuePresentation: TabContainerPanelComponent> = observer(function ImageValuePresentation({
model,
resultIndex,
}) {
const translate = useTranslate();
- const [stretch, setStretch] = useState(false);
- const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
+ const notificationService = useService(NotificationService);
+ const style = useStyles(styles);
- const focusCell = selection.getFocusedElement();
+ const state = useObservableRef(() => ({
+ get selectedCell() {
+ const selection = this.model.source.getAction(this.resultIndex, ResultSetSelectAction);
+ const focusCell = selection.getFocusedElement();
- let src: string | undefined;
+ return selection.elements[0] || focusCell;
+ },
+ get cellValue() {
+ const view = this.model.source.getAction(this.resultIndex, ResultSetViewAction);
+ const cellValue = view.getCellValue(this.selectedCell);
- if (selection.elements.length > 0 || focusCell) {
- const view = model.source.getAction(resultIndex, ResultSetViewAction);
- const firstSelectedCell = selection.elements[0] || focusCell;
+ return cellValue;
+ },
+ get src() {
+ if (this.savedSrc) {
+ return this.savedSrc;
+ }
- const cellValue = view.getCellValue(firstSelectedCell);
+ if (isResultSetContentValue(this.cellValue) && this.cellValue.binary) {
+ return `data:${getMIME(this.cellValue.binary)};base64,${this.cellValue.binary}`;
+ } else if (typeof this.cellValue === 'string' && isValidUrl(this.cellValue) && isImageFormat(this.cellValue)) {
+ return this.cellValue;
+ }
- if (isResultSetContentValue(cellValue) && cellValue.binary) {
- src = `data:${getMIME(cellValue.binary)};base64,${cellValue.binary}`;
- } else if (typeof cellValue === 'string' && isValidUrl(cellValue) && isImageFormat(cellValue)) {
- src = cellValue;
- }
+ return '';
+ },
+ get savedSrc() {
+ return this.model.source.dataManager.retrieveFileDataUrlFromCache(this.selectedCell, this.resultIndex);
+ },
+ get content() {
+ return this.model.source.dataManager.isContent(this.selectedCell, this.resultIndex);
+ },
+ get canSave() {
+ return this.content || !!this.src;
+ },
+ get truncated() {
+ return isResultSetContentValue(this.cellValue) && isResultSetContentValueTruncated(this.cellValue);
+ },
+ stretch: false,
+ toggleStretch() {
+ this.stretch = !this.stretch;
+ },
+ async save() {
+ try {
+ if (!this.content) {
+ download(this.src, '', true);
+ } else {
+ await this.model.source.dataManager.downloadFileData(this.selectedCell, this.resultIndex);
+ }
+ } catch (exception: any) {
+ this.notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error');
+ }
+ },
+ }), {
+ selectedCell: computed,
+ cellValue: computed,
+ src: computed,
+ savedSrc: computed,
+ canSave: computed,
+ truncated: computed,
+ stretch: observable.ref,
+ toggleStretch: action.bound,
+ save: action.bound,
+ }, { model, resultIndex, notificationService });
+
+ const save = state.canSave ? state.save : undefined;
+ const loading = model.isLoading();
+
+ if (state.truncated && !state.savedSrc) {
+ const load = async () => {
+ try {
+ await model.source.dataManager.resolveFileDataUrl(state.selectedCell, resultIndex);
+ } catch (exception: any) {
+ notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error');
+ }
+ };
+
+ return styled(style)(
+
+
+ {translate('data_viewer_presentation_value_content_trimmed_placeholder')}
+
+
+
+ );
}
- return styled(useStyles(styles))(
+ return styled(style)(
-
-
-
-
-
+
+
);
-});
+});
\ No newline at end of file
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 e344c2b6ea..f94fd16879 100644
--- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx
+++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx
@@ -10,8 +10,10 @@ import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
-import { BASE_CONTAINERS_STYLES, Textarea, useObservableRef } from '@cloudbeaver/core-blocks';
+import { BASE_CONTAINERS_STYLES, IconOrImage, Textarea, useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
+import { NotificationService } from '@cloudbeaver/core-events';
+import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { BASE_TAB_STYLES, TabContainerPanelComponent, TabList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
import { CodeEditorLoader } from '@cloudbeaver/plugin-codemirror';
@@ -24,6 +26,7 @@ import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet
import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction';
import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet';
import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService';
+import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES';
import { TextValuePresentationService } from './TextValuePresentationService';
const styles = css`
@@ -32,6 +35,7 @@ const styles = css`
}
container {
display: flex;
+ gap: 16px;
flex-direction: column;
overflow: auto;
flex: 1;
@@ -69,8 +73,10 @@ export const TextValuePresentation: TabContainerPanelComponent ({
currentContentType: 'text/plain',
lastContentType: 'text/plain',
@@ -129,14 +135,27 @@ export const TextValuePresentation: TabContainerPanelComponent {
+ function handleChange(newValue: string) {
if (firstSelectedCell && !readonly) {
editor.set(firstSelectedCell, newValue);
}
- };
+ }
+
+ async function save() {
+ if (!firstSelectedCell) {
+ return;
+ }
+
+ try {
+ await model.source.dataManager.downloadFileData(firstSelectedCell, resultIndex);
+ } catch (exception) {
+ notificationService.logException(exception as any, 'data_viewer_presentation_value_content_download_error');
+ }
+ }
const useCodeEditor = state.currentContentType !== 'text/plain';
const autoFormat = firstSelectedCell && !editor.isElementEdited(firstSelectedCell);
+ const canSave = firstSelectedCell && model.source.dataManager.isContent(firstSelectedCell, resultIndex);
return styled(style)(
@@ -176,6 +195,20 @@ export const TextValuePresentation: TabContainerPanelComponent
)}
+ {canSave && (
+
+
+
+
+
+
+
+ )}
);
});
diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts
new file mode 100644
index 0000000000..a68b613c57
--- /dev/null
+++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ValuePanelTools/VALUE_PANEL_TOOLS_STYLES.ts
@@ -0,0 +1,36 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2022 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 { css } from 'reshadow';
+
+export const VALUE_PANEL_TOOLS_STYLES = css`
+ tools-container {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ }
+ tools {
+ composes: theme-background-surface theme-text-on-surface theme-border-color-background theme-form-element-radius from global;
+ display: flex;
+ box-sizing: border-box;
+ border: 2px solid;
+ }
+ tools-action {
+ composes: theme-ripple from global;
+ box-sizing: border-box;
+ background: inherit;
+ cursor: pointer;
+ padding: 4px;
+ width: 24px;
+ height: 100%;
+ }
+ IconOrImage {
+ width: 100%;
+ height: 100%;
+ }
+`;
\ No newline at end of file
diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts
index e3982fc049..12ecac1c8e 100644
--- a/webapp/packages/plugin-data-viewer/src/index.ts
+++ b/webapp/packages/plugin-data-viewer/src/index.ts
@@ -38,7 +38,8 @@ export * from './DatabaseDataModel/IDatabaseDataSource';
export * from './DatabaseDataModel/IDatabaseResultSet';
export * from './DatabaseDataModel/Order';
export * from './DataViewerService';
-export * from './DataViewerContentSaverService';
+export * from './DatabaseDataModel/DatabaseDataManager';
+export * from './DatabaseDataModel/IDatabaseDataManager';
// All Services and Components that is provided by this plugin should be exported here
export * from './TableViewer/TableViewerStorageService';
diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts
index 4b048609ac..688573780d 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/en.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts
@@ -27,6 +27,8 @@ export default [
['data_viewer_presentation_value_image_fit', 'Fit Window'],
['data_viewer_presentation_value_image_original_size', 'Original Size'],
['data_viewer_presentation_value_boolean_placeholder', "Can't show current value as boolean"],
+ ['data_viewer_presentation_value_content_trimmed_placeholder', 'The size of the value exceeds the limit'],
+ ['data_viewer_presentation_value_content_download_error', 'Download failed'],
['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 4fe9d282da..4098f486d5 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/it.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts
@@ -23,5 +23,7 @@ export default [
['data_viewer_presentation_value_image_fit', 'Adatta alla Finestra'],
['data_viewer_presentation_value_image_original_size', 'Dimensioni Originali'],
['data_viewer_presentation_value_boolean_placeholder', 'Non posso rappresentare il valore corrente come booleano'],
+ ['data_viewer_presentation_value_content_trimmed_placeholder', 'The size of the value exceeds the limit'],
+ ['data_viewer_presentation_value_content_download_error', 'Download failed'],
['data_viewer_refresh_result_set', 'Refresh result set'],
];
diff --git a/webapp/packages/plugin-data-viewer/src/locales/ru.ts b/webapp/packages/plugin-data-viewer/src/locales/ru.ts
index f6cef7db1e..14fcd04a56 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/ru.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/ru.ts
@@ -23,6 +23,8 @@ export default [
['data_viewer_presentation_value_image_fit', 'Растянуть'],
['data_viewer_presentation_value_image_original_size', 'Оригинальный размер'],
['data_viewer_presentation_value_boolean_placeholder', 'Не удалось отобразить текущее значение как boolean'],
+ ['data_viewer_presentation_value_content_trimmed_placeholder', 'Размер значения превышает лимит'],
+ ['data_viewer_presentation_value_content_download_error', 'Не удалось загрузить файл'],
['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 83244020a4..55692b3e87 100644
--- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts
+++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts
@@ -27,6 +27,8 @@ export default [
['data_viewer_presentation_value_image_fit', '适应窗口'],
['data_viewer_presentation_value_image_original_size', '原始尺寸'],
['data_viewer_presentation_value_boolean_placeholder', '无法将当前值显示为布尔值'],
+ ['data_viewer_presentation_value_content_trimmed_placeholder', 'The size of the value exceeds the limit'],
+ ['data_viewer_presentation_value_content_download_error', 'Download failed'],
['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/manifest.ts b/webapp/packages/plugin-data-viewer/src/manifest.ts
index c06b7471e8..641fb6fdeb 100644
--- a/webapp/packages/plugin-data-viewer/src/manifest.ts
+++ b/webapp/packages/plugin-data-viewer/src/manifest.ts
@@ -10,7 +10,6 @@ import type { PluginManifest } from '@cloudbeaver/core-di';
import { DataPresentationService } from './DataPresentationService';
import { DataViewerBootstrap } from './DataViewerBootstrap';
-import { DataViewerContentSaverService } from './DataViewerContentSaverService';
import { DataViewerDataChangeConfirmationService } from './DataViewerDataChangeConfirmationService';
import { DataViewerService } from './DataViewerService';
import { DataViewerSettingsService } from './DataViewerSettingsService';
@@ -52,6 +51,5 @@ export const manifest: PluginManifest = {
DataValuePanelBootstrap,
DataViewerSettingsService,
DataViewerService,
- DataViewerContentSaverService,
],
};
diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts
index 6b172d8295..363c64fad3 100644
--- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts
+++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts
@@ -11,7 +11,7 @@ import { observable, makeObservable } from 'mobx';
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import type { ITask } from '@cloudbeaver/core-executor';
import { AsyncTaskInfoService, GraphQLService, ResultDataFormat, SqlExecuteInfo, SqlQueryResults, UpdateResultsDataBatchMutationVariables } from '@cloudbeaver/core-sdk';
-import { DatabaseDataSource, DocumentEditAction, IDatabaseDataOptions, IDatabaseResultSet, IRequestInfo, ResultSetEditAction } from '@cloudbeaver/plugin-data-viewer';
+import { DatabaseDataManager, DatabaseDataSource, DocumentEditAction, IDatabaseDataManager, IDatabaseDataOptions, IDatabaseResultSet, IRequestInfo, ResultSetEditAction } from '@cloudbeaver/plugin-data-viewer';
export interface IDataQueryOptions extends IDatabaseDataOptions {
query: string;
@@ -22,6 +22,7 @@ export interface IQueryRequestInfo extends IRequestInfo {
}
export class QueryDataSource extends DatabaseDataSource {
+ readonly dataManager: IDatabaseDataManager;
currentTask: ITask | null;
requestInfo: IQueryRequestInfo;
@@ -35,6 +36,7 @@ export class QueryDataSource extends DatabaseDataSource