Files
cloudbeaver/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts
T
9c6f301fa9 dbeaver/pro#7562 [CB] keep data editor filter state during session (#4261)
* dbeaver/pro#7562 feat: persist data editor filter state after reconnect

* dbeaver/pro#7562 feat: update data viewer to persist filter state

* dbeaver/pro#7562 feat: enhance constraint resolution in WebSQLDataFilter

allow using attrName without attrPosition

* dbeaver/pro#7562 refactor: don't use options

replaced private restorePendingState()  with public restoreViewState(pinnedColumnNames, columnOrderNames?) that takes data directly as arguments

* dbeaver/pro#7562 fix: stale constraints in UI

bug with skipping constraints if any without a position.
if (!prevColumn) {
        return;
      }

* dbeaver/pro#7562 refactor: add method to retrieve pinned column names in GridViewAction

* dbeaver/pro#7562 refactor: simplify state validation using schema

* dbeaver/pro#7562 refactor: simplify state management in useDataViewerPanel

* dbeaver/pro#7562 refactor: move persisted state to IDataViewerPageState

* dbeaver/pro#7562 fix: initial pageState

* dbeaver/pro#7562 refactor: simplify getPinnedColumnNames method in GridViewAction

* dbeaver/pro#7562 refactor: shadowed column renamed to c for clarity

* dbeaver/pro#7562 refactor: change type of value in IPersistedConstraint to unknown

* dbeaver/pro#7562 refactor: remove manual state operations

* dbeaver/pro#7562 refactor: introduce DatabasePersistedStateAction for centralized state persistence

* dbeaver/pro#7562 fix

* dbeaver/pro#7562 refactor: replace reaction with when

* Revert "dbeaver/pro#7562 feat: enhance constraint resolution in WebSQLDataFilter"

This reverts commit 3e0e86f4b2.

Last test showed that we don't need any additional logic on BE. It handles all constraints properly.

* dbeaver/pro#7562 fix: make pinColumns optional

* dbeaver/pro#7562 refactor: simplify pinned column handling

* dbeaver/pro#7562 refactor: initialize store in DatabasePersistedStateAction constructor as observable

* dbeaver/pro#7562 refactor: rename getColumnNameByPosition to getColumnNameAt for clarity

* dbeaver/pro#7562 refactor: move DatabasePersistedStateAction to source level, simplify persisted state sync

- Make DatabasePersistedStateAction a standalone class on DatabaseDataSource instead of per-result action
- Convert options to getter
- Auto-sync constraints to persisted state via autorun + action batching
- Restore view state (pinned columns, column order) in GridViewAction constructor

* dbeaver/pro#7562 refactor: replace DatabasePersistedStateAction with DatabasePersistedStateStore and update related references

* dbeaver/pro#7562 refactor: simplify view-state persistence

- DatabasePersistedStateStore: pure KV (observable.ref + actions)
- DatabaseDataSource: add loadPersistedState() + onPersistedStateLoaded() hook
- DatabaseDataConstraintAction: own the constraint persist
- ResultSetDataSource: thin dispatcher
- GridViewAction: columnsOrder/pinnedColumns become computed getters

* dbeaver/pro#7562 refactor: create universal ColumnReference

* dbeaver/pro#7562 refactor: simplify persistence flow, remove extra guards, simplify persisted data filter state handling

* dbeaver/pro#7562 refactor: simplify GridViewAction column-ref handling and layout resolution

* dbeaver/pro#7562 refactor: per-key reactive persisted state store, drop constraint persistence hacks

* dbeaver/pro#7562 refactor: simplify setOptions method to use superclass implementation

* dbeaver/pro#7562 add missing fields

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: alex <48489896+devnaumov@users.noreply.github.com>
2026-04-22 14:52:59 +02:00

157 lines
5.5 KiB
TypeScript

