apache / apache/superset

Native filter selections are lost when reopening a dashboard — no persistence without manually preserving the URL key

Open
#43,057 4 comments 0 reactions 0 assignees View on GitHub
dashboard:native-filters enhancement:request
Dominant language
Python
Stars
74.8k
Forks
18.3k
Avg merge
2d 5h
Merged PRs (30d)
685

Description

### Bug description

Superset version: 6.1.0

Bug description

When a user applies native dashboard filters, Superset generates a native_filters_key and appends it to the URL via history.replaceState. This only restores filter state if the user manually retains and reuses that exact URL. On every other normal navigation path — opening the dashboard from the dashboard list, the nav menu, a bookmark, or a fresh tab — there's no native_filters_key in the URL, so applied filters are silently discarded and revert to their configured defaults (or empty, if none is set).

There is currently no mechanism that remembers a user's last-applied filter state for a dashboard across normal navigation.

Steps to reproduce
Open any dashboard with native filters enabled.
Apply one or more filter values.
Navigate away via the UI (not by editing the URL) — e.g. through the Superset menu — and reopen the same dashboard from the dashboard list.
Observe: filters have reset to configured defaults, or are empty if no default is set. The user's selections are gone.
Expected behavior

A user's filter selections should persist for that user/dashboard across normal navigation, not only when the exact permalink URL (with native_filters_key) is manually preserved.

Actual behavior

Filter state is only recoverable via the exact URL containing native_filters_key. Any other route back to the dashboard loses it entirely.

Impact

Anyone using dashboards for ongoing/repeated analysis loses filter context every time they navigate away and back through the normal UI, not just after a browser restart. This affects daily usability, not an edge case.

Root cause
Filter state is stored server-side, keyed by native_filters_key; nothing in the browser or per-user backend remembers "last state" independent of that key.
The older Filter Sets feature (named, saved filter states) was removed in Superset 4.0, and nothing has replaced its implicit "restore last state" behavior since.
No client-side (localStorage) or per-user server-side fallback exists for when the key isn't present in the URL.
Proposed solution

A lightweight client-side fix: persist the dashboard's dataMask to localStorage, keyed per dashboard, and fall back to it when no native_filters_key/permalinkKey is present in the URL on load.

Implementation sketch (in DashboardPage.tsx):

ts
const DASHBOARD_FILTERS_STORAGE_PREFIX = 'superset_dashboard_filters_';

function getSavedDashboardFilters(dashboardId: number) {
try {
const raw = localStorage.getItem(`${DASHBOARD_FILTERS_STORAGE_PREFIX}${dashboardId}`);
return raw ? JSON.parse(raw) : null;
} catch {
return null; // storage disabled, quota exceeded, corrupt JSON, etc.
}
}

function saveDashboardFilters(dashboardId: number, dataMask: unknown) {
try {
localStorage.setItem(`${DASHBOARD_FILTERS_STORAGE_PREFIX}${dashboardId}`, JSON.stringify(dataMask));
} catch {
// fail silently — persistence is a nice-to-have, not critical path
}
}

Save effect — gated on the hydrated dashboard id matching the current id, to avoid writing one dashboard's mask under another dashboard's key during SPA navigation:

ts
const hydratedDashboardId = useSelector(
state => state.dashboardInfo?.id,
);

useEffect(() => {
if (!id || hydratedDashboardId !== id) return;
saveDashboardFilters(id, fullDataMask);
}, [id, hydratedDashboardId, fullDataMask]);

Load path — only fall back to the saved mask when neither a permalink key nor a native filters key is present in the URL:

ts
else {
const savedFilters = getSavedDashboardFilters(id);
if (savedFilters) {
dataMask = savedFilters;
}
}

Note on the gating condition: an earlier version of this fix used a useRef flag (isDashboardHydrated) instead of comparing against hydratedDashboardId. That approach had a bug — the ref wasn't reset when id changed during in-app (SPA) navigation between dashboards, so a stale true value combined with the previous dashboard's still-in-memory dataMask could cause one dashboard's filter state to be saved under another dashboard's storage key. Using hydratedDashboardId === id (driven by Redux state rather than a ref) avoids this, since it's only true once the specific dashboard being viewed has actually finished hydrating.

Known limitations of this approach (open for discussion)
Per-browser, not per-account. Doesn't sync across devices/browsers for the same user.
No per-user scoping in the sketch above — on a shared browser profile, one user's saved filters could be shown to another user of the same browser. Should key by ${dashboardId}_${userId} if a per-user fix is required.
No invalidation on filter config changes. If an admin changes a filter's default or the filter set itself, a stale saved dataMask will keep overriding the new default indefinitely.
Filter values (potentially including free-text search terms) persist indefinitely client-side with no TTL — worth flagging for privacy-sensitive deployments.

A more complete fix would move this server-side (e.g. a last_filter_state reference per user+dashboard), which would also solve the per-device and multi-user sync gaps. Opening this as a starting point for discussion on which approach is preferred upstream.

### Screenshots/recordings

_No response_

### Superset version

master / latest-dev

### Python version

3.11

### Node version

16

### Browser

Chrome

### Additional context

_No response_

### Checklist

- [ ] I have searched Superset docs and Slack and didn't find a solution to my problem.
- [x] I have searched the GitHub issue tracker and didn't find a similar bug report.
- [ ] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Contributor guide

Open the contributing guide

Research direction

Start in DashboardPage.tsx and trace how dataMask is loaded when native_filters_key or permalinkKey is absent. Review the proposed localStorage fallback and hydratedDashboardId gating, then determine the chosen persistence scope and invalidation behavior. Done means filter state survives normal dashboard navigation without URL keys and cannot be saved across dashboards accidentally.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
analytics, data-visualization, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.