Skip to content

Commit b991734

Browse files
authored
feat: Add storing data browser filters on server (#3090)
1 parent ff535e9 commit b991734

File tree

7 files changed

+631
-46
lines changed

7 files changed

+631
-46
lines changed

src/components/CategoryList/CategoryList.react.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export default class CategoryList extends React.Component {
3636

3737
componentDidUpdate(prevProps) {
3838
// Auto-expand if the URL params changed (e.g., user navigated to a different filter)
39-
if (prevProps.params !== this.props.params) {
39+
// OR if categories changed (e.g., filters finished loading asynchronously)
40+
if (prevProps.params !== this.props.params || prevProps.categories !== this.props.categories) {
4041
this._autoExpandForSelectedFilter();
4142
}
4243
this._updateHighlight();

src/dashboard/Data/Browser/Browser.react.js

Lines changed: 152 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { ActionTypes } from 'lib/stores/SchemaStore';
3838
import stringCompare from 'lib/stringCompare';
3939
import subscribeTo from 'lib/subscribeTo';
4040
import { withRouter } from 'lib/withRouter';
41+
import FilterPreferencesManager from 'lib/FilterPreferencesManager';
42+
import { prefersServerStorage } from 'lib/StoragePreferences';
4143
import Parse from 'parse';
4244
import React from 'react';
4345
import { Helmet } from 'react-helmet';
@@ -129,6 +131,7 @@ class Browser extends DashboardView {
129131
this.subsection = 'Browser';
130132
this.noteTimeout = null;
131133
this.currentQuery = null;
134+
this.filterPreferencesManager = null;
132135
const limit = window.localStorage?.getItem('browserLimit');
133136

134137
this.state = {
@@ -188,6 +191,7 @@ class Browser extends DashboardView {
188191
AggregationPanelData: {},
189192
isLoadingInfoPanel: false,
190193
errorAggregatedData: {},
194+
classFilters: {}, // Map of className -> filters array
191195
};
192196

193197
this.addLocation = this.addLocation.bind(this);
@@ -298,6 +302,52 @@ class Browser extends DashboardView {
298302
this.setState({ configData: data });
299303
this.classAndCloudFuntionMap(this.state.configData);
300304
});
305+
306+
// Initialize FilterPreferencesManager
307+
if (this.context) {
308+
this.filterPreferencesManager = new FilterPreferencesManager(this.context);
309+
// Load all class filters if schema is already available
310+
if (this.props.schema?.data?.get('classes')) {
311+
this.loadAllClassFilters();
312+
}
313+
}
314+
}
315+
316+
async loadAllClassFilters(propsToUse) {
317+
if (!this.filterPreferencesManager) {
318+
return;
319+
}
320+
321+
// Use provided props or fall back to this.props
322+
const props = propsToUse || this.props;
323+
const schema = props.schema;
324+
if (!schema || !schema.data) {
325+
return;
326+
}
327+
328+
const classFilters = {};
329+
330+
// Load filters for all classes
331+
const classList = schema.data.get('classes');
332+
if (classList) {
333+
const classNames = Object.keys(classList.toObject());
334+
await Promise.all(
335+
classNames.map(async (className) => {
336+
try {
337+
const filters = await this.filterPreferencesManager.getFilters(
338+
this.context.applicationId,
339+
className
340+
);
341+
classFilters[className] = filters || [];
342+
} catch (error) {
343+
console.error(`Failed to load filters for class ${className}:`, error);
344+
classFilters[className] = [];
345+
}
346+
})
347+
);
348+
}
349+
350+
this.setState({ classFilters });
301351
}
302352

303353
componentWillUnmount() {
@@ -340,6 +390,15 @@ class Browser extends DashboardView {
340390
);
341391
this.redirectToFirstClass(nextProps.schema.data.get('classes'), nextContext);
342392
}
393+
394+
// Load filters when schema becomes available or changes
395+
if (
396+
nextProps.schema?.data?.get('classes') &&
397+
(!this.props.schema?.data?.get('classes') ||
398+
this.props.params.appId !== nextProps.params.appId)
399+
) {
400+
this.loadAllClassFilters(nextProps);
401+
}
343402
}
344403

345404
setLoadingInfoPanel(bool) {
@@ -1294,7 +1353,7 @@ class Browser extends DashboardView {
12941353
});
12951354
}
12961355

1297-
saveFilters(filters, name, relativeDate, filterId = null) {
1356+
async saveFilters(filters, name, relativeDate, filterId = null) {
12981357
const jsonFilters = filters.toJSON();
12991358
if (relativeDate && jsonFilters?.length) {
13001359
for (let i = 0; i < jsonFilters.length; i++) {
@@ -1364,15 +1423,16 @@ class Browser extends DashboardView {
13641423
});
13651424
}
13661425
} else {
1367-
// Check if this is updating a legacy filter (no filterId but filter content matches existing filter without ID)
1368-
const existingLegacyFilterIndex = preferences.filters.findIndex(filter =>
1369-
!filter.id && filter.name === name && filter.filter === _filters
1426+
// Check if this is updating an existing filter by name and content match
1427+
// (legacy filters get auto-assigned UUIDs when read, so we match by content)
1428+
const existingFilterIndex = preferences.filters.findIndex(filter =>
1429+
filter.name === name && filter.filter === _filters
13701430
);
13711431

1372-
if (existingLegacyFilterIndex !== -1) {
1373-
// Convert legacy filter to modern filter by adding an ID
1374-
newFilterId = crypto.randomUUID();
1375-
preferences.filters[existingLegacyFilterIndex] = {
1432+
if (existingFilterIndex !== -1) {
1433+
// Update existing filter, keeping its ID
1434+
newFilterId = preferences.filters[existingFilterIndex].id;
1435+
preferences.filters[existingFilterIndex] = {
13761436
name,
13771437
id: newFilterId,
13781438
filter: _filters,
@@ -1388,19 +1448,60 @@ class Browser extends DashboardView {
13881448
}
13891449
}
13901450

1391-
ClassPreferences.updatePreferences(
1392-
preferences,
1393-
this.context.applicationId,
1394-
this.props.params.className
1395-
);
1451+
// Use FilterPreferencesManager if available, otherwise fallback to local storage
1452+
if (this.filterPreferencesManager) {
1453+
const filterToSave = {
1454+
id: newFilterId,
1455+
name,
1456+
className: this.props.params.className,
1457+
filter: _filters,
1458+
};
1459+
await this.filterPreferencesManager.saveFilter(
1460+
this.context.applicationId,
1461+
this.props.params.className,
1462+
filterToSave,
1463+
preferences.filters
1464+
);
1465+
} else {
1466+
// Fallback to local storage
1467+
ClassPreferences.updatePreferences(
1468+
preferences,
1469+
this.context.applicationId,
1470+
this.props.params.className
1471+
);
1472+
}
1473+
1474+
// Reload filters for this class to update the sidebar
1475+
await this.reloadClassFilters(this.props.params.className);
13961476

13971477
super.forceUpdate();
13981478

13991479
// Return the filter ID for new filters so the caller can apply them
14001480
return newFilterId;
14011481
}
14021482

1403-
deleteFilter(filterIdOrObject) {
1483+
async reloadClassFilters(className) {
1484+
if (!this.filterPreferencesManager) {
1485+
return;
1486+
}
1487+
1488+
try {
1489+
const filters = await this.filterPreferencesManager.getFilters(
1490+
this.context.applicationId,
1491+
className
1492+
);
1493+
this.setState(prevState => ({
1494+
classFilters: {
1495+
...prevState.classFilters,
1496+
[className]: filters || []
1497+
}
1498+
}));
1499+
} catch (error) {
1500+
console.error(`Failed to reload filters for class ${className}:`, error);
1501+
}
1502+
}
1503+
1504+
async deleteFilter(filterIdOrObject) {
14041505
const preferences = ClassPreferences.getPreferences(
14051506
this.context.applicationId,
14061507
this.props.params.className
@@ -1425,13 +1526,27 @@ class Browser extends DashboardView {
14251526
}
14261527
}
14271528

1428-
ClassPreferences.updatePreferences(
1429-
{ ...preferences, filters: updatedFilters },
1430-
this.context.applicationId,
1431-
this.props.params.className
1432-
);
1529+
// Use FilterPreferencesManager if available, otherwise fallback to local storage
1530+
if (this.filterPreferencesManager) {
1531+
await this.filterPreferencesManager.deleteFilter(
1532+
this.context.applicationId,
1533+
this.props.params.className,
1534+
filterIdOrObject,
1535+
updatedFilters
1536+
);
1537+
} else {
1538+
// Fallback to local storage
1539+
ClassPreferences.updatePreferences(
1540+
{ ...preferences, filters: updatedFilters },
1541+
this.context.applicationId,
1542+
this.props.params.className
1543+
);
1544+
}
14331545
}
14341546

1547+
// Reload filters for this class to update the sidebar
1548+
await this.reloadClassFilters(this.props.params.className);
1549+
14351550
super.forceUpdate();
14361551
}
14371552

@@ -2263,13 +2378,24 @@ class Browser extends DashboardView {
22632378
}
22642379
const allCategories = [];
22652380
for (const row of [...special, ...categories]) {
2266-
const { filters = [] } = ClassPreferences.getPreferences(
2267-
this.context.applicationId,
2268-
row.name
2269-
);
2270-
// Set filters sorted alphabetically
2271-
row.filters = filters.sort((a, b) => a.name.localeCompare(b.name));
2272-
allCategories.push(row);
2381+
let filters = this.state.classFilters[row.name];
2382+
2383+
// Fallback to local storage ONLY if not using server storage and filters not loaded yet
2384+
if (filters === undefined &&
2385+
(!this.filterPreferencesManager?.isServerConfigEnabled() ||
2386+
!prefersServerStorage(this.context.applicationId))) {
2387+
const prefs = ClassPreferences.getPreferences(this.context.applicationId, row.name);
2388+
filters = prefs?.filters || [];
2389+
} else if (filters === undefined) {
2390+
filters = [];
2391+
}
2392+
2393+
// Set filters sorted alphabetically and create a new row object to trigger re-render
2394+
const sortedFilters = filters.sort((a, b) => a.name.localeCompare(b.name));
2395+
allCategories.push({
2396+
...row,
2397+
filters: sortedFilters
2398+
});
22732399
}
22742400

22752401
return (

src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Notification from 'dashboard/Data/Browser/Notification.react';
1818
import * as ColumnPreferences from 'lib/ColumnPreferences';
1919
import * as ClassPreferences from 'lib/ClassPreferences';
2020
import ViewPreferencesManager from 'lib/ViewPreferencesManager';
21+
import FilterPreferencesManager from 'lib/FilterPreferencesManager';
2122
import ScriptManager from 'lib/ScriptManager';
2223
import bcrypt from 'bcryptjs';
2324
import * as OTPAuth from 'otpauth';
@@ -29,6 +30,7 @@ export default class DashboardSettings extends DashboardView {
2930
this.section = 'App Settings';
3031
this.subsection = 'Dashboard Configuration';
3132
this.viewPreferencesManager = null;
33+
this.filterPreferencesManager = null;
3234
this.scriptManager = null;
3335

3436
this.state = {
@@ -65,6 +67,7 @@ export default class DashboardSettings extends DashboardView {
6567
initializeManagers() {
6668
if (this.context) {
6769
this.viewPreferencesManager = new ViewPreferencesManager(this.context);
70+
this.filterPreferencesManager = new FilterPreferencesManager(this.context);
6871
this.scriptManager = new ScriptManager(this.context);
6972
this.loadStoragePreference();
7073
}
@@ -85,6 +88,11 @@ export default class DashboardSettings extends DashboardView {
8588
// Show a notification about the change
8689
this.showNote(`Storage preference changed to ${preference === 'server' ? 'server' : 'browser'}`);
8790
}
91+
92+
// Filters use the same storage preference as views
93+
if (this.filterPreferencesManager) {
94+
this.filterPreferencesManager.setStoragePreference(this.context.applicationId, preference);
95+
}
8896
}
8997

9098
async migrateToServer() {
@@ -93,6 +101,11 @@ export default class DashboardSettings extends DashboardView {
93101
return;
94102
}
95103

104+
if (!this.filterPreferencesManager) {
105+
this.showNote('FilterPreferencesManager not initialized');
106+
return;
107+
}
108+
96109
if (!this.viewPreferencesManager.isServerConfigEnabled()) {
97110
this.showNote('Server configuration is not enabled for this app. Please add a "config" section to your app configuration.');
98111
return;
@@ -101,16 +114,30 @@ export default class DashboardSettings extends DashboardView {
101114
this.setState({ migrationLoading: true });
102115

103116
try {
104-
const result = await this.viewPreferencesManager.migrateToServer(this.context.applicationId);
105-
if (result.success) {
106-
if (result.viewCount > 0) {
107-
this.showNote(`Successfully migrated ${result.viewCount} view(s) to server storage.`);
117+
// Migrate views
118+
const viewsResult = await this.viewPreferencesManager.migrateToServer(this.context.applicationId);
119+
120+
// Migrate filters
121+
const filtersResult = await this.filterPreferencesManager.migrateToServer(this.context.applicationId);
122+
123+
const totalItems = viewsResult.viewCount + filtersResult.filterCount;
124+
125+
if (viewsResult.success && filtersResult.success) {
126+
if (totalItems > 0) {
127+
const messages = [];
128+
if (viewsResult.viewCount > 0) {
129+
messages.push(`${viewsResult.viewCount} view(s)`);
130+
}
131+
if (filtersResult.filterCount > 0) {
132+
messages.push(`${filtersResult.filterCount} filter(s)`);
133+
}
134+
this.showNote(`Successfully migrated ${messages.join(' and ')} to server storage.`);
108135
} else {
109-
this.showNote('No views found to migrate.');
136+
this.showNote('No views or filters found to migrate.');
110137
}
111138
}
112139
} catch (error) {
113-
this.showNote(`Failed to migrate views: ${error.message}`);
140+
this.showNote(`Failed to migrate settings: ${error.message}`);
114141
} finally {
115142
this.setState({ migrationLoading: false });
116143
}
@@ -126,15 +153,21 @@ export default class DashboardSettings extends DashboardView {
126153
return;
127154
}
128155

156+
if (!this.filterPreferencesManager) {
157+
this.showNote('FilterPreferencesManager not initialized');
158+
return;
159+
}
160+
129161
if (!this.scriptManager) {
130162
this.showNote('ScriptManager not initialized');
131163
return;
132164
}
133165

134166
const viewsSuccess = this.viewPreferencesManager.deleteFromBrowser(this.context.applicationId);
167+
const filtersSuccess = this.filterPreferencesManager.deleteFromBrowser(this.context.applicationId);
135168
const scriptsSuccess = this.scriptManager.deleteFromBrowser(this.context.applicationId);
136169

137-
if (viewsSuccess && scriptsSuccess) {
170+
if (viewsSuccess && filtersSuccess && scriptsSuccess) {
138171
this.showNote('Successfully deleted dashboard settings from browser storage.');
139172
} else {
140173
this.showNote('Failed to delete all dashboard settings from browser storage.');
@@ -474,7 +507,7 @@ export default class DashboardSettings extends DashboardView {
474507
{this.viewPreferencesManager && this.scriptManager && this.viewPreferencesManager.isServerConfigEnabled() && (
475508
<Fieldset legend="Settings Storage">
476509
<div style={{ marginBottom: '20px', color: '#666', fontSize: '14px', textAlign: 'center' }}>
477-
Storing dashboard settings on the server rather than locally in the browser storage makes the settings available across devices and browsers. It also prevents them from getting lost when resetting the browser website data. Settings that can be stored on the server are currently Views, Keyboard Shortcuts and JS Console scripts.
510+
Storing dashboard settings on the server rather than locally in the browser storage makes the settings available across devices and browsers. It also prevents them from getting lost when resetting the browser website data. Settings that can be stored on the server are currently Data Browser Filters, Views, Keyboard Shortcuts and JS Console scripts.
478511
</div>
479512
<Field
480513
label={

0 commit comments

Comments
 (0)