mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #766 from dbeaver/feat/lob
feat(plugin-data-viewer): CB-1500 possibility to save and see large lobs
This commit is contained in:
@@ -358,4 +358,18 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.5028 23.3973L21.5948 23.4523L22.4458 22.0583H21.5018L21.5028 23.3973Z" fill="#338ECC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.7214 22.053L25.5024 22.055L22.4454 22.058L21.5954 23.453L21.5024 23.397L21.5054 26.587L21.5074 29.635H22.2054L29.0844 29.63L29.0904 22.053H28.7214Z" fill="#D7351F"/>
|
||||
</symbol>
|
||||
<symbol id="img-fit-size" viewBox="0 0 32 32">
|
||||
<g clip-path="url(#clip0_3722_1285)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12.0001L3 5.82851L9.8996 12.7281L12.728 9.89968L5.82835 3L12.0001 3V0H0.000105858V8.2016e-05H0V12.0001H3ZM20 3.00206L26.1714 3.00206L19.2719 9.90155L22.1003 12.73L29 5.8303L29 12.0021H32V3.00206V0.00205817L29.1714 0.00205814L29 0.00205813L20 0.00205707V3.00206Z" fill="#338ECC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 20L29 26.1714L22.1005 19.2719L19.2721 22.1003L26.1718 29L20 29L20 32L29 32L32 32L32 29.1714L32 29L32 20L29 20ZM12.0001 28.9981L5.8287 28.9981L12.7282 22.0986L9.89976 19.2702L3.00008 26.1699L3.00008 19.9981L8.30652e-05 19.9981L8.22784e-05 28.9981L8.20162e-05 31.9981L2.8287 31.9981L3.00008 31.9981L12.0001 31.9981L12.0001 28.9981Z" fill="#338ECC"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3722_1285">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</symbol>
|
||||
<symbol id="img-original-size" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.00049 9.9977L7.17187 9.9977L0.27238 3.0982L3.10081 0.269775L10.0005 7.16946L10.0005 0.997697L13.0005 0.997697V9.9977V12.9977H10.1719H10.0005L1.00049 12.9977L1.00049 9.9977ZM22 0.999838L22 7.17122L28.8995 0.27173L31.7279 3.10016L24.8282 9.99984L31 9.99984V12.9998H22H19V10.1712V9.99984V0.999838L22 0.999838ZM22.0002 30.9998L22.0002 24.8284L28.8997 31.7279L31.7281 28.8995L24.8284 21.9998H31.0002V18.9998H22.0002H19.0002V21.8284V21.9998V30.9998H22.0002ZM7.17147 21.9999L1.00008 21.9999L1.00008 18.9999H10.0001H10.1715H13.0001V21.9999V30.9999H10.0001L10.0001 24.8281L3.1004 31.7278L0.271973 28.8994L7.17147 21.9999Z" fill="#338ECC"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
@@ -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"],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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', 'У вас нет разрешения на это действие'],
|
||||
|
||||
@@ -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', '您没有权限执行此操作'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+15
-15
@@ -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
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export function useGridSelectionContext(
|
||||
|
||||
let selected = true;
|
||||
const rowsSelection: IResultSetElementKey[][] = [];
|
||||
const columnsToSelect: Array<IResultSetColumnKey| undefined> = columns.length > 0 ? columns : [undefined];
|
||||
const columnsToSelect: Array<IResultSetColumnKey | undefined> = columns.length > 0 ? columns : [undefined];
|
||||
|
||||
for (let rowIndex = firstRowIndex; rowIndex <= lastRowIndex; rowIndex++) {
|
||||
const row = props.tableData.getRow(rowIndex);
|
||||
|
||||
@@ -170,7 +170,7 @@ export function useTableData(
|
||||
|
||||
return (
|
||||
!column.editable
|
||||
|| this.format.isReadOnly(key)
|
||||
|| this.format.isReadOnly(key)
|
||||
);
|
||||
},
|
||||
}), {
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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', 'Доступно только для чтения'],
|
||||
];
|
||||
|
||||
@@ -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', '只读'],
|
||||
];
|
||||
|
||||
@@ -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<IDataContainerOptions, IDatabaseResultSet> {
|
||||
currentTask: ITask<SqlExecuteInfo> | null;
|
||||
dataManager: IDatabaseDataManager;
|
||||
|
||||
get canCancel(): boolean {
|
||||
return this.currentTask?.cancellable || false;
|
||||
@@ -36,6 +39,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
) {
|
||||
super();
|
||||
|
||||
this.dataManager = new DatabaseDataManager(this.graphQLService, this);
|
||||
this.currentTask = null;
|
||||
this.executionContext = null;
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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 { injectable } from '@cloudbeaver/core-di';
|
||||
import { GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { GlobalConstants } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IResultSetElementKey } from './DatabaseDataModel/Actions/ResultSet/IResultSetDataKey';
|
||||
import { isResultSetContentValue } from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
|
||||
import { ResultSetDataAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction';
|
||||
import { ResultSetViewAction } from './DatabaseDataModel/Actions/ResultSet/ResultSetViewAction';
|
||||
import type { IDatabaseDataModel } from './DatabaseDataModel/IDatabaseDataModel';
|
||||
|
||||
const RESULT_VALUE_PATH = 'sql-result-value';
|
||||
|
||||
@injectable()
|
||||
export class DataViewerContentSaverService {
|
||||
|
||||
constructor(
|
||||
private readonly graphQLService: GraphQLService
|
||||
) { }
|
||||
|
||||
canSaveElementValue(
|
||||
model: IDatabaseDataModel,
|
||||
resultIndex: number,
|
||||
element: IResultSetElementKey
|
||||
) {
|
||||
const view = model.source.getAction(resultIndex, ResultSetViewAction);
|
||||
const cellValue = view.getCellValue(element);
|
||||
|
||||
return isResultSetContentValue(cellValue);
|
||||
}
|
||||
|
||||
async getElementValueURL(
|
||||
model: IDatabaseDataModel,
|
||||
resultIndex: number,
|
||||
element: IResultSetElementKey
|
||||
) {
|
||||
const result = model.getResult(resultIndex);
|
||||
const data = model.source.getAction(resultIndex, ResultSetDataAction);
|
||||
const column = data.getColumn(element.column);
|
||||
const row = data.getRowValue(element.row);
|
||||
|
||||
if (!result?.id || !row || !column) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.graphQLService.sdk.getResultsetDataURL({
|
||||
resultsId: result.id,
|
||||
connectionId: result.connectionId,
|
||||
contextId: result.contextId,
|
||||
lobColumnIndex: column.position,
|
||||
row: {
|
||||
data: row,
|
||||
},
|
||||
});
|
||||
|
||||
return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${response.url}`;
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -6,9 +6,15 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import type { IResultSetColumnKey, IResultSetRowKey } from './IResultSetDataKey';
|
||||
import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey';
|
||||
|
||||
export const ResultSetDataKeysUtils = {
|
||||
serializeElementKey(elementKey: IResultSetElementKey): string {
|
||||
return this.serialize(elementKey.column) + this.serialize(elementKey.row);
|
||||
},
|
||||
isElementsKeyEqual(a: IResultSetElementKey, b: IResultSetElementKey) {
|
||||
return this.isEqual(a.column, b.column) && this.isEqual(a.row, b.row);
|
||||
},
|
||||
serialize(key: IResultSetColumnKey | IResultSetRowKey): string {
|
||||
let base = `${key.index}`;
|
||||
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
|
||||
implements IDatabaseDataFormatAction<IResultSetElementKey, IDatabaseResultSet> {
|
||||
static dataFormat = [ResultDataFormat.Resultset];
|
||||
|
||||
private view: ResultSetViewAction;
|
||||
private edit: ResultSetEditAction;
|
||||
private readonly view: ResultSetViewAction;
|
||||
private readonly edit: ResultSetEditAction;
|
||||
|
||||
constructor(
|
||||
source: IDatabaseDataSource<any, IDatabaseResultSet>,
|
||||
|
||||
+4
-4
@@ -36,10 +36,10 @@ export class ResultSetSelectAction extends DatabaseSelectAction<any, IDatabaseRe
|
||||
readonly selectedElements: Map<string, IResultSetElementKey[]>;
|
||||
|
||||
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<any, IDatabaseResultSet>,
|
||||
|
||||
+2
-2
@@ -47,8 +47,8 @@ export class ResultSetViewAction extends DatabaseDataAction<any, IDatabaseResult
|
||||
return this.result.data?.columns || [];
|
||||
}
|
||||
|
||||
private data: ResultSetDataAction;
|
||||
private editor: ResultSetEditAction;
|
||||
private readonly data: ResultSetDataAction;
|
||||
private readonly editor: ResultSetEditAction;
|
||||
|
||||
constructor(
|
||||
source: IDatabaseDataSource<any, IDatabaseResultSet>,
|
||||
|
||||
+6
@@ -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;
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
activeElement: IResultSetElementKey | null;
|
||||
|
||||
constructor(
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly source: IDatabaseDataSource<any, any>
|
||||
) {
|
||||
this.cache = new Map();
|
||||
this.activeElement = null;
|
||||
|
||||
makeObservable<this, 'cache'>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<TOptions, TResult extends IDatabaseDataResult>
|
||||
implements IDatabaseDataSource<TOptions, TResult> {
|
||||
abstract readonly dataManager: IDatabaseDataManager;
|
||||
access: DatabaseDataAccessMode;
|
||||
dataFormat: ResultDataFormat;
|
||||
supportedDataFormats: ResultDataFormat[];
|
||||
@@ -171,6 +173,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
|
||||
setResults(results: TResult[]): this {
|
||||
this.actions.updateResults(results);
|
||||
this.results = results;
|
||||
this.dataManager.clearCache();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 type { IResultSetElementKey } from './Actions/ResultSet/IResultSetDataKey';
|
||||
|
||||
export interface IDatabaseDataManager {
|
||||
activeElement: IResultSetElementKey | null;
|
||||
isContent: (element: IResultSetElementKey, resultIndex: number) => boolean;
|
||||
getFileDataUrl: (element: IResultSetElementKey, resultIndex: number) => Promise<string>;
|
||||
resolveFileDataUrl: (element: IResultSetElementKey, resultIndex: number) => Promise<string>;
|
||||
retrieveFileDataUrlFromCache: (element: IResultSetElementKey, resultIndex: number) => string | undefined;
|
||||
downloadFileData: (element: IResultSetElementKey, resultIndex: number) => Promise<void>;
|
||||
clearCache: () => void;
|
||||
}
|
||||
@@ -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<TOptions, TResult extends IDatabaseDataResult = IDatabaseDataResult> {
|
||||
readonly dataManager: IDatabaseDataManager;
|
||||
readonly access: DatabaseDataAccessMode;
|
||||
readonly dataFormat: ResultDataFormat;
|
||||
readonly supportedDataFormats: ResultDataFormat[];
|
||||
|
||||
@@ -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<Props>(function ContentLoader({ disabled, loading, className, onLoad, children }) {
|
||||
const translate = useTranslate();
|
||||
return styled(style)(
|
||||
<loader className={className}>
|
||||
<loader-label>{children}</loader-label>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={onLoad}
|
||||
>
|
||||
{translate('ui_download')}
|
||||
</Button>
|
||||
</loader>
|
||||
);
|
||||
});
|
||||
+158
-33
@@ -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<IToolsProps>(function Tools({ loading, stretch, onToggleStretch, onSave }) {
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(VALUE_PANEL_TOOLS_STYLES)(
|
||||
<tools-container>
|
||||
{onSave && (
|
||||
<tools>
|
||||
<tools-action
|
||||
as='button'
|
||||
title={translate('ui_processing_save')}
|
||||
disabled={loading}
|
||||
onClick={onSave}
|
||||
>
|
||||
<IconOrImage icon='/icons/save.svg' />
|
||||
</tools-action>
|
||||
</tools>
|
||||
)}
|
||||
{onToggleStretch && (
|
||||
<tools>
|
||||
<tools-action
|
||||
as='button'
|
||||
title={translate('data_viewer_presentation_value_image_fit')}
|
||||
disabled={stretch}
|
||||
onClick={onToggleStretch}
|
||||
>
|
||||
<IconOrImage icon='img-fit-size' />
|
||||
</tools-action>
|
||||
<tools-action
|
||||
as='button'
|
||||
title={translate('data_viewer_presentation_value_image_original_size')}
|
||||
disabled={!stretch}
|
||||
onClick={onToggleStretch}
|
||||
>
|
||||
<IconOrImage icon='img-original-size' />
|
||||
</tools-action>
|
||||
</tools>
|
||||
)}
|
||||
</tools-container>
|
||||
);
|
||||
});
|
||||
|
||||
export const ImageValuePresentation: TabContainerPanelComponent<IDataValuePanelProps<any, IDatabaseResultSet>> = 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)(
|
||||
<container>
|
||||
<ContentLoader
|
||||
disabled={loading}
|
||||
loading={!!model.source.dataManager.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(
|
||||
model.source.dataManager.activeElement, state.selectedCell
|
||||
)}
|
||||
onLoad={load}
|
||||
>
|
||||
{translate('data_viewer_presentation_value_content_trimmed_placeholder')}
|
||||
</ContentLoader>
|
||||
<Tools loading={loading} onSave={save} />
|
||||
</container>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
return styled(style)(
|
||||
<container>
|
||||
<tools>
|
||||
<Button disabled={stretch} onClick={() => setStretch(true)}>{translate('data_viewer_presentation_value_image_fit')}</Button>
|
||||
<Button disabled={!stretch} onClick={() => setStretch(false)}>{translate('data_viewer_presentation_value_image_original_size')}</Button>
|
||||
</tools>
|
||||
<image>
|
||||
<img src={src} {...use({ stretch })} />
|
||||
<img src={state.src} {...use({ stretch: state.stretch })} />
|
||||
</image>
|
||||
<Tools
|
||||
loading={loading}
|
||||
stretch={state.stretch}
|
||||
onToggleStretch={state.toggleStretch}
|
||||
onSave={save}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
});
|
||||
+37
-4
@@ -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<IDataValuePanelPr
|
||||
model,
|
||||
resultIndex,
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
const notificationService = useService(NotificationService);
|
||||
const textValuePresentationService = useService(TextValuePresentationService);
|
||||
const style = useStyles(styles, BASE_CONTAINERS_STYLES, UNDERLINE_TAB_STYLES);
|
||||
const style = useStyles(styles, BASE_CONTAINERS_STYLES, UNDERLINE_TAB_STYLES, VALUE_PANEL_TOOLS_STYLES);
|
||||
const state = useObservableRef(() => ({
|
||||
currentContentType: 'text/plain',
|
||||
lastContentType: 'text/plain',
|
||||
@@ -129,14 +135,27 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
state.setDefaultContentType(contentType);
|
||||
}
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
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)(
|
||||
<container>
|
||||
@@ -176,6 +195,20 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{canSave && (
|
||||
<tools-container>
|
||||
<tools>
|
||||
<tools-action
|
||||
as='button'
|
||||
title={translate('ui_processing_save')}
|
||||
disabled={model.isLoading()}
|
||||
onClick={save}
|
||||
>
|
||||
<IconOrImage icon='/icons/save.svg' />
|
||||
</tools-action>
|
||||
</tools>
|
||||
</tools-container>
|
||||
)}
|
||||
</container>
|
||||
);
|
||||
});
|
||||
|
||||
+36
@@ -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%;
|
||||
}
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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', 'Не удалось получить скрипт'],
|
||||
|
||||
@@ -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', '无法获取脚本'],
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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<IDataQueryOptions, IDatabaseResultSet> {
|
||||
readonly dataManager: IDatabaseDataManager;
|
||||
currentTask: ITask<SqlExecuteInfo> | null;
|
||||
requestInfo: IQueryRequestInfo;
|
||||
|
||||
@@ -35,6 +36,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataQueryOptions, IData
|
||||
) {
|
||||
super();
|
||||
|
||||
this.dataManager = new DatabaseDataManager(this.graphQLService, this);
|
||||
this.currentTask = null;
|
||||
this.requestInfo = {
|
||||
requestDuration: 0,
|
||||
|
||||
Reference in New Issue
Block a user