Skip to content

Commit 9c6f301

Browse files
SychevAndreydariamarutkinadevnaumov
authored
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 3e0e86f. 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>
1 parent a2ccd0b commit 9c6f301

17 files changed

Lines changed: 427 additions & 78 deletions

webapp/packages/plugin-data-viewer-result-trace-details/src/DVResultTraceDetailsBootstrap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* CloudBeaver - Cloud Database Manager
3-
* Copyright (C) 2020-2025 DBeaver Corp and others
3+
* Copyright (C) 2020-2026 DBeaver Corp and others
44
*
55
* Licensed under the Apache License, Version 2.0.
66
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ export class DVResultTraceDetailsBootstrap extends Bootstrap {
2020
super();
2121
}
2222

23-
override register() {
23+
override register(): void {
2424
this.dataPresentationService.add({
2525
id: 'result-trace-details-presentation',
2626
type: DataPresentationType.toolsPanel,
@@ -32,7 +32,7 @@ export class DVResultTraceDetailsBootstrap extends Bootstrap {
3232
if (!isResultSetDataSource(source)) {
3333
return true;
3434
}
35-
const result = (source as ResultSetDataSource<unknown>).getResult(resultIndex);
35+
const result = (source as ResultSetDataSource).getResult(resultIndex);
3636
return !result?.data?.hasDynamicTrace;
3737
},
3838
getPresentationComponent: () => DVResultTraceDetailsPresentation,

webapp/packages/plugin-data-viewer/src/DataViewerPage/DataViewerPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const DataViewerPanel: ObjectPagePanelComponent<IDataViewerPageState> = o
3030
presentationId,
3131
resultIndex: 0,
3232
valuePresentationId: null,
33+
persistedState: {},
3334
});
3435
} else {
3536
pageState.presentationId = presentationId;
@@ -47,6 +48,7 @@ export const DataViewerPanel: ObjectPagePanelComponent<IDataViewerPageState> = o
4748
presentationId: '',
4849
resultIndex: 0,
4950
valuePresentationId,
51+
persistedState: {},
5052
});
5153
} else {
5254
pageState.valuePresentationId = valuePresentationId;

webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerPanel.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* CloudBeaver - Cloud Database Manager
3-
* Copyright (C) 2020-2024 DBeaver Corp and others
3+
* Copyright (C) 2020-2026 DBeaver Corp and others
44
*
55
* Licensed under the Apache License, Version 2.0.
66
* you may not use this file except in compliance with the License.
@@ -57,18 +57,29 @@ export function useDataViewerPanel(tab: ITab<IObjectViewerTabState>) {
5757

5858
model = dataViewerTableService.create(connectionInfo, node);
5959
tab.handlerState.tableId = model.id;
60-
model.source.setOutdated();
61-
dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
6260

63-
const pageState = dataViewerTabService.page.getState(tab);
61+
let pageState = dataViewerTabService.page.getState(tab);
62+
63+
if (!pageState) {
64+
dataViewerTabService.page.setState(tab, {
65+
resultIndex: 0,
66+
presentationId: '',
67+
valuePresentationId: null,
68+
persistedState: {},
69+
});
70+
pageState = dataViewerTabService.page.getState(tab)!;
71+
}
72+
73+
model.source.loadPersistedState(pageState.persistedState);
6474

65-
if (pageState) {
66-
const presentation = dataPresentationService.get(pageState.presentationId);
75+
const presentation = dataPresentationService.get(pageState.presentationId);
6776

68-
if (presentation?.dataFormat !== undefined) {
69-
model.setDataFormat(presentation.dataFormat);
70-
}
77+
if (presentation?.dataFormat !== undefined) {
78+
model.setDataFormat(presentation.dataFormat);
7179
}
80+
81+
model.source.setOutdated();
82+
dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
7283
}
7384

7485
if (node?.name) {

webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* CloudBeaver - Cloud Database Manager
3-
* Copyright (C) 2020-2025 DBeaver Corp and others
3+
* Copyright (C) 2020-2026 DBeaver Corp and others
44
*
55
* Licensed under the Apache License, Version 2.0.
66
* you may not use this file except in compliance with the License.
@@ -111,7 +111,12 @@ export class DataViewerTabService {
111111
await initTab();
112112

113113
if (tabInfo.isNewlyCreated) {
114-
trySwitchPage(this.page);
114+
trySwitchPage(this.page, {
115+
resultIndex: 0,
116+
presentationId: '',
117+
valuePresentationId: null,
118+
persistedState: {},
119+
});
115120
}
116121
} catch (exception: any) {
117122
this.notificationService.logException(exception, 'Data Editor Error', 'Error in Data Editor while processing action with database node');

webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseDataConstraintAction.ts

Lines changed: 111 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* Licensed under the Apache License, Version 2.0.
66
* you may not use this file except in compliance with the License.
77
*/
8-
import { computed, makeObservable } from 'mobx';
8+
import { action, computed, makeObservable, runInAction } from 'mobx';
99

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

1212
import { DatabaseDataAction } from '../DatabaseDataAction.js';
1313
import type { IDatabaseDataOptions } from '../IDatabaseDataOptions.js';
@@ -21,10 +21,47 @@ import { IDatabaseDataResult } from '../IDatabaseDataResult.js';
2121
export const IS_NULL_ID = 'IS_NULL';
2222
export const IS_NOT_NULL_ID = 'IS_NOT_NULL';
2323

24+
const CONSTRAINTS_KEY = 'constraints';
25+
const WHERE_FILTER_KEY = 'whereFilter';
26+
27+
export function persistDataFilterConstraints<TOptions extends IDatabaseDataOptions>(
28+
source: IDatabaseDataSource<TOptions, IDatabaseResultSet>,
29+
): void {
30+
const options = source.options;
31+
if (!options) {
32+
return;
33+
}
34+
35+
source.persistedState.set(CONSTRAINTS_KEY, options.constraints.filter(hasConstraintIdentity));
36+
source.persistedState.set(WHERE_FILTER_KEY, options.whereFilter || '');
37+
}
38+
39+
export function applyPersistedDataFilterConstraints<TOptions extends IDatabaseDataOptions>(
40+
source: IDatabaseDataSource<TOptions, IDatabaseResultSet>,
41+
): void {
42+
const options = source.options;
43+
if (!options) {
44+
return;
45+
}
46+
47+
const constraints = source.persistedState.get<SqlDataFilterConstraint[]>(CONSTRAINTS_KEY);
48+
const whereFilter = source.persistedState.get<string>(WHERE_FILTER_KEY);
49+
50+
if (!Array.isArray(constraints) || typeof whereFilter !== 'string') {
51+
return;
52+
}
53+
54+
runInAction(() => {
55+
options.constraints = constraints.map(constraint => ({ ...constraint }));
56+
options.whereFilter = whereFilter;
57+
});
58+
}
59+
2460
@injectable(() => [IDatabaseDataSource, IDatabaseDataResult])
2561
export class DatabaseDataConstraintAction
2662
extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
27-
implements IDatabaseDataConstraintAction<IDatabaseResultSet> {
63+
implements IDatabaseDataConstraintAction<IDatabaseResultSet>
64+
{
2865
static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document];
2966

3067
get supported(): boolean {
@@ -54,6 +91,16 @@ export class DatabaseDataConstraintAction
5491
makeObservable(this, {
5592
orderConstraints: computed,
5693
filterConstraints: computed,
94+
deleteAll: action,
95+
deleteFilter: action,
96+
deleteFilters: action,
97+
deleteOrders: action,
98+
deleteOrder: action,
99+
deleteDataFilters: action,
100+
deleteData: action,
101+
setWhereFilter: action,
102+
setFilter: action,
103+
setOrder: action,
57104
});
58105
}
59106

@@ -161,19 +208,19 @@ export class DatabaseDataConstraintAction
161208
this.resetWhereFilter();
162209
}
163210

164-
setWhereFilter(value: string) {
211+
setWhereFilter(value: string): void {
165212
if (!this.source.options) {
166213
throw new Error('Options must be provided');
167214
}
168215

169216
this.source.options.whereFilter = value;
170217
}
171218

172-
resetWhereFilter() {
219+
resetWhereFilter(): void {
173220
this.setWhereFilter('');
174221
}
175222

176-
setFilter(attributePosition: number, operator: string, value?: any): void {
223+
setFilter(attributePosition: number, operator: string, value?: unknown): void {
177224
if (!this.source.options) {
178225
throw new Error('Options must be provided');
179226
}
@@ -182,6 +229,7 @@ export class DatabaseDataConstraintAction
182229

183230
if (currentConstraint) {
184231
currentConstraint.operator = operator;
232+
currentConstraint.attributeName = this.getColumnNameAt(attributePosition);
185233
if (value !== undefined) {
186234
currentConstraint.value = value;
187235
} else if (currentConstraint.value !== undefined) {
@@ -192,6 +240,7 @@ export class DatabaseDataConstraintAction
192240

193241
const constraint: SqlDataFilterConstraint = {
194242
attributePosition,
243+
attributeName: this.getColumnNameAt(attributePosition),
195244
operator,
196245
};
197246

@@ -219,6 +268,7 @@ export class DatabaseDataConstraintAction
219268
if (!resetOrder) {
220269
this.source.options.constraints.push({
221270
attributePosition,
271+
attributeName: this.getColumnNameAt(attributePosition),
222272
orderPosition: this.getMaxOrderPosition(),
223273
orderAsc: order === EOrder.asc,
224274
});
@@ -257,38 +307,77 @@ export class DatabaseDataConstraintAction
257307
override updateResult(result: IDatabaseResultSet): void {
258308
updateConstraintsForResult(this.source, result);
259309
}
310+
311+
private getColumnNameAt(colIdx: number): string | undefined {
312+
return this.result.data?.columns?.find(c => c.position === colIdx)?.name;
313+
}
260314
}
261315

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

267-
for (const constraint of source.options.constraints) {
268-
const prevColumn = result.data?.columns?.find(column => column.position === constraint.attributePosition);
321+
const columns = result.data?.columns ?? [];
269322

270-
if (!prevColumn) {
271-
return;
272-
}
323+
if (columns.length === 0) {
324+
return;
325+
}
273326

274-
let column = result.data?.columns?.find(column => column.position === prevColumn.position);
327+
runInAction(() => {
328+
for (const constraint of source.options!.constraints) {
329+
if (!hasConstraintIdentity(constraint)) {
330+
resetDataFilterState(source);
331+
return;
332+
}
275333

276-
if (!column || column.label !== prevColumn.label) {
277-
column = result.data?.columns?.find(column => column.label === prevColumn.label);
278-
}
334+
const initialPosition = constraint.attributePosition;
335+
const initialName = constraint.attributeName;
336+
337+
const resolvedColumn = resolveConstraintColumn(columns, initialName, initialPosition);
338+
339+
if (!resolvedColumn) {
340+
resetDataFilterState(source);
341+
return;
342+
}
343+
344+
constraint.attributeName = resolvedColumn.name;
345+
constraint.attributePosition = resolvedColumn.position;
279346

280-
if (column && prevColumn.position !== column.position) {
281347
const prevConstraint = source.prevOptions?.constraints.find(
282-
prevConstraint => prevConstraint.attributePosition === constraint.attributePosition,
348+
prevConstraint => prevConstraint.attributePosition === initialPosition && prevConstraint.attributeName === initialName,
283349
);
284350

285-
constraint.attributePosition = column.position;
286-
287351
if (prevConstraint) {
352+
prevConstraint.attributeName = constraint.attributeName;
288353
prevConstraint.attributePosition = constraint.attributePosition;
289354
}
290355
}
291-
}
356+
});
357+
}
358+
359+
function resetDataFilterState(source: IDatabaseDataSource<IDatabaseDataOptions, IDatabaseResultSet>): void {
360+
source.options!.constraints = [];
361+
source.options!.whereFilter = '';
362+
source.persistedState.delete(CONSTRAINTS_KEY);
363+
source.persistedState.delete(WHERE_FILTER_KEY);
364+
}
365+
366+
function resolveConstraintColumn(
367+
columns: SqlResultColumn[],
368+
attributeName: string,
369+
attributePosition: number,
370+
): (SqlResultColumn & { name: string; position: number }) | undefined {
371+
return columns.find(
372+
(column): column is SqlResultColumn & { name: string; position: number } =>
373+
typeof column.name === 'string' && typeof column.position === 'number' && column.position === attributePosition && column.name === attributeName,
374+
);
375+
}
376+
377+
function hasConstraintIdentity(
378+
constraint: SqlDataFilterConstraint,
379+
): constraint is SqlDataFilterConstraint & { attributeName: string; attributePosition: number } {
380+
return typeof constraint.attributeName === 'string' && constraint.attributeName.length > 0 && typeof constraint.attributePosition === 'number';
292381
}
293382

294383
export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean {
@@ -306,12 +395,12 @@ export function getNextOrder(order: Order): Order {
306395
}
307396
}
308397

309-
export function wrapOperationArgument(operationId: string, argument: any): string {
398+
export function wrapOperationArgument(operationId: string, argument: unknown): string {
310399
if (operationId === 'LIKE') {
311400
return `%${argument}%`;
312401
}
313402

314-
return argument;
403+
return String(argument);
315404
}
316405

317406
export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean {

webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridDataResultAction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* CloudBeaver - Cloud Database Manager
3-
* Copyright (C) 2020-2025 DBeaver Corp and others
3+
* Copyright (C) 2020-2026 DBeaver Corp and others
44
*
55
* Licensed under the Apache License, Version 2.0.
66
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)