Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-4681 fix: content downloading in value panel #2417

Merged
merged 4 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions webapp/packages/core-utils/src/downloadFromURL.ts
Original file line number Diff line number Diff line change
@@ -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<Blob> {
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<Blob>((res, rej) => {
resolve = res;
reject = rej;
});

req.onload = () => {
resolve(req.response);
};

req.onerror = e => {
reject(e);
};

req.send();

return promise;
}
6 changes: 3 additions & 3 deletions webapp/packages/core-utils/src/getMIME.ts
sergeyteleshev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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';
sergeyteleshev marked this conversation as resolved.
Show resolved Hide resolved
}

switch (binary[0]) {
Expand All @@ -21,7 +21,7 @@ export function getMIME(binary: string): string | null {
case 'U':
return 'image/webp';
default:
return null;
return 'application/octet-stream';
}
}

Expand Down
1 change: 1 addition & 0 deletions webapp/packages/core-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export interface IDatabaseDataCacheAction<TKey, TResult extends IDatabaseDataRes
get<T>(key: TKey, scope: symbol): T | undefined;
set<T>(key: TKey, scope: symbol, value: T): void;
delete(key: TKey, scope: symbol): void;
deleteAll(scope: symbol): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export interface IResultSetDataContentAction {
isTextTruncated: (element: IResultSetElementKey) => boolean;
isDownloadable: (element: IResultSetElementKey) => boolean;
getFileDataUrl: (element: IResultSetElementKey) => Promise<string>;
resolveFileDataUrl: (element: IResultSetElementKey) => Promise<string>;
retrieveFileDataUrlFromCache: (element: IResultSetElementKey) => string | undefined;
resolveFileDataUrl: (element: IResultSetElementKey) => Promise<Blob>;
retrieveBlobFromCache: (element: IResultSetElementKey) => Blob | undefined;
downloadFileData: (element: IResultSetElementKey) => Promise<void>;
clearCache: () => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -33,6 +33,11 @@ export class ResultSetCacheAction

makeObservable<this, 'cache'>(this, {
cache: observable,
set: action,
setRow: action,
delete: action,
deleteAll: action,
deleteRow: action,
});
}

Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,36 +18,34 @@ 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;
}

