From e4e8fb92ae0daa18f515f20b8375b1fa4fee3697 Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 27 Feb 2024 23:53:06 +0300 Subject: [PATCH] CB-4681 fix: content downloading in value panel (#2417) * CB-4681 fix: content downloading in value panel * CB-4681 fix: review * CB-4681 fix: review --------- Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../ObjectPropertyInfoForm/RenderField.tsx | 4 +- .../core-utils/src/downloadFromURL.ts | 32 +++ .../packages/core-utils/src/getMIME.test.ts | 8 +- webapp/packages/core-utils/src/getMIME.ts | 6 +- webapp/packages/core-utils/src/index.ts | 3 +- ...t.ts => removeMetadataFromDataURL.test.ts} | 8 +- ...Base64.ts => removeMetadataFromDataURL.ts} | 12 +- .../DataGridContextMenuSaveContentService.ts | 10 +- .../Actions/IDatabaseDataCacheAction.ts | 1 + .../ResultSet/IResultSetDataContentAction.ts | 7 +- .../Actions/ResultSet/ResultSetCacheAction.ts | 13 +- .../ResultSet/ResultSetDataContentAction.ts | 214 +++++++++--------- .../ImageValue/ImageValuePresentation.tsx | 159 +++++++------ .../isImageValuePresentationAvailable.ts | 14 +- .../TextValue/TextValuePresentation.tsx | 5 +- .../TextValue/useTextValue.ts | 6 +- 16 files changed, 270 insertions(+), 232 deletions(-) create mode 100644 webapp/packages/core-utils/src/downloadFromURL.ts rename webapp/packages/core-utils/src/{removeMetadataFromBase64.test.ts => removeMetadataFromDataURL.test.ts} (93%) rename webapp/packages/core-utils/src/{removeMetadataFromBase64.ts => removeMetadataFromDataURL.ts} (50%) diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index d7f5fccdf4..0f18fe4aae 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; -import { removeMetadataFromBase64 } from '@cloudbeaver/core-utils'; +import { removeMetadataFromDataURL } from '@cloudbeaver/core-utils'; import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox'; import { Combobox } from '../../FormControls/Combobox'; @@ -187,7 +187,7 @@ export const RenderField = observer(function RenderField({ disabled={disabled} fileName={passwordSavedMessage} className={className} - mapValue={removeMetadataFromBase64} + mapValue={removeMetadataFromDataURL} > {property.displayName} diff --git a/webapp/packages/core-utils/src/downloadFromURL.ts b/webapp/packages/core-utils/src/downloadFromURL.ts new file mode 100644 index 0000000000..0ee73cb201 --- /dev/null +++ b/webapp/packages/core-utils/src/downloadFromURL.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function downloadFromURL(url: string): Promise { + const req = new XMLHttpRequest(); + req.open('GET', url, true); + req.responseType = 'blob'; + + let resolve: (value: Blob) => void; + let reject: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + req.onload = () => { + resolve(req.response); + }; + + req.onerror = e => { + reject(e); + }; + + req.send(); + + return promise; +} diff --git a/webapp/packages/core-utils/src/getMIME.test.ts b/webapp/packages/core-utils/src/getMIME.test.ts index d1ea3f9ef5..f0c4699294 100644 --- a/webapp/packages/core-utils/src/getMIME.test.ts +++ b/webapp/packages/core-utils/src/getMIME.test.ts @@ -1,8 +1,8 @@ import { getMIME } from './getMIME'; describe('getMIME', () => { - it('should return null if binary is empty', () => { - expect(getMIME('')).toBe(null); + it("should return 'application/octet-stream' if binary is empty", () => { + expect(getMIME('')).toBe('application/octet-stream'); }); it('should return image/jpeg if binary starts with /', () => { @@ -26,7 +26,7 @@ describe('getMIME', () => { expect(getMIME(webpBase64Image)).toBe('image/webp'); }); - it('should return null if binary starts with anything else', () => { - expect(getMIME('aasdqwe')).toBe(null); + it("should return 'application/octet-stream' if binary starts with anything else", () => { + expect(getMIME('aasdqwe')).toBe('application/octet-stream'); }); }); diff --git a/webapp/packages/core-utils/src/getMIME.ts b/webapp/packages/core-utils/src/getMIME.ts index 98416f0609..ac700a5b07 100644 --- a/webapp/packages/core-utils/src/getMIME.ts +++ b/webapp/packages/core-utils/src/getMIME.ts @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ -export function getMIME(binary: string): string | null { +export function getMIME(binary: string): string { if (binary.length === 0) { - return null; + return 'application/octet-stream'; } switch (binary[0]) { @@ -21,7 +21,7 @@ export function getMIME(binary: string): string | null { case 'U': return 'image/webp'; default: - return null; + return 'application/octet-stream'; } } diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index e5a850255b..e16378ab72 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -56,6 +56,7 @@ export * from './isMapsEqual'; export * from './isObjectsEqual'; export * from './openCenteredPopup'; export * from './download'; +export * from './downloadFromURL'; export * from './getTextFileReadingProcess'; export * from './getTextBetween'; export * from './timestampToDate'; @@ -70,7 +71,7 @@ export * from './TempMap'; export * from './uriToBlob'; export * from './utf8ToBase64'; export * from './createLastPromiseGetter'; -export * from './removeMetadataFromBase64'; +export * from './removeMetadataFromDataURL'; export * from './renamePathName'; export * from './removeLineBreak'; export * from './replaceSubstring'; diff --git a/webapp/packages/core-utils/src/removeMetadataFromBase64.test.ts b/webapp/packages/core-utils/src/removeMetadataFromDataURL.test.ts similarity index 93% rename from webapp/packages/core-utils/src/removeMetadataFromBase64.test.ts rename to webapp/packages/core-utils/src/removeMetadataFromDataURL.test.ts index 5dea4141d2..2a33ef5893 100644 --- a/webapp/packages/core-utils/src/removeMetadataFromBase64.test.ts +++ b/webapp/packages/core-utils/src/removeMetadataFromDataURL.test.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { removeMetadataFromBase64 } from './removeMetadataFromBase64'; +import { removeMetadataFromDataURL } from './removeMetadataFromDataURL'; const BASE64 = 'data:application/zip;base64,UEsDBBQACAgIAIB8CVUAAAAAAAAAAAAAAAALAAAAZXdhbGxldC5wZW3Nmceyq0qyhuc8Rc+JExiBgEEP8CC8NzMBEt4j3NNf1trH7D59um/c7slVhCJEVVKUsjK//FP65ZfrxfCirP+N11k7Ml2e+5tpyz7t8n9T+Ohr+hdAk2X+XtEpk/djUZcitcEMR/OcRmM8a4R9yZnBGawCncsdTzsso1WSGXc6voSWMeQpMO3R8lwYheqHxvfostBL22Fpwy29OiJqPLhNgQvd6bgdg7dgPWBkDDOsZrbbZHYVLwPudK1k79VpirZlyROasi04dWTJOr7fzNwbIhoC68nCKqpB4GzRMToanV0M9VcBCm0gsepQGcPZWZOzGCM70Txzy9rPS9P1eafhIG6Jw2my4gEKyWgYBv2IqZfSetjjHgjhG9BQO361HARiEoTf/bIpHpsXPTp6SF/e2mAoGT4/dSkXKJ0sFcTPybY2S0MgjuwjTUwANr0FNmiSqppurQ1SPYhPY6xBrzU4X6HX3hljtwzDkycpSJUIFqznp78LUXKaqy+/DWDAGnXw7aTcWVSR7/K8cH5LGKUzpvW0jfXqjzJW+DdY5620saGli5XG0ibNsIr7A+0AY3a6bJ5YWbDWUWA2Z36C7bRDiOQlchlrpPEKjKWAFK7a7XCa0VPEHcy6BTTfEFZNAki9BOOdeKx4t50aXGVgWLve+VDz5Z6IHk+2TZgGg3nnBsrcxdug7OKWrpqTremQYTlgrrU55QVXCcnHgMESdTMKFgx6zPjYRRM/TxLSbLD2QMxgZxg2RbXCRtRVeePZ5WkQ0Aa0kpy3wb35bI/Xl0Ye+BtpZXRvTnIHUy9PA5txFiN2fSwZ4noz1bGgitJuoWt7T0CcngXEv813HqmFR9JKj+exx3Iv4nlAkroF8wNlVjJkO1hooGg9lJuzzm1KTmllpdMA6PJq9oUEr7zsUqDZIjcVsZei7V9uPGLhiz2Xup9TYezPe0IX2CzuHQiLVWPf2/TZlcBoX9+zaHnbTLLuZsQ5pTs3TQpbxPMDPONEseHJlH5YueYd+vSZ3kXVxKbafZ7Z/HJCQF0gnKJK8Ei2TzEb/Avjl4WQiaisukF85Y3eRNWD3uUH8hJrbgPh/DztLpDKk5dFkQEejjvzxNiLmWQgReFhIDl7+xu8kWPygEaHJN9C1dqPnXCzwlcFthA0t24i7pnXBW2ogErgmuAUt2Ta7vAOIk97lzDNiYVnazSZNJxvZh4jsUgC0NXTFPcErqCsFM71V9LK/hvoY9Q5LOQ2xqxagSoZTU60mrx/F+8TnEvy04rld021haT2K3TibVzBLAjRdMp+AjnVAVUx0r1MHGOeVyJlaXdgn7e4GUBO8pPtwQpYstUPZh3NA0U/NrHj1r3nu2BeD39YIgnwSyxGqnBEEFOv2ed42EyGN+276BRjte5v4+iYVHIR3/PigS3w6bXx5Jw1C9h2Zqy/gOljLXVSqqXz6PeG52M8JX0HjjuuP6T47aOcqGw3vkyz0jTkqnbh11a4UqtPUWqbNwIQb+AAkWwmHjeEb0J+ZBA2KD+HcniVdWIDRjVu5lyOFQX1cLF6l1+61eqh9Qmi+PlpAJFH2S29zej+GtlwLk8iIeh2gSQ+50ZMfXqLHLo80R47Eb9LlulN2GEapa6ecuQ/RAJoOBa8S8LRivfCuMOxdI'; @@ -14,14 +14,14 @@ const BASE64_NO_METADATA = describe('Remove metadata from base64', () => { test('should return base64 without metadata when base64 with metadata is passed', () => { - expect(removeMetadataFromBase64(BASE64)).toMatch(BASE64_NO_METADATA); + expect(removeMetadataFromDataURL(BASE64)).toMatch(BASE64_NO_METADATA); }); test('should return base64 when base64 with no metadata is passed', () => { - expect(removeMetadataFromBase64(BASE64_NO_METADATA)).toMatch(BASE64_NO_METADATA); + expect(removeMetadataFromDataURL(BASE64_NO_METADATA)).toMatch(BASE64_NO_METADATA); }); test('should return empty string when empty string is passed', () => { - expect(removeMetadataFromBase64('')).toMatch(''); + expect(removeMetadataFromDataURL('')).toMatch(''); }); }); diff --git a/webapp/packages/core-utils/src/removeMetadataFromBase64.ts b/webapp/packages/core-utils/src/removeMetadataFromDataURL.ts similarity index 50% rename from webapp/packages/core-utils/src/removeMetadataFromBase64.ts rename to webapp/packages/core-utils/src/removeMetadataFromDataURL.ts index b1e998dc2c..291d7d5057 100644 --- a/webapp/packages/core-utils/src/removeMetadataFromBase64.ts +++ b/webapp/packages/core-utils/src/removeMetadataFromDataURL.ts @@ -6,12 +6,14 @@ * you may not use this file except in compliance with the License. */ -export function removeMetadataFromBase64(base64: string) { - const parts = base64.split(','); +const dataUrlFilter = /data:(.*?)\/(.*?);base64,(.*)/; - if (parts[1]) { - return parts[1]; +export function removeMetadataFromDataURL(base64: string) { + const matches = base64.match(dataUrlFilter); + + if (!matches) { + return base64; } - return parts[0]; + return matches[3]; } 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 95bcac5e22..80969a5147 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 @@ -46,10 +46,7 @@ export class DataGridContextMenuSaveContentService { isDisabled: context => { const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); - return ( - context.data.model.isLoading() || - (!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement)) - ); + return context.data.model.isLoading() || content.isLoading(context.data.key); }, }); this.dataGridContextMenuService.add(this.dataGridContextMenuService.getMenuToken(), { @@ -76,10 +73,7 @@ export class DataGridContextMenuSaveContentService { isDisabled: context => { const content = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataContentAction); - return ( - context.data.model.isLoading() || - (!!content.activeElement && ResultSetDataKeysUtils.isElementsKeyEqual(context.data.key, content.activeElement)) - ); + return context.data.model.isLoading() || content.isLoading(context.data.key); }, }); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataCacheAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataCacheAction.ts index e260829165..94c5732055 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataCacheAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataCacheAction.ts @@ -13,4 +13,5 @@ export interface IDatabaseDataCacheAction(key: TKey, scope: symbol): T | undefined; set(key: TKey, scope: symbol, value: T): void; delete(key: TKey, scope: symbol): void; + deleteAll(scope: symbol): void; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts index 6a6c53cbca..2cadad2189 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetDataContentAction.ts @@ -8,13 +8,12 @@ import type { IResultSetElementKey } from './IResultSetDataKey'; export interface IResultSetDataContentAction { - activeElement: IResultSetElementKey | null; + isLoading: (element: IResultSetElementKey) => boolean; isBlobTruncated: (element: IResultSetElementKey) => boolean; isTextTruncated: (element: IResultSetElementKey) => boolean; isDownloadable: (element: IResultSetElementKey) => boolean; - getFileDataUrl: (element: IResultSetElementKey) => Promise; - resolveFileDataUrl: (element: IResultSetElementKey) => Promise; - retrieveFileDataUrlFromCache: (element: IResultSetElementKey) => string | undefined; + resolveFileDataUrl: (element: IResultSetElementKey) => Promise; + retrieveBlobFromCache: (element: IResultSetElementKey) => Blob | undefined; downloadFileData: (element: IResultSetElementKey) => Promise; clearCache: () => void; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts index c1af786deb..4cde45648b 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetCacheAction.ts @@ -5,7 +5,7 @@ * 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 { action, makeObservable, observable } from 'mobx'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; @@ -33,6 +33,11 @@ export class ResultSetCacheAction makeObservable(this, { cache: observable, + set: action, + setRow: action, + delete: action, + deleteAll: action, + deleteRow: action, }); } @@ -94,6 +99,12 @@ export class ResultSetCacheAction } } + deleteAll(scope: symbol) { + for (const [, keyCache] of this.cache) { + keyCache.delete(scope); + } + } + deleteRow(key: IResultSetRowKey, scope: symbol) { const keyCache = this.getRowCache(key); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts index b59d45de85..c32557fbf5 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.ts @@ -9,7 +9,7 @@ import { makeObservable, observable } from 'mobx'; import { QuotasService } from '@cloudbeaver/core-root'; import { GraphQLService, ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { bytesToSize, download, GlobalConstants, isNotNullDefined } from '@cloudbeaver/core-utils'; +import { bytesToSize, download, downloadFromURL, GlobalConstants, isNotNullDefined } from '@cloudbeaver/core-utils'; import { DatabaseDataAction } from '../../DatabaseDataAction'; import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; @@ -18,41 +18,35 @@ import { databaseDataAction } from '../DatabaseDataActionDecorator'; import type { IResultSetDataContentAction } from './IResultSetDataContentAction'; import type { IResultSetElementKey } from './IResultSetDataKey'; import { isResultSetContentValue } from './isResultSetContentValue'; +import { ResultSetCacheAction } from './ResultSetCacheAction'; import { ResultSetDataAction } from './ResultSetDataAction'; -import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; import { IResultSetValue, ResultSetFormatAction } from './ResultSetFormatAction'; -import { ResultSetViewAction } from './ResultSetViewAction'; const RESULT_VALUE_PATH = 'sql-result-value'; +const CONTENT_CACHE_KEY = Symbol('content-cache-key'); interface ICacheEntry { - url?: string; + blob?: Blob; fullText?: string; + loading?: boolean; } @databaseDataAction() export class ResultSetDataContentAction extends DatabaseDataAction implements IResultSetDataContentAction { static dataFormat = [ResultDataFormat.Resultset]; - private readonly cache: Map>; - activeElement: IResultSetElementKey | null; - constructor( source: IDatabaseDataSource, - private readonly view: ResultSetViewAction, private readonly data: ResultSetDataAction, private readonly format: ResultSetFormatAction, private readonly graphQLService: GraphQLService, private readonly quotasService: QuotasService, + private readonly cache: ResultSetCacheAction, ) { super(source); - this.cache = new Map(); - this.activeElement = null; - makeObservable(this, { cache: observable, - activeElement: observable.ref, }); } @@ -79,13 +73,15 @@ export class ResultSetDataContentAction extends DatabaseDataAction { + try { + this.updateCache(element, { loading: true }); + return await this.loadFileFullText(this.result, column.position, row); + } finally { + this.updateCache(element, { loading: false }); + } + }); + + this.updateCache(element, { fullText }); + + return fullText; + } + + async resolveFileDataUrl(element: IResultSetElementKey) { + const cachedUrl = this.retrieveBlobFromCache(element); + + if (cachedUrl) { + return cachedUrl; + } + + const url = await this.getFileDataUrl(element); + const blob = await downloadFromURL(url); + + this.updateCache(element, { blob }); + + return blob; + } + + async downloadFileData(element: IResultSetElementKey) { + const url = await this.getFileDataUrl(element); + download(url); + } + + clearCache() { + this.cache.deleteAll(CONTENT_CACHE_KEY); + } + + dispose(): void { + this.clearCache(); + } + + private async getFileDataUrl(element: IResultSetElementKey) { + const column = this.data.getColumn(element.column); + const row = this.data.getRowValue(element.row); + + if (!row || !column) { + throw new Error('Failed to get value metadata information'); + } + + const url = await this.source.runTask(async () => { + try { + this.updateCache(element, { loading: true }); + return await this.loadDataURL(this.result, column.position, row); + } finally { + this.updateCache(element, { loading: false }); + } + }); + + return url; + } + private async loadFileFullText(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) { if (!result.id) { throw new Error("Result's id must be provided"); @@ -127,107 +205,25 @@ export class ResultSetDataContentAction extends DatabaseDataAction { - 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); - - if (!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(this.result, column.position, row); - return this.generateFileDataUrl(fileName); - } finally { - this.activeElement = null; - } - }); - - return url; - } - - async resolveFileDataUrl(element: IResultSetElementKey) { - const cachedUrl = this.retrieveFileDataUrlFromCache(element); - - if (cachedUrl) { - return cachedUrl; - } - - const url = await this.getFileDataUrl(element); - this.updateCache(element, { url }); - - return url; - } - private updateCache(element: IResultSetElementKey, partialCache: Partial) { - const hash = this.getHash(element); - const cachedElement = this.cache.get(hash) ?? {}; - this.cache.set(hash, { ...cachedElement, ...partialCache }); + const cachedElement = this.getCache(element) ?? {}; + this.setCache(element, { ...cachedElement, ...partialCache }); } - retrieveFileFullTextFromCache(element: IResultSetElementKey) { - const hash = this.getHash(element); - return this.cache.get(hash)?.fullText; + private getCache(element: IResultSetElementKey) { + return this.cache.get(element, CONTENT_CACHE_KEY); } - retrieveFileDataUrlFromCache(element: IResultSetElementKey) { - const hash = this.getHash(element); - return this.cache.get(hash)?.url; + private setCache(element: IResultSetElementKey, value: ICacheEntry) { + this.cache.set(element, CONTENT_CACHE_KEY, value); } - async downloadFileData(element: IResultSetElementKey) { - const url = await this.getFileDataUrl(element); - download(url); - } - - clearCache() { - this.cache.clear(); - } - - private generateFileDataUrl(fileName: string) { - return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${fileName}`; - } - - private getHash(element: IResultSetElementKey) { - return ResultSetDataKeysUtils.serializeElementKey(element); - } - - private async loadFileName(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) { + private async loadDataURL(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({ + const { url } = await this.graphQLService.sdk.getResultsetDataURL({ resultsId: result.id, connectionId: result.connectionId, contextId: result.contextId, @@ -237,6 +233,6 @@ export class ResultSetDataContentAction extends DatabaseDataAction void; - onSave?: () => void; - onUpload?: () => void; -} - -const Tools = observer(function Tools({ loading, stretch, onToggleStretch, onSave, onUpload }) { - const translate = useTranslate(); - - return ( - - - {onSave && } - {onUpload && } - - - {onToggleStretch && ( - - - - )} - - ); -}); - export const ImageValuePresentation: TabContainerPanelComponent> = observer( function ImageValuePresentation({ model, resultIndex }) { const translate = useTranslate(); @@ -113,35 +82,52 @@ export const ImageValuePresentation: TabContainerPanelComponent { - if (!data.selectedCell) { - return; - } + const valueSize = bytesToSize(isResultSetContentValue(data.cellValue) ? data.cellValue.contentLength ?? 0 : 0); + const isTruncatedMessageDisplay = !!data.truncated && !data.src; + const isDownloadable = isTruncatedMessageDisplay && !!data.selectedCell && data.contentAction.isDownloadable(data.selectedCell); + const isCacheDownloading = isDownloadable && data.contentAction.isLoading(data.selectedCell); - try { - await data.contentAction.resolveFileDataUrl(data.selectedCell); - } catch (exception: any) { - notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); - } - }; - - const valueSize = bytesToSize(isResultSetContentValue(value) ? value.contentLength ?? 0 : 0); - const isDownloadable = data.selectedCell && data.contentAction.isDownloadable(data.selectedCell); + const debouncedDownload = useMemo(() => throttle(() => data.download(), 1000, false), []); return ( - {data.shouldShowImage && } - {data.truncated ? ( + {data.src && } + {isTruncatedMessageDisplay && ( {isDownloadable && ( - )} - ) : null} + )} + + + + {data.canSave && ( + + )} + {data.canUpload && ( + + )} + + + + + - ); }, diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts index aa1316bfd0..418737f629 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts @@ -7,16 +7,22 @@ */ import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; +import { isResultSetBinaryValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryValue'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; export function isImageValuePresentationAvailable(value: IResultSetValue) { - if (isResultSetContentValue(value) && value?.binary) { - return getMIME(value.binary || '') !== null; + let contentType = null; + + if (isResultSetBinaryValue(value)) { + contentType = getMIME(value.binary); + } else if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { + contentType = value?.contentType ?? null; } - if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { - return value?.contentType?.startsWith('image/') ?? false; + + if (contentType?.startsWith('image/')) { + return true; } if (typeof value !== 'string') { 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 99ac3c0a04..090cc9ff9d 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -127,7 +127,6 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(contentType!) ?? [], [contentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); @@ -188,9 +187,9 @@ export const TextValuePresentation: TabContainerPanelComponent - {firstSelectedCell && contentAction.isTextTruncated(firstSelectedCell) ? ( + {textValueData.isTruncated ? ( - {shouldShowPasteButton && ( + {textValueData.isTextColumn && (