diff --git a/webapp/packages/core-utils/src/base64ToHex.ts b/webapp/packages/core-utils/src/base64ToHex.ts new file mode 100644 index 0000000000..4ca675dc69 --- /dev/null +++ b/webapp/packages/core-utils/src/base64ToHex.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +// be careful with this when you calculate a big size blobs +// it can block the main thread and cause freezes +export function base64ToHex(base64String: string): string { + const raw = atob(base64String); + let result = ''; + for (let i = 0; i < raw.length; i++) { + const hex = raw.charCodeAt(i).toString(16); + result += hex.length === 2 ? hex : `0${hex}`; + } + return result.toUpperCase(); +} diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 7f244bcaea..02613fa438 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -10,6 +10,7 @@ export * from './underscore'; export * from './base64ToBlob'; export * from './blobToBase64'; +export * from './base64ToHex'; export * from './bytesToSize'; export * from './cacheValue'; export * from './clsx'; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBinaryFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBinaryFileValue.ts new file mode 100644 index 0000000000..84d282ac20 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/IResultSetBinaryFileValue.ts @@ -0,0 +1,5 @@ +import type { IResultSetContentValue } from './IResultSetContentValue'; + +export interface IResultSetBinaryFileValue extends IResultSetContentValue { + binary: string; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue.ts new file mode 100644 index 0000000000..87afa5cb3e --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue.ts @@ -0,0 +1,6 @@ +import type { IResultSetBinaryFileValue } from './IResultSetBinaryFileValue'; +import type { IResultSetContentValue } from './IResultSetContentValue'; + +export function isResultSetBinaryFileValue(value: IResultSetContentValue): value is IResultSetBinaryFileValue { + return value.contentType === 'application/octet-stream' && Boolean(value?.binary); +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts index 3f7658f7cc..08ea8f8a45 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.ts @@ -7,15 +7,12 @@ */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; -import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; -import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; -import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; import { ImageValuePresentation } from './ImageValuePresentation'; +import { isImageValuePresentationAvailable } from './isImageValuePresentationAvailable'; @injectable() export class ImageValuePresentationBootstrap extends Bootstrap { @@ -46,7 +43,7 @@ export class ImageValuePresentationBootstrap extends Bootstrap { const cellValue = view.getCellValue(firstSelectedCell); - return !this.isImage(cellValue); + return !isImageValuePresentationAvailable(cellValue); } return true; @@ -55,19 +52,4 @@ export class ImageValuePresentationBootstrap extends Bootstrap { } load(): void {} - - private isImage(value: IResultSetValue) { - if (isResultSetContentValue(value) && value?.binary) { - return getMIME(value.binary || '') !== null; - } - if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { - return value?.contentType?.startsWith('image/') ?? false; - } - - if (typeof value !== 'string') { - return false; - } - - return isValidUrl(value) && isImageFormat(value); - } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts new file mode 100644 index 0000000000..82d07d19ef --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/isImageValuePresentationAvailable.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; + +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; + } + if (isResultSetContentValue(value) || isResultSetBlobValue(value)) { + return value?.contentType?.startsWith('image/') ?? false; + } + + if (typeof value !== 'string') { + return false; + } + + return isValidUrl(value) && isImageFormat(value); +} 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 a7bc46d9c6..32a96da23b 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -24,14 +24,13 @@ import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/Resu import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; 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 { QuotaPlaceholder } from '../QuotaPlaceholder'; import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES'; import { getTypeExtension } from './getTypeExtension'; import { TextValuePresentationService } from './TextValuePresentationService'; -import { useAutoFormat } from './useAutoFormat'; +import { useTextValue } from './useTextValue'; const styles = css` Tab { @@ -72,7 +71,7 @@ const styles = css` `; export const TextValuePresentation: TabContainerPanelComponent> = observer( - function TextValuePresentation({ model, resultIndex }) { + function TextValuePresentation({ model, resultIndex, dataFormat }) { const translate = useTranslate(); const notificationService = useService(NotificationService); const quotasService = useService(QuotasService); @@ -106,7 +105,6 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); const extensions = useCodemirrorExtensions(undefined, typeExtension); - const value = autoFormat ? formatter.format(state.currentContentType, stringValue) : stringValue; + const value = useTextValue({ + model, + resultIndex, + currentContentType: state.currentContentType, + }); return styled(style)( state.setContentType(tab.tabId)} > diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts index a3faaf8720..6d6d6bee01 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts @@ -11,6 +11,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService'; +import { isBlobPresentationAvailable } from './isTextValuePresentationAvailable'; import { TextValuePresentationService } from './TextValuePresentationService'; const TextValuePresentation = lazy(async () => { @@ -47,18 +48,36 @@ export class TextValuePresentationBootstrap extends Bootstrap { name: 'data_viewer_presentation_value_text_html_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + isHidden: (_, context) => isBlobPresentationAvailable(context), }); this.textValuePresentationService.add({ key: 'text/xml', name: 'data_viewer_presentation_value_text_xml_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + isHidden: (_, context) => isBlobPresentationAvailable(context), }); this.textValuePresentationService.add({ key: 'application/json', name: 'data_viewer_presentation_value_text_json_title', order: Number.MAX_SAFE_INTEGER, panel: () => React.Fragment, + isHidden: (_, context) => isBlobPresentationAvailable(context), + }); + + this.textValuePresentationService.add({ + key: 'text/hex', + name: 'data_viewer_presentation_value_text_hex_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + isHidden: (_, context) => !isBlobPresentationAvailable(context), + }); + this.textValuePresentationService.add({ + key: 'text/base64', + name: 'data_viewer_presentation_value_text_base64_title', + order: Number.MAX_SAFE_INTEGER, + panel: () => React.Fragment, + isHidden: (_, context) => !isBlobPresentationAvailable(context), }); } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts index 5d08269de2..6f99127900 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationService.ts @@ -8,19 +8,21 @@ import { injectable } from '@cloudbeaver/core-di'; import { ITabInfo, ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui'; +import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; + @injectable() export class TextValuePresentationService { - readonly tabs: TabsContainer; + readonly tabs: TabsContainer, IDataValuePanelOptions>; constructor() { this.tabs = new TabsContainer('Value presentation'); } - get(tabId: string): ITabInfo | undefined { + get(tabId: string): ITabInfo, IDataValuePanelOptions> | undefined { return this.tabs.getTabInfo(tabId); } - add(tabInfo: ITabInfoOptions): void { + add(tabInfo: ITabInfoOptions, IDataValuePanelOptions>): void { this.tabs.add(tabInfo); } } diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts new file mode 100644 index 0000000000..be74cb8bbf --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValuePresentationAvailable.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { isResultSetBinaryFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; +import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction'; +import type { IDatabaseDataResult } from '../../DatabaseDataModel/IDatabaseDataResult'; +import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService'; + +export function isBlobPresentationAvailable(context: IDataValuePanelProps | undefined): boolean { + if (!context?.model.source.hasResult(context.resultIndex)) { + return true; + } + + const selection = context.model.source.getAction(context.resultIndex, ResultSetSelectAction); + + const focusedElement = selection.getFocusedElement(); + + if (selection.elements.length > 0 || focusedElement) { + const view = context.model.source.getAction(context.resultIndex, ResultSetViewAction); + + const firstSelectedCell = selection.elements[0] || focusedElement; + + const cellValue = view.getCellValue(firstSelectedCell); + + return isResultSetContentValue(cellValue) && isResultSetBinaryFileValue(cellValue); + } + + return true; +} diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts index 80a7f8b711..783addb1dc 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useAutoFormat.ts @@ -6,6 +6,9 @@ * you may not use this file except in compliance with the License. */ import { useObjectRef } from '@cloudbeaver/core-blocks'; +import { base64ToHex } from '@cloudbeaver/core-utils'; + +import type { IResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; export function useAutoFormat() { return useObjectRef( @@ -25,6 +28,20 @@ export function useAutoFormat() { return value; } }, + formatBlob(type: string, value: IResultSetContentValue) { + if (!value.binary) { + return value.text; + } + + switch (type) { + case 'text/base64': + return value.binary; + case 'text/hex': + return base64ToHex(value.binary); + default: + return value.text; + } + }, }), false, ); diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts new file mode 100644 index 0000000000..62d8bf187e --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/useTextValue.ts @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; +import { ResultSetEditAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetEditAction'; +import { ResultSetFormatAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; +import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel'; +import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; +import { useAutoFormat } from './useAutoFormat'; + +interface IUseTextValueArgs { + resultIndex: number; + model: IDatabaseDataModel; + currentContentType: string; +} + +export function useTextValue({ model, resultIndex, currentContentType }: IUseTextValueArgs) { + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const editor = model.source.getAction(resultIndex, ResultSetEditAction); + const selection = model.source.getAction(resultIndex, ResultSetSelectAction); + const focusCell = selection.getFocusedElement(); + const firstSelectedCell = selection.elements?.[0] ?? focusCell; + const autoFormat = !!firstSelectedCell && !editor.isElementEdited(firstSelectedCell); + const formatter = useAutoFormat(); + + if (!autoFormat) { + return; + } + + const blob = format.get(firstSelectedCell); + + if (isResultSetContentValue(blob)) { + const value = formatter.formatBlob(currentContentType, blob); + + if (value) { + return value; + } + } + + return formatter.format(currentContentType, format.getText(firstSelectedCell)); +} diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 78e4d15ae3..adb058ca96 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -17,6 +17,8 @@ export * from './DatabaseDataModel/Actions/ResultSet/IResultSetComplexValue'; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetFileValue'; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetContentValue'; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetGeometryValue'; +export * from './DatabaseDataModel/Actions/ResultSet/IResultSetBinaryFileValue'; +export * from './DatabaseDataModel/Actions/ResultSet/isResultSetBinaryFileValue'; export * from './DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; export * from './DatabaseDataModel/Actions/ResultSet/isResultSetComplexValue'; export * from './DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; diff --git a/webapp/packages/plugin-data-viewer/src/locales/en.ts b/webapp/packages/plugin-data-viewer/src/locales/en.ts index 3bdde1c450..fec9e03994 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/en.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/en.ts @@ -28,6 +28,8 @@ export default [ ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', 'Image'], ['data_viewer_presentation_value_image_fit', 'Fit Window'], ['data_viewer_presentation_value_image_original_size', 'Original Size'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/it.ts b/webapp/packages/plugin-data-viewer/src/locales/it.ts index c133992bc1..c4ea762cc5 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/it.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/it.ts @@ -24,6 +24,8 @@ export default [ ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', 'Immagine'], ['data_viewer_presentation_value_image_fit', 'Adatta alla Finestra'], ['data_viewer_presentation_value_image_original_size', 'Dimensioni Originali'], diff --git a/webapp/packages/plugin-data-viewer/src/locales/zh.ts b/webapp/packages/plugin-data-viewer/src/locales/zh.ts index 93ebeb170e..2d03d261e8 100644 --- a/webapp/packages/plugin-data-viewer/src/locales/zh.ts +++ b/webapp/packages/plugin-data-viewer/src/locales/zh.ts @@ -28,6 +28,8 @@ export default [ ['data_viewer_presentation_value_text_html_title', 'HTML'], ['data_viewer_presentation_value_text_xml_title', 'XML'], ['data_viewer_presentation_value_text_json_title', 'JSON'], + ['data_viewer_presentation_value_text_hex_title', 'HEX'], + ['data_viewer_presentation_value_text_base64_title', 'Base64'], ['data_viewer_presentation_value_image_title', '图片'], ['data_viewer_presentation_value_image_fit', '适应窗口'], ['data_viewer_presentation_value_image_original_size', '原始尺寸'],