Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1458076
dbeaver/pro#7562 feat: persist data editor filter state after reconnect
SychevAndrey Mar 23, 2026
98de428
dbeaver/pro#7562 feat: update data viewer to persist filter state
SychevAndrey Mar 24, 2026
3e0e86f
dbeaver/pro#7562 feat: enhance constraint resolution in WebSQLDataFilter
SychevAndrey Mar 24, 2026
472815d
dbeaver/pro#7562 refactor: don't use options
SychevAndrey Mar 25, 2026
6b2f4f9
dbeaver/pro#7562 fix: stale constraints in UI
SychevAndrey Mar 25, 2026
52dd2b8
dbeaver/pro#7562 refactor: add method to retrieve pinned column names…
SychevAndrey Mar 25, 2026
0517067
dbeaver/pro#7562 refactor: simplify state validation using schema
SychevAndrey Mar 26, 2026
8038250
dbeaver/pro#7562 refactor: simplify state management in useDataViewer…
SychevAndrey Mar 26, 2026
b897cc2
Merge branch 'devel' into 7562-cb-keep-data-editor-filter-state-after…
SychevAndrey Mar 27, 2026
e3563d4
dbeaver/pro#7562 refactor: move persisted state to IDataViewerPageState
SychevAndrey Mar 27, 2026
1c0eea7
Merge branch '7562-cb-keep-data-editor-filter-state-after-reconnect' …
SychevAndrey Mar 27, 2026
9d26ed9
dbeaver/pro#7562 fix: initial pageState
SychevAndrey Mar 27, 2026
17e733e
dbeaver/pro#7562 refactor: simplify getPinnedColumnNames method in Gr…
SychevAndrey Mar 27, 2026
3ba78e6
dbeaver/pro#7562 refactor: shadowed column renamed to c for clarity
SychevAndrey Mar 27, 2026
a7bd511
dbeaver/pro#7562 refactor: change type of value in IPersistedConstrai…
SychevAndrey Mar 27, 2026
576cfeb
dbeaver/pro#7562 refactor: remove manual state operations
SychevAndrey Mar 27, 2026
f1f5a6c
Merge branch 'devel' into 7562-cb-keep-data-editor-filter-state-after…
SychevAndrey Apr 2, 2026
281c3dd
dbeaver/pro#7562 refactor: introduce DatabasePersistedStateAction for…
SychevAndrey Apr 2, 2026
7cdb1a9
Merge branch 'devel' into 7562-cb-keep-data-editor-filter-state-after…
SychevAndrey Apr 2, 2026
64fe0ae
Merge branch 'devel' into 7562-cb-keep-data-editor-filter-state-after…
SychevAndrey Apr 2, 2026
32cb99d
dbeaver/pro#7562 fix
SychevAndrey Apr 2, 2026
04bec45
dbeaver/pro#7562 refactor: replace reaction with when
SychevAndrey Apr 3, 2026
98d7c92
Revert "dbeaver/pro#7562 feat: enhance constraint resolution in WebSQ…
SychevAndrey Apr 3, 2026
b228b7b
dbeaver/pro#7562 fix: make pinColumns optional
SychevAndrey Apr 3, 2026
094c231
dbeaver/pro#7562 refactor: simplify pinned column handling
SychevAndrey Apr 3, 2026
4229081
dbeaver/pro#7562 refactor: initialize store in DatabasePersistedState…
SychevAndrey Apr 6, 2026
f07eb1e
dbeaver/pro#7562 refactor: rename getColumnNameByPosition to getColum…
SychevAndrey Apr 6, 2026
ccd6c7b
dbeaver/pro#7562 refactor: move DatabasePersistedStateAction to sourc…
SychevAndrey Apr 9, 2026
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* 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.
Expand All @@ -20,7 +20,7 @@
import { TableViewerStorageService } from '../TableViewer/TableViewerStorageService.js';
import { useDataViewerModel } from '../useDataViewerModel.js';

export function useDataViewerPanel(tab: ITab<IObjectViewerTabState>) {

Check warning on line 23 in webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
const dataViewerTableService = useService(DataViewerTableService);
const tableViewerStorageService = useService(TableViewerStorageService);
const navNodeManagerService = useService(NavNodeManagerService);
Expand Down Expand Up @@ -57,18 +57,34 @@

model = dataViewerTableService.create(connectionInfo, node);
tab.handlerState.tableId = model.id;
model.source.setOutdated();
dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);

const pageState = dataViewerTabService.page.getState(tab);
let pageState = dataViewerTabService.page.getState(tab);

if (!pageState) {
dataViewerTabService.page.setState(tab, {
resultIndex: 0,
presentationId: '',
valuePresentationId: null,
});
pageState = dataViewerTabService.page.getState(tab);
}

if (pageState) {
if (!pageState.persistedState) {
pageState.persistedState = {};
}

model.source.persistedState.setStore(pageState.persistedState);

const presentation = dataPresentationService.get(pageState.presentationId);

if (presentation?.dataFormat !== undefined) {
model.setDataFormat(presentation.dataFormat);
}
}

