From 0dc8ff88be17bf1bab942cf72e22bac76ec8cf4e Mon Sep 17 00:00:00 2001 From: Aleksei Potsetsuev Date: Thu, 4 Jan 2024 15:18:25 +0800 Subject: [PATCH 1/2] CB-4401 feat: save value panel state per column --- .../src/ExceptionsCatcherService.ts | 2 + .../Actions/DatabaseDataResultAction.ts | 28 +++++++ .../Actions/DatabaseMetadataAction.ts | 47 ++++++++++++ .../Actions/Document/DocumentDataAction.ts | 14 +++- .../Actions/IDatabaseDataMetadataAction.ts | 17 +++++ .../Actions/IDatabaseDataResultAction.ts | 5 +- .../Actions/ResultSet/ResultSetDataAction.ts | 14 +++- .../Actions/ResultSet/ResultSetViewAction.ts | 4 +- .../src/TableViewer/ValuePanel/ValuePanel.tsx | 43 ++++++++--- .../BooleanValue/BooleanValuePresentation.tsx | 6 +- .../BooleanValuePresentationBootstrap.ts | 6 +- .../ImageValue/ImageValuePresentation.tsx | 74 +++++++++++++------ .../ImageValuePresentationBootstrap.ts | 6 +- .../TextValue/TextValuePresentation.tsx | 44 ++++++----- .../isTextValuePresentationAvailable.ts | 6 +- .../TextValue/useTextValue.ts | 4 +- .../packages/plugin-data-viewer/src/index.ts | 4 + .../src/GISValuePresentation.tsx | 39 ++++------ .../src/GISViewerBootstrap.ts | 11 +-- 19 files changed, 269 insertions(+), 105 deletions(-) create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseMetadataAction.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataMetadataAction.ts diff --git a/webapp/packages/core-events/src/ExceptionsCatcherService.ts b/webapp/packages/core-events/src/ExceptionsCatcherService.ts index d7ec27ae3d..822fce5453 100644 --- a/webapp/packages/core-events/src/ExceptionsCatcherService.ts +++ b/webapp/packages/core-events/src/ExceptionsCatcherService.ts @@ -47,6 +47,8 @@ export class ExceptionsCatcherService extends Bootstrap { }); } + console.error(_error); + if (this.baseCatcher) { return this.baseCatcher(event, source, lineno, colno, _error); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts new file mode 100644 index 0000000000..eb95b012d4 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataResultAction.ts @@ -0,0 +1,28 @@ +/* + * 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 type { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../DatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource'; +import { databaseDataAction } from './DatabaseDataActionDecorator'; +import type { IDatabaseDataResultAction } from './IDatabaseDataResultAction'; + +@databaseDataAction() +export abstract class DatabaseDataResultAction + extends DatabaseDataAction + implements IDatabaseDataResultAction +{ + static dataFormat: ResultDataFormat[] | null = null; + + constructor(source: IDatabaseDataSource) { + super(source); + } + abstract getIdentifier(key: TKey): string; + abstract serialize(key: TKey): string; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseMetadataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseMetadataAction.ts new file mode 100644 index 0000000000..1e48d4be3f --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseMetadataAction.ts @@ -0,0 +1,47 @@ +/* + * 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 type { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import { DatabaseDataAction } from '../DatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IDatabaseDataSource } from '../IDatabaseDataSource'; +import { databaseDataAction } from './DatabaseDataActionDecorator'; +import type { IDatabaseDataMetadataAction } from './IDatabaseDataMetadataAction'; + +@databaseDataAction() +export class DatabaseMetadataAction + extends DatabaseDataAction + implements IDatabaseDataMetadataAction +{ + static dataFormat: ResultDataFormat[] | null = null; + readonly metadata: MetadataMap; + + constructor(source: IDatabaseDataSource) { + super(source); + this.metadata = new MetadataMap(); + } + + has(key: string): boolean { + return this.metadata.has(key); + } + + get(key: string): T | undefined; + get(key: string, getDefaultValue: (() => T) | undefined): T; + get(key: string, getDefaultValue?: (() => T) | undefined): T | undefined { + return this.metadata.get(key, getDefaultValue); + } + + set(key: string, value: any): void { + this.metadata.set(key, value); + } + + delete(key: string): void { + this.metadata.delete(key); + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts index 8d5773077e..e32d081193 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Document/DocumentDataAction.ts @@ -9,15 +9,15 @@ import { computed, makeObservable } from 'mobx'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; -import { DatabaseDataAction } from '../../DatabaseDataAction'; import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction'; +import { DatabaseDataResultAction } from '../DatabaseDataResultAction'; import type { IDatabaseDataDocument } from './IDatabaseDataDocument'; +import type { IDocumentElementKey } from './IDocumentElementKey'; @databaseDataAction() -export class DocumentDataAction extends DatabaseDataAction implements IDatabaseDataResultAction { +export class DocumentDataAction extends DatabaseDataResultAction { static dataFormat = [ResultDataFormat.Document]; get documents(): IDatabaseDataDocument[] { @@ -37,6 +37,14 @@ export class DocumentDataAction extends DatabaseDataAction extends IDatabaseDataAction { + get(key: string): T | undefined; + get(key: string, getDefaultValue: () => T): T; + set(key: string, value: any): void; + delete(key: string): void; + has(key: string): boolean; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts index c74490c590..ac3baae398 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataResultAction.ts @@ -8,4 +8,7 @@ import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; -export type IDatabaseDataResultAction = IDatabaseDataAction; +export interface IDatabaseDataResultAction extends IDatabaseDataAction { + getIdentifier(key: TKey): string; + serialize(key: TKey): string; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 328ca9d696..183539c22e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -9,18 +9,18 @@ import { computed, makeObservable } from 'mobx'; import { DataTypeLogicalOperation, ResultDataFormat, SqlResultColumn } from '@cloudbeaver/core-sdk'; -import { DatabaseDataAction } from '../../DatabaseDataAction'; import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction'; +import { DatabaseDataResultAction } from '../DatabaseDataResultAction'; import type { IResultSetContentValue } from './IResultSetContentValue'; import type { IResultSetColumnKey, IResultSetElementKey, IResultSetRowKey } from './IResultSetDataKey'; import { isResultSetContentValue } from './isResultSetContentValue'; +import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; import type { IResultSetValue } from './ResultSetFormatAction'; @databaseDataAction() -export class ResultSetDataAction extends DatabaseDataAction implements IDatabaseDataResultAction { +export class ResultSetDataAction extends DatabaseDataResultAction { static dataFormat = [ResultDataFormat.Resultset]; get rows(): IResultSetValue[][] { @@ -39,6 +39,14 @@ export class ResultSetDataAction extends DatabaseDataAction implements IDatabaseDataResultAction { +export class ResultSetViewAction extends DatabaseDataAction implements IDatabaseDataAction { static dataFormat = [ResultDataFormat.Resultset]; get rowKeys(): IResultSetRowKey[] { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index 239b24f84e..8c2444b443 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -5,13 +5,17 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useRef, useState } from 'react'; import styled, { css } from 'reshadow'; import { useService } from '@cloudbeaver/core-di'; import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui'; +import { MetadataMap } from '@cloudbeaver/core-utils'; +import { DatabaseDataResultAction } from '../../DatabaseDataModel/Actions/DatabaseDataResultAction'; +import { DatabaseMetadataAction } from '../../DatabaseDataModel/Actions/DatabaseMetadataAction'; +import { DatabaseSelectAction } from '../../DatabaseDataModel/Actions/DatabaseSelectAction'; import type { IDatabaseResultSet } from '../../DatabaseDataModel/IDatabaseResultSet'; import type { DataPresentationComponent } from '../../DataPresentationService'; import { DataValuePanelService } from './DataValuePanelService'; @@ -47,17 +51,35 @@ const styles = css` export const ValuePanel: DataPresentationComponent = observer(function ValuePanel({ dataFormat, model, resultIndex }) { const service = useService(DataValuePanelService); - const [currentTabId, setCurrentTabId] = useState(''); - const lastTabId = useRef(''); + const selectAction = model.source.getActionImplementation(resultIndex, DatabaseSelectAction); + const dataResultAction = model.source.getActionImplementation(resultIndex, DatabaseDataResultAction); + const metadataAction = model.source.getAction(resultIndex, DatabaseMetadataAction); + const activeElements = selectAction?.getActiveElements(); + let elementKey: string | null = null; + + if (dataResultAction && activeElements && activeElements.length > 0) { + elementKey = dataResultAction.getIdentifier(activeElements[0]); + } + + const state = metadataAction.get(`value-panel-${elementKey}`, () => + observable( + { + currentTabId: '', + tabsState: new MetadataMap(), + setCurrentTabId(tabId: string) { + this.currentTabId = tabId; + }, + }, + { tabsState: false }, + {}, + ), + ); const displayed = service.getDisplayed({ dataFormat, model, resultIndex }); + let currentTabId = state.currentTabId; - if (displayed.length > 0) { - const firstTabId = displayed[0].key; - if (firstTabId !== lastTabId.current) { - setCurrentTabId(firstTabId); - lastTabId.current = firstTabId; - } + if (displayed.length > 0 && (!currentTabId || !displayed.some(tab => tab.key === currentTabId))) { + currentTabId = displayed[0].key; } return styled( @@ -71,8 +93,9 @@ export const ValuePanel: DataPresentationComponent = ob dataFormat={dataFormat} model={model} resultIndex={resultIndex} + localState={state.tabsState} lazy - onChange={tab => setCurrentTabId(tab.tabId)} + onChange={tab => state.setCurrentTabId(tab.tabId)} > diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx index 6a4d9cdf8c..c7923db010 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx @@ -33,9 +33,9 @@ export const BooleanValuePresentation: TabContainerPanelComponent 0 || focusedElement) { + if (activeElements.length > 0) { const view = context.model.source.getAction(context.resultIndex, ResultSetViewAction); - const firstSelectedCell = selection.elements[0] || focusedElement; + const firstSelectedCell = activeElements[0]; const cellValue = view.getCellValue(firstSelectedCell); if (cellValue === undefined) { diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index fc1cee6b42..b811489679 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -13,10 +13,11 @@ import { selectFiles } from '@cloudbeaver/core-browser'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { QuotasService } from '@cloudbeaver/core-root'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import { type TabContainerPanelComponent, useTabLocalState } from '@cloudbeaver/core-ui'; import { bytesToSize, download, getMIME, isImageFormat, isValidUrl } from '@cloudbeaver/core-utils'; import { createResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/createResultSetBlobValue'; +import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey'; import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue'; import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue'; import { isResultSetFileValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetFileValue'; @@ -68,7 +69,22 @@ export const ImageValuePresentation: TabContainerPanelComponent + observable( + { + stretch: false, + toggleStretch() { + this.stretch = !this.stretch; + }, + }, + { + stretch: observable.ref, + toggleStretch: action.bound, + }, + ), + ); + + const data = useObservableRef( () => ({ get editAction(): ResultSetEditAction { return this.model.source.getAction(this.resultIndex, ResultSetEditAction); @@ -82,12 +98,20 @@ export const ImageValuePresentation: TabContainerPanelComponent { const file = files?.item(0) ?? undefined; - if (file) { + if (file && this.selectedCell) { this.editAction.set(this.selectedCell, createResultSetBlobValue(file)); } }); @@ -167,28 +193,30 @@ export const ImageValuePresentation: TabContainerPanelComponent { + if (!data.selectedCell) { + return; + } + try { - await state.contentAction.resolveFileDataUrl(state.selectedCell); + await data.contentAction.resolveFileDataUrl(data.selectedCell); } catch (exception: any) { notificationService.logException(exception, 'data_viewer_presentation_value_content_download_error'); } @@ -198,12 +226,12 @@ export const ImageValuePresentation: TabContainerPanelComponent - {state.contentAction.isDownloadable(state.selectedCell) && ( + {data.selectedCell && data.contentAction.isDownloadable(data.selectedCell) && (