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-4539 fix: value panel auto type detection #2320

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';

import { Button, s, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { ActionIconButton, Button, Container, Fill, Group, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { QuotasService } from '@cloudbeaver/core-root';
Expand All @@ -26,33 +26,13 @@ import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValu
import { QuotaPlaceholder } from '../QuotaPlaceholder';
import { VALUE_PANEL_TOOLS_STYLES } from '../ValuePanelTools/VALUE_PANEL_TOOLS_STYLES';
import { getTypeExtension } from './getTypeExtension';
import moduleStyles from './TextValuePresentation.m.css';
import { TextValuePresentationService } from './TextValuePresentationService';
import { useTextValue } from './useTextValue';

const styles = css`
Tab {
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
}
container {
display: flex;
gap: 8px;
flex-direction: column;
overflow: auto;
flex: 1;
}
actions {
display: flex;
justify-content: center;
flex: 0;
}
EditorLoader {
border-radius: var(--theme-group-element-radius);
}
EditorLoader {
flex: 1;
overflow: auto;
}
TabList {
composes: theme-border-color-background theme-background-background from global;
overflow: auto;
Expand All @@ -68,9 +48,7 @@ const styles = css`
}
`;

const TEXT_PLAIN_TYPE = 'text/plain';
const TEXT_JSON_TYPE = 'text/json';
const APPLICATION_JSON_TYPE = 'application/json';
const DEFAULT_CONTENT_TYPE = 'text/plain';

export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelProps<any, IDatabaseResultSet>> = observer(
function TextValuePresentation({ model, resultIndex, dataFormat }) {
Expand All @@ -79,7 +57,6 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const quotasService = useService(QuotasService);
const textValuePresentationService = useService(TextValuePresentationService);
const style = useStyles(styles, UNDERLINE_TAB_STYLES, VALUE_PANEL_TOOLS_STYLES);
const moduleStyle = useS(moduleStyles);
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const activeElements = selection.getActiveElements();
const firstSelectedCell = activeElements?.[0];
Expand All @@ -95,27 +72,53 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const contentValue = firstSelectedCell ? formatAction.get(firstSelectedCell) : null;
const state = useTabLocalState(() =>
observable({
currentContentType: TEXT_PLAIN_TYPE,

setContentType(contentType: string) {
if (contentType === TEXT_JSON_TYPE) {
contentType = APPLICATION_JSON_TYPE;
}
lineWrapping: null as boolean | null,
currentContentType: null as string | null,

setContentType(contentType: string | null) {
this.currentContentType = contentType;
},
handleTabOpen(tabId: string) {
// currentContentType may be selected automatically we don't want to change state in this case
if (tabId !== this.currentContentType) {
this.setContentType(tabId);
}
setLineWrapping(lineWrapping: boolean | null) {
this.lineWrapping = lineWrapping;
},
}),
);

let contentType = state.currentContentType;
let autoContentType = DEFAULT_CONTENT_TYPE;
let autoLineWrapping = false;

if (isResultSetContentValue(contentValue)) {
if (contentValue.contentType) {
switch (contentValue.contentType) {
case 'text/json':
autoContentType = 'application/json';
break;
case 'application/octet-stream':
autoContentType = 'application/octet-stream;type=base64';
autoLineWrapping = true;
break;
default:
Copy link
Contributor

Choose a reason for hiding this comment

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

'application/octet-stream;type=hex': - should we consider this case?

Copy link
Member Author

Choose a reason for hiding this comment

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

we will detect tab automatically later, by now we just set base64 as default for all binary data

autoContentType = contentValue.contentType;
break;
}
}
}

if (contentType === null) {
contentType = autoContentType ?? DEFAULT_CONTENT_TYPE;
}

if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === contentType)) {
contentType = activeTabs[0].key;
}

const lineWrapping = state.lineWrapping ?? autoLineWrapping;

const { textValue, isTruncated, isTextColumn, pasteFullText } = useTextValue({
model,
resultIndex,
currentContentType: state.currentContentType,
currentContentType: contentType,
});
const isSelectedCellReadonly = firstSelectedCell && (formatAction.isReadOnly(firstSelectedCell) || formatAction.isBinary(firstSelectedCell));
const isReadonlyByResultIndex = model.isReadonly(resultIndex) || model.isDisabled(resultIndex) || !firstSelectedCell;
Expand All @@ -125,16 +128,16 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const limit = bytesToSize(quotasService.getQuota('sqlBinaryPreviewMaxLength'));
const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell);
const shouldShowPasteButton = isTextColumn && isTruncated;
const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]);
const typeExtension = useMemo(() => getTypeExtension(contentType!) ?? [], [contentType]);
Copy link
Contributor

Choose a reason for hiding this comment

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

should we go with contentType ?? DEFAULT_CONTENT_TYPE? if something went wrong and contentType is null, then interface probably will be broken at all

Copy link
Member Author

Choose a reason for hiding this comment

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

no we shouldn't because this is typescript check and we will never have contentType = null at this point (only if we will broke something)

const extensions = useCodemirrorExtensions(undefined, typeExtension);

function handleChange(newValue: string) {
function valueChangeHandler(newValue: string) {
if (firstSelectedCell && !isReadonly) {
editAction.set(firstSelectedCell, newValue);
}
}

async function save() {
async function saveHandler() {
if (!firstSelectedCell) {
return;
}
Expand All @@ -146,41 +149,66 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
}
}

if (!activeTabs.some(tab => tab.key === state.currentContentType)) {
const contentType = activeTabs.length > 0 && activeTabs[0].key ? activeTabs[0].key : TEXT_PLAIN_TYPE;
state.setContentType(contentType);
async function selectTabHandler(tabId: string) {
// currentContentType may be selected automatically we don't want to change state in this case
if (tabId !== contentType) {
state.setContentType(tabId);
}
}

function toggleLineWrappingHandler() {
state.setLineWrapping(!lineWrapping);
}

return styled(style)(
<container>
<actions>
<TabsState
dataFormat={dataFormat}
resultIndex={resultIndex}
container={textValuePresentationService.tabs}
currentTabId={state.currentContentType}
model={model}
lazy
onChange={tab => state.handleTabOpen(tab.tabId)}
>
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} />
</TabsState>
</actions>
<EditorLoader key={isReadonly ? '1' : '0'} value={textValue} readonly={isReadonly} extensions={extensions} onChange={handleChange} />
{isTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
<div className={s(moduleStyle, { toolsContainer: true })}>
<Container vertical gap dense overflow>
<Container keepSize center overflow>
<Container keepSize>
Comment on lines +165 to +166
Copy link
Contributor

Choose a reason for hiding this comment

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

image

Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose something went wrong inside container component if we have to do this twice Container thing in order to get a correct layout

Copy link
Member Author

Choose a reason for hiding this comment

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

in this case it's because i don't want to add extra styles to TabsList
I suppose it's not always about problem in the Container but in matching other components together

<TabsState
dataFormat={dataFormat}
resultIndex={resultIndex}
container={textValuePresentationService.tabs}
currentTabId={contentType}
model={model}
lazy
onChange={tab => selectTabHandler(tab.tabId)}
>
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} />
</TabsState>
</Container>
</Container>
<Group maximum box>
<EditorLoader
key={isReadonly ? '1' : '0'}
value={textValue}
lineWrapping={lineWrapping}
readonly={isReadonly}
extensions={extensions}
onChange={valueChangeHandler}
/>
{isTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
</Group>
<Container keepSize center overflow>
{canSave && (
<Button disabled={model.isLoading()} onClick={save}>
{translate('ui_download')}
</Button>
<ActionIconButton title={translate('ui_download')} name="/icons/export.svg" disabled={model.isLoading()} img onClick={saveHandler} />
)}
<ActionIconButton
title={translate(
lineWrapping ? 'data_viewer_presentation_value_text_line_wrapping_no_wrap' : 'data_viewer_presentation_value_text_line_wrapping_wrap',
)}
name={lineWrapping ? 'img-original-size' : 'img-fit-size'}
onClick={toggleLineWrappingHandler}
/>
<Fill />
{shouldShowPasteButton && (
<Button disabled={model.isLoading()} onClick={pasteFullText}>
{translate('data_viewer_presentation_value_content_full_text_button')}
</Button>
<Container keepSize>
<Button disabled={model.isLoading()} onClick={pasteFullText}>
{translate('data_viewer_presentation_value_content_full_text_button')}
</Button>
</Container>
)}
</div>
</container>,
</Container>
</Container>,
);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ export class TextValuePresentationBootstrap extends Bootstrap {
});

this.textValuePresentationService.add({
key: 'text/hex',
key: 'application/octet-stream;type=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',
key: 'application/octet-stream;type=base64',
name: 'data_viewer_presentation_value_text_base64_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export function useAutoFormat() {
}

switch (type) {
case 'text/base64':
case 'application/octet-stream;type=base64':
return value.binary;
case 'text/hex':
case 'application/octet-stream;type=hex':
return base64ToHex(value.binary);
default:
return value.text;
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-data-viewer/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default [
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', 'Value'],
['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'],
['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"],
['data_viewer_presentation_value_text_title', 'Text'],
['data_viewer_presentation_value_text_plain_title', 'Text'],
['data_viewer_presentation_value_text_html_title', 'HTML'],
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-data-viewer/src/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default [
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', 'Valore'],
['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'],
['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"],
['data_viewer_presentation_value_text_title', 'Testo'],
['data_viewer_presentation_value_text_plain_title', 'Testo'],
['data_viewer_presentation_value_text_html_title', 'HTML'],
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-data-viewer/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default [
['data_viewer_auto_refresh_settings', 'Параметры автообновления'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Остановить при ошибке'],
['data_viewer_presentation_value_title', 'Значение'],
['data_viewer_presentation_value_text_line_wrapping_wrap', 'Переносить строки'],
['data_viewer_presentation_value_text_line_wrapping_no_wrap', 'Не переносить строки'],
['data_viewer_presentation_value_text_title', 'Текст'],
['data_viewer_presentation_value_image_title', 'Изображение'],
['data_viewer_presentation_value_image_fit', 'Растянуть'],
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/plugin-data-viewer/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default [
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', '值'],
['data_viewer_presentation_value_text_line_wrapping_wrap', 'Wrap lines'],
['data_viewer_presentation_value_text_line_wrapping_no_wrap', "Don't wrap lines"],
['data_viewer_presentation_value_text_title', '文本'],
['data_viewer_presentation_value_text_plain_title', '文本'],
['data_viewer_presentation_value_text_html_title', 'HTML'],
Expand Down
Loading