@databaseDataAction()
export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabaseResultSet> implements IResultSetDataContentAction {
static dataFormat = [ResultDataFormat.Resultset];

private readonly cache: Map<string, Partial<ICacheEntry>>;
activeElement: IResultSetElementKey | null;

constructor(
source: IDatabaseDataSource<any, IDatabaseResultSet>,
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'>(this, {
Expand Down Expand Up @@ -82,10 +80,8 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
isBlobTruncated(elementKey: IResultSetElementKey) {
const limit = this.getLimitInfo(elementKey).limit;
const content = this.format.get(elementKey);
const cachedBlobUrl = this.retrieveFileDataUrlFromCache(elementKey);
const isLoadedFullBlob = Boolean(cachedBlobUrl) && this.format.isBinary(elementKey);

if (!isNotNullDefined(limit) || !isResultSetContentValue(content) || isLoadedFullBlob || !this.format.isBinary(elementKey)) {
if (!isNotNullDefined(limit) || !isResultSetContentValue(content) || !this.format.isBinary(elementKey)) {
return false;
}

Expand All @@ -95,10 +91,8 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
isTextTruncated(elementKey: IResultSetElementKey) {
const limit = this.getLimitInfo(elementKey).limit;
const content = this.format.get(elementKey);
const cachedFullText = this.retrieveFileFullTextFromCache(elementKey);
const isLoadedFullText = Boolean(cachedFullText) && this.format.isText(elementKey);

if (!isNotNullDefined(limit) || !isResultSetContentValue(content) || isLoadedFullText) {
if (!isNotNullDefined(limit) || !isResultSetContentValue(content)) {
return false;
}

Expand All @@ -109,29 +103,19 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
return !!this.result.data?.hasRowIdentifier && isResultSetContentValue(this.format.get(element));
}

private async loadFileFullText(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) {
if (!result.id) {
throw new Error("Result's id must be provided");
}

const response = await this.graphQLService.sdk.sqlReadStringValue({
resultsId: result.id,
connectionId: result.connectionId,
contextId: result.contextId,
columnIndex,
row: {
data: row,
},
});
retrieveFullTextFromCache(element: IResultSetElementKey) {
return this.getCache(element)?.fullText;
}

return response.text;
retrieveBlobFromCache(element: IResultSetElementKey) {
return this.getCache(element)?.blob;
}

async getFileFullText(element: IResultSetElementKey) {
const column = this.data.getColumn(element.column);
const row = this.data.getRowValue(element.row);

const cachedFullText = this.retrieveFileFullTextFromCache(element);
const cachedFullText = this.retrieveFullTextFromCache(element);

if (cachedFullText) {
return cachedFullText;
Expand Down Expand Up @@ -166,8 +150,7 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
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);
return await this.loadDataURL(this.result, column.position, row);
} finally {
this.activeElement = null;
}
Expand All @@ -177,57 +160,70 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
}

async resolveFileDataUrl(element: IResultSetElementKey) {
const cachedUrl = this.retrieveFileDataUrlFromCache(element);
const cachedUrl = this.retrieveBlobFromCache(element);

if (cachedUrl) {
return cachedUrl;
}

const url = await this.getFileDataUrl(element);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function sets cache loading state as expects
but we have a somekind of freeze in view button cause const blob = await downloadFromURL(url); below is not handled with loader, I suppose

I've send you the video

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be fixed later

this.updateCache(element, { url });
const blob = await downloadFromURL(url);

return url;
this.updateCache(element, { blob });

return blob;
}

private updateCache(element: IResultSetElementKey, partialCache: Partial<ICacheEntry>) {
const hash = this.getHash(element);
const cachedElement = this.cache.get(hash) ?? {};
this.cache.set(hash, { ...cachedElement, ...partialCache });
async downloadFileData(element: IResultSetElementKey) {
const url = await this.getFileDataUrl(element);
download(url);
}

retrieveFileFullTextFromCache(element: IResultSetElementKey) {
const hash = this.getHash(element);
return this.cache.get(hash)?.fullText;
clearCache() {
this.cache.deleteAll(CONTENT_CACHE_KEY);
}

retrieveFileDataUrlFromCache(element: IResultSetElementKey) {
const hash = this.getHash(element);
return this.cache.get(hash)?.url;
dispose(): void {
this.clearCache();
}

async downloadFileData(element: IResultSetElementKey) {
const url = await this.getFileDataUrl(element);
download(url);
private async loadFileFullText(result: IDatabaseResultSet, columnIndex: number, row: IResultSetValue[]) {
if (!result.id) {
throw new Error("Result's id must be provided");
}

const response = await this.graphQLService.sdk.sqlReadStringValue({
resultsId: result.id,
connectionId: result.connectionId,
contextId: result.contextId,
columnIndex,
row: {
data: row,
},
});

return response.text;
}

clearCache() {
this.cache.clear();
private updateCache(element: IResultSetElementKey, partialCache: Partial<ICacheEntry>) {
const cachedElement = this.getCache(element) ?? {};
this.setCache(element, { ...cachedElement, ...partialCache });
}

private generateFileDataUrl(fileName: string) {
return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${fileName}`;
private getCache(element: IResultSetElementKey) {
return this.cache.get<ICacheEntry>(element, CONTENT_CACHE_KEY);
}

private getHash(element: IResultSetElementKey) {
return ResultSetDataKeysUtils.serializeElementKey(element);
private setCache(element: IResultSetElementKey, value: ICacheEntry) {
this.cache.set(element, CONTENT_CACHE_KEY, value);
}

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,
Expand All @@ -237,6 +233,6 @@ export class ResultSetDataContentAction extends DatabaseDataAction<any, IDatabas
},
});

return response.url;
return `${GlobalConstants.serviceURI}/${RESULT_VALUE_PATH}/${url}`;
}
}
Loading
Loading