/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource, ConnectionsManagerService, type IConnectionExecutorData } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { type INodeNavigationData, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { resourceKeyList } from '@cloudbeaver/core-resource';
import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import {
DBObjectPageService,
type IObjectViewerTabState,
isObjectViewerTab,
ObjectPage,
ObjectViewerTabService,
} from '@cloudbeaver/plugin-object-viewer';
import type { IDataViewerPageState } from './IDataViewerPageState.js';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js';
const DataViewerTab = importLazyComponent(() => import('./DataViewerPage/DataViewerTab.js').then(module => module.DataViewerTab));
const DataViewerPanel = importLazyComponent(() => import('./DataViewerPage/DataViewerPanel.js').then(module => module.DataViewerPanel));
@injectable(() => [
NavNodeManagerService,
ObjectViewerTabService,
DBObjectPageService,
NotificationService,
ConnectionsManagerService,
NavigationTabsService,
ConnectionInfoResource,
TableViewerStorageService,
])
export class DataViewerTabService {
readonly page: ObjectPage<IDataViewerPageState>;
constructor(
private readonly navNodeManagerService: NavNodeManagerService,
private readonly objectViewerTabService: ObjectViewerTabService,
private readonly dbObjectPageService: DBObjectPageService,
private readonly notificationService: NotificationService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly navigationTabsService: NavigationTabsService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly tableViewerStorageService: TableViewerStorageService,
) {
this.page = this.dbObjectPageService.register({
key: 'data_viewer_data',
priority: 2,
order: 2,
getTabComponent: () => DataViewerTab,
getPanelComponent: () => DataViewerPanel,
onRestore: this.handleTabRestore.bind(this),
onUnload: this.handleTabClose.bind(this),
canClose: this.handleTabCanClose.bind(this),
onClose: this.handleTabClose.bind(this),
});
}
register() {
this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this));
}
registerTabHandler(): void {
this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this));
}
private async disconnectHandler(data: IConnectionExecutorData, contexts: IExecutionContextProvider<IConnectionExecutorData>) {
const connectionsKey = resourceKeyList(data.connections);
const tabs = Array.from(
this.navigationTabsService.findTabs(
isObjectViewerTab(tab => {
if (!tab.handlerState.connectionKey) {
return false;
}
return this.connectionInfoResource.isIntersect(connectionsKey, tab.handlerState.connectionKey);
}),
),
);
for (const tab of tabs) {
if (data.state === 'before') {
const canDisconnect = await this.handleTabCanClose(tab);
if (!canDisconnect) {
ExecutorInterrupter.interrupt(contexts);
return;
}
} else if (isObjectViewerTab(tab) && tab.handlerState.tableId) {
await this.disposeTableModel(tab.handlerState.tableId);
}
}
}
private async navigationHandler(data: INodeNavigationData, contexts: IExecutionContextProvider<INodeNavigationData>) {
try {
const { nodeInfo, tabInfo, initTab, trySwitchPage } = contexts.getContext(this.objectViewerTabService.objectViewerTabContext);
const node = await this.navNodeManagerService.loadNode(nodeInfo);
if (!this.navNodeManagerService.isNodeHasData(node)) {
return;
}
await initTab();
if (tabInfo.isNewlyCreated) {
trySwitchPage(this.page, {
resultIndex: 0,
presentationId: '',
valuePresentationId: null,
persistedState: {},
});
}
} catch (exception: any) {
this.notificationService.logException(exception, 'Data Editor Error', 'Error in Data Editor while processing action with database node');
}
}
private handleTabRestore(tab: ITab<IObjectViewerTabState>) {
return true;
}
private async handleTabCanClose(tab: ITab<IObjectViewerTabState>): Promise<boolean> {
const model = this.tableViewerStorageService.get(tab.handlerState.tableId || '');
if (model) {
return await model.source.canSafelyDispose();
}
return true;
}
private async handleTabClose(tab: ITab<IObjectViewerTabState>) {
if (tab.handlerState.tableId) {
await this.disposeTableModel(tab.handlerState.tableId);
}
}
private async disposeTableModel(tableId: string) {
if (tableId) {
const model = this.tableViewerStorageService.get(tableId);
if (model) {
await model.dispose();
this.tableViewerStorageService.remove(tableId);
}
}
}
}