model.source.setOutdated();
dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
}

if (node?.name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* 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.
Expand Down Expand Up @@ -63,7 +63,7 @@
});
}

register() {

Check warning on line 66 in webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this));
}

Expand Down Expand Up @@ -111,14 +111,18 @@
await initTab();

if (tabInfo.isNewlyCreated) {
trySwitchPage(this.page);
trySwitchPage(this.page, {
resultIndex: 0,
presentationId: '',
valuePresentationId: null,
});
}
} 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>) {

Check warning on line 125 in webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

'tab' is defined but never used
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

export interface IDataViewerPersistedState {
constraints: IPersistedConstraint[];
whereFilter: string;
pinnedColumns?: string[];
columnOrder?: string[];
}

export interface IPersistedConstraint {
attributeName: string;
operator?: string;
value?: unknown;
orderAsc?: boolean;
orderPosition?: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { schema } from '@cloudbeaver/core-utils';

import type { IDataViewerPersistedState } from './IDataViewerPersistedState.js';

const persistedConstraintSchema = schema.object({
attributeName: schema.string().min(1),
operator: schema.string().optional(),
value: schema.unknown().optional(),
orderAsc: schema.boolean().optional(),
orderPosition: schema.number().optional(),
});

const persistedStateSchema = schema.object({
constraints: schema.array(persistedConstraintSchema),
whereFilter: schema.string(),
pinnedColumns: schema.array(schema.string()).optional().default([]),
columnOrder: schema.array(schema.string()).optional(),
});

export function validatePersistedState(data: unknown): data is IDataViewerPersistedState {
return persistedStateSchema.safeParse(data).success;
}
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 { computed, makeObservable } from 'mobx';
import { action, autorun, computed, type IReactionDisposer, makeObservable, runInAction } from 'mobx';

import { type DataTypeLogicalOperation, ResultDataFormat, type SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';

Expand All @@ -17,16 +17,21 @@
import type { IDatabaseDataConstraintAction } from './IDatabaseDataConstraintAction.js';
import { injectable } from '@cloudbeaver/core-di';
import { IDatabaseDataResult } from '../IDatabaseDataResult.js';
import { PERSISTED_STATE_KEY } from './IDatabasePersistedStateAction.js';
import type { IPersistedConstraint } from '../../DataViewerTableState/IDataViewerPersistedState.js';

export const IS_NULL_ID = 'IS_NULL';
export const IS_NOT_NULL_ID = 'IS_NOT_NULL';

@injectable(() => [IDatabaseDataSource, IDatabaseDataResult])
export class DatabaseDataConstraintAction
extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
implements IDatabaseDataConstraintAction<IDatabaseResultSet> {
implements IDatabaseDataConstraintAction<IDatabaseResultSet>
{
static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document];

private readonly persistDisposer: IReactionDisposer;

get supported(): boolean {
return this.source.constraintsAvailable && this.source.results.length < 2;
}
Expand Down Expand Up @@ -54,7 +59,24 @@
makeObservable(this, {
orderConstraints: computed,
filterConstraints: computed,
deleteAll: action,
deleteFilter: action,
deleteFilters: action,
deleteOrders: action,
deleteOrder: action,
deleteDataFilters: action,
deleteData: action,
setWhereFilter: action,
setFilter: action,
setOrder: action,
});

this.persistDisposer = autorun(() => this.persistConstraints());
}

override dispose(): void {
this.persistDisposer();
super.dispose();
}

private deleteConstraint(attributePosition: number) {
Expand Down Expand Up @@ -161,7 +183,7 @@
this.resetWhereFilter();
}

setWhereFilter(value: string) {

Check warning on line 186 in webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
if (!this.source.options) {
throw new Error('Options must be provided');
}
Expand All @@ -169,11 +191,11 @@
this.source.options.whereFilter = value;
}

resetWhereFilter() {

Check warning on line 194 in webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
this.setWhereFilter('');
}

setFilter(attributePosition: number, operator: string, value?: any): void {

Check warning on line 198 in webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Argument 'value' should be typed with a non-any type
if (!this.source.options) {
throw new Error('Options must be provided');
}
Expand All @@ -182,6 +204,7 @@

if (currentConstraint) {
currentConstraint.operator = operator;
currentConstraint.attributeName = this.getColumnNameAt(attributePosition);
if (value !== undefined) {
currentConstraint.value = value;
} else if (currentConstraint.value !== undefined) {
Expand All @@ -192,6 +215,7 @@

const constraint: SqlDataFilterConstraint = {
attributePosition,
attributeName: this.getColumnNameAt(attributePosition),
operator,
};

Expand Down Expand Up @@ -219,6 +243,7 @@
if (!resetOrder) {
this.source.options.constraints.push({
attributePosition,
attributeName: this.getColumnNameAt(attributePosition),
orderPosition: this.getMaxOrderPosition(),
orderAsc: order === EOrder.asc,
});
Expand Down Expand Up @@ -257,38 +282,86 @@
override updateResult(result: IDatabaseResultSet): void {
updateConstraintsForResult(this.source, result);
}

private getColumnNameAt(colIdx: number): string | undefined {
return this.result.data?.columns?.find(c => c.position === colIdx)?.name;
}

private persistConstraints(): void {
if (!this.source.options) {
return;
}

const ps = this.source.persistedState;
const constraints: IPersistedConstraint[] = this.source.options.constraints
.map(c => {
const name = c.attributeName ?? this.getColumnNameAt(c.attributePosition!);

if (!name) {
return null;
}

const persisted: IPersistedConstraint = { attributeName: name };

if (isFilterConstraint(c)) {
persisted.operator = c.operator;
persisted.value = c.value;
}

if (isOrderConstraint(c)) {
persisted.orderAsc = c.orderAsc;
persisted.orderPosition = c.orderPosition;
}

return persisted;
})
.filter((c): c is IPersistedConstraint => c !== null);

ps.set(PERSISTED_STATE_KEY.constraints, constraints);
ps.set(PERSISTED_STATE_KEY.whereFilter, this.source.options.whereFilter || '');
}
}

function updateConstraintsForResult(source: IDatabaseDataSource<IDatabaseDataOptions, IDatabaseResultSet>, result: IDatabaseResultSet) {
if (!source.options) {
return;
}

for (const constraint of source.options.constraints) {
const prevColumn = result.data?.columns?.find(column => column.position === constraint.attributePosition);
runInAction(() => {
for (const constraint of source.options!.constraints) {
let prevColumn = result.data?.columns?.find(c => c.position === constraint.attributePosition);

if (!prevColumn) {
return;
}
if (!prevColumn && constraint.attributeName) {
prevColumn = result.data?.columns?.find(c => c.name === constraint.attributeName);

let column = result.data?.columns?.find(column => column.position === prevColumn.position);
if (prevColumn) {
constraint.attributePosition = prevColumn.position;
}
}

if (!column || column.label !== prevColumn.label) {
column = result.data?.columns?.find(column => column.label === prevColumn.label);
}
if (!prevColumn) {
continue;
}

let column = result.data?.columns?.find(c => c.position === prevColumn.position);

if (column && prevColumn.position !== column.position) {
const prevConstraint = source.prevOptions?.constraints.find(
prevConstraint => prevConstraint.attributePosition === constraint.attributePosition,
);
if (!column || column.label !== prevColumn.label) {
column = result.data?.columns?.find(c => c.label === prevColumn.label);
}

if (column && prevColumn.position !== column.position) {
const prevConstraint = source.prevOptions?.constraints.find(
prevConstraint => prevConstraint.attributePosition === constraint.attributePosition,
);

constraint.attributePosition = column.position;
constraint.attributePosition = column.position;

if (prevConstraint) {
prevConstraint.attributePosition = constraint.attributePosition;
if (prevConstraint) {
prevConstraint.attributePosition = constraint.attributePosition;
}
}
}
}
});
}

export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean {
Expand All @@ -306,7 +379,7 @@
}
}

export function wrapOperationArgument(operationId: string, argument: any): string {

Check warning on line 382 in webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Argument 'argument' should be typed with a non-any type
if (operationId === 'LIKE') {
return `%${argument}%`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { makeObservable, observable } from 'mobx';

import type { IDatabasePersistedStateAction } from '../IDatabasePersistedStateAction.js';
import { validatePersistedState } from '../../../DataViewerTableState/validatePersistedState.js';

export class DatabasePersistedStateAction implements IDatabasePersistedStateAction {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please change it's name because it's not an action anymore

private store: Record<string, unknown>;
private readonly source: { options: any };

constructor(source: { options: any }) {
this.source = source;
this.store = {};

makeObservable<this, 'store'>(this, {
store: observable.ref,
});
}

setStore(store: Record<string, unknown>): void {
this.store = store;
this.applyPersistedConstraints();
}

has(key: string): boolean {
return key in this.store;
}

get<T>(key: string): T | undefined {
return this.store[key] as T | undefined;
}

set(key: string, value: unknown): void {
this.store[key] = value;
}

delete(key: string): void {
delete this.store[key];
}

private applyPersistedConstraints(): void {
const options = this.source.options;

if (!options || !validatePersistedState(this.store)) {
return;
}

if ('constraints' in options && 'whereFilter' in options) {
options.constraints = this.store.constraints.map(c => ({
attributeName: c.attributeName,
operator: c.operator,
value: c.value,
orderAsc: c.orderAsc,
orderPosition: c.orderPosition,
}));
options.whereFilter = this.store.whereFilter || '';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* 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.
Expand Down Expand Up @@ -56,6 +56,8 @@ export abstract class GridDataResultAction<
}

abstract getColumnName(key: IGridColumnKey): string | undefined;

abstract getColumnNameAt(colIdx: number): string | undefined;
abstract insertRow(row: IGridRowKey, value: TCell[], shift?: number): IGridRowKey | undefined;
abstract removeRow(row: IGridRowKey, shift?: number): IGridRowKey | undefined;
abstract setRowValue(row: IGridRowKey, value: TCell[], shift?: number): void;
Expand Down
Loading
Loading