You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by am...@apache.org on 2026/03/04 09:19:11 UTC

(superset) branch fix/ag-grid-column-filters-permalink-persistence created (now 1a61c148435)

This is an automated email from the ASF dual-hosted git repository.

amaan pushed a change to branch fix/ag-grid-column-filters-permalink-persistence
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 1a61c148435 fix ag grid table column filters not persisting in explore permalinks

This branch includes the following new commits:

     new 1a61c148435 fix ag grid table column filters not persisting in explore permalinks

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fix ag grid table column filters not persisting in explore permalinks

Posted by am...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

amaan pushed a commit to branch fix/ag-grid-column-filters-permalink-persistence
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1a61c14843531002dcf27c75ed2879d050fc8761
Author: amaannawab923 <am...@gmail.com>
AuthorDate: Wed Mar 4 14:46:52 2026 +0530

    fix ag grid table column filters not persisting in explore permalinks
---
 superset-frontend/src/dataMask/reducer.ts          | 20 ++++++-
 .../src/explore/actions/exploreActions.ts          | 13 +++++
 .../src/explore/actions/hydrateExplore.ts          | 14 +++--
 .../explore/components/ExploreChartPanel/index.tsx | 61 +++++++++++++++++++++-
 .../useExploreAdditionalActionsMenu/index.tsx      | 24 +++++++--
 .../src/explore/reducers/exploreReducer.ts         | 33 +++++++++++-
 superset-frontend/src/explore/types.ts             |  5 +-
 superset-frontend/src/pages/Chart/index.tsx        | 16 ++++++
 superset-frontend/src/utils/urlUtils.ts            |  9 +++-
 superset/commands/explore/get.py                   |  7 ++-
 superset/explore/permalink/schemas.py              | 10 ++++
 superset/explore/permalink/types.py                |  1 +
 12 files changed, 198 insertions(+), 15 deletions(-)

diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts
index 011cc49440c..90051cbe54a 100644
--- a/superset-frontend/src/dataMask/reducer.ts
+++ b/superset-frontend/src/dataMask/reducer.ts
@@ -36,6 +36,10 @@ import {
   isChartCustomization,
 } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
 import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
+import {
+  HYDRATE_EXPLORE,
+  HydrateExplore,
+} from 'src/explore/actions/hydrateExplore';
 import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
 import {
   migrateChartCustomizationArray,
@@ -195,7 +199,7 @@ function updateDataMaskForFilterChanges(
 const dataMaskReducer = produce(
   (
     draft: DataMaskStateWithId,
-    action: AnyDataMaskAction | HydrateDashboardAction,
+    action: AnyDataMaskAction | HydrateDashboardAction | HydrateExplore,
   ) => {
     const cleanState: DataMaskStateWithId = {};
     switch (action.type) {
@@ -286,6 +290,20 @@ const dataMaskReducer = produce(
 
         return cleanState;
       }
+      case HYDRATE_EXPLORE: {
+        const hydrateExploreAction = action as HydrateExplore;
+        const loadedDataMask = hydrateExploreAction.data.dataMask;
+        if (loadedDataMask) {
+          Object.entries(loadedDataMask).forEach(([id, mask]) => {
+            draft[id] = {
+              ...getInitialDataMask(id),
+              ...draft[id],
+              ...mask,
+            };
+          });
+        }
+        return draft;
+      }
       case SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE:
         updateDataMaskForFilterChanges(
           action.filterChanges,
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts b/superset-frontend/src/explore/actions/exploreActions.ts
index 3855e3b93bf..4c59a6783c3 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -153,6 +153,19 @@ export function setForceQuery(force: boolean) {
   };
 }
 
+export const UPDATE_EXPLORE_CHART_STATE = 'UPDATE_EXPLORE_CHART_STATE';
+export function updateExploreChartState(
+  chartId: number,
+  chartState: Record<string, unknown>,
+) {
+  return {
+    type: UPDATE_EXPLORE_CHART_STATE,
+    chartId,
+    chartState,
+    lastModified: Date.now(),
+  };
+}
+
 export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA';
 export function setStashFormData(
   isHidden: boolean,
diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts
index b8e0375a648..3201fd3f8ff 100644
--- a/superset-frontend/src/explore/actions/hydrateExplore.ts
+++ b/superset-frontend/src/explore/actions/hydrateExplore.ts
@@ -28,6 +28,8 @@ import { getControlsState } from 'src/explore/store';
 import { Dispatch } from 'redux';
 import {
   Currency,
+  DataMaskStateWithId,
+  JsonObject,
   ensureIsArray,
   getCategoricalSchemeRegistry,
   getColumnLabel,
@@ -58,7 +60,12 @@ export const hydrateExplore =
     dataset,
     metadata,
     saveAction = null,
-  }: ExplorePageInitialData) =>
+    dataMask,
+    chartStates,
+  }: ExplorePageInitialData & {
+    dataMask?: DataMaskStateWithId;
+    chartStates?: Record<number, JsonObject>;
+  }) =>
   (dispatch: Dispatch, getState: () => ExplorePageState) => {
     const { user, datasources, charts, sliceEntities, common, explore } =
       getState();
@@ -213,12 +220,13 @@ export const hydrateExplore =
           saveModalAlert: null,
           isVisible: false,
         },
-        explore: exploreState,
+        explore: { ...exploreState, chartStates },
+        dataMask,
       },
     });
   };
 
 export type HydrateExplore = {
   type: typeof HYDRATE_EXPLORE;
-  data: ExplorePageState;
+  data: ExplorePageState & { dataMask?: DataMaskStateWithId };
 };
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
index fd29a604383..55bfb3e5e03 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { useState, useEffect, useCallback, useMemo, ReactNode } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
 import Split from 'react-split';
 import { t } from '@apache-superset/core';
 import {
@@ -32,6 +33,11 @@ import {
 } from '@superset-ui/core';
 import { css, styled, useTheme, Alert } from '@apache-superset/core/ui';
 import ChartContainer from 'src/components/Chart/ChartContainer';
+import { updateExploreChartState } from 'src/explore/actions/exploreActions';
+import {
+  convertChartStateToOwnState,
+  hasChartStateConverter,
+} from 'src/dashboard/util/chartStateConverter';
 import {
   getItem,
   setItem,
@@ -42,6 +48,7 @@ import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
 import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
 import { getChartRequiredFieldsMissingMessage } from 'src/utils/getChartRequiredFieldsMissingMessage';
 import type { ChartState, Datasource } from 'src/explore/types';
+import type { ExploreState } from 'src/explore/reducers/exploreReducer';
 import type { Slice } from 'src/types/Chart';
 import LastQueriedLabel from 'src/components/LastQueriedLabel';
 import { DataTablesPane } from '../DataTablesPane';
@@ -125,6 +132,28 @@ const Styles = styled.div<{ showSplite: boolean }>`
   }
 `;
 
+const EMPTY_OBJECT: Record<string, never> = {};
+
+const createOwnStateWithChartState = (
+  baseOwnState: JsonObject,
+  chartState: { state?: JsonObject } | undefined,
+  vizTypeArg: string,
+): JsonObject => {
+  if (!hasChartStateConverter(vizTypeArg)) {
+    return baseOwnState;
+  }
+  const state = chartState?.state;
+  if (!state) {
+    return baseOwnState;
+  }
+  const convertedState = convertChartStateToOwnState(vizTypeArg, state);
+  return {
+    ...baseOwnState,
+    ...convertedState,
+    chartState: state,
+  };
+};
+
 const ExploreChartPanel = ({
   chart,
   slice,
@@ -144,8 +173,34 @@ const ExploreChartPanel = ({
   can_download: canDownload,
 }: ExploreChartPanelProps) => {
   const theme = useTheme();
+  const dispatch = useDispatch();
   const gutterMargin = theme.sizeUnit * GUTTER_SIZE_FACTOR;
   const gutterHeight = theme.sizeUnit * GUTTER_SIZE_FACTOR;
+
+  const chartState = useSelector(
+    (state: { explore?: ExploreState }) =>
+      state.explore?.chartStates?.[chart.id],
+  );
+
+  const handleChartStateChange = useCallback(
+    (chartStateArg: JsonObject) => {
+      if (hasChartStateConverter(vizType)) {
+        dispatch(updateExploreChartState(chart.id, chartStateArg));
+      }
+    },
+    [dispatch, chart.id, vizType],
+  );
+
+  const mergedOwnState = useMemo(
+    () =>
+      createOwnStateWithChartState(
+        ownState || EMPTY_OBJECT,
+        chartState as { state?: JsonObject } | undefined,
+        vizType,
+      ),
+    [ownState, chartState, vizType],
+  );
+
   const {
     ref: chartPanelRef,
     observerRef: resizeObserverRef,
@@ -258,7 +313,7 @@ const ExploreChartPanel = ({
           <ChartContainer
             width={Math.floor(chartPanelWidth)}
             height={chartPanelHeight}
-            ownState={ownState}
+            ownState={mergedOwnState}
             annotationData={chart.annotationData}
             chartId={chart.id}
             triggerRender={triggerRender}
@@ -276,6 +331,7 @@ const ExploreChartPanel = ({
             timeout={timeout}
             triggerQuery={chart.triggerQuery}
             vizType={vizType}
+            onChartStateChange={handleChartStateChange}
             {...(chart.chartAlert && { chartAlert: chart.chartAlert })}
             {...(chart.chartStackTrace && {
               chartStackTrace: chart.chartStackTrace,
@@ -303,8 +359,9 @@ const ExploreChartPanel = ({
       errorMessage,
       force,
       formData,
+      handleChartStateChange,
       onQuery,
-      ownState,
+      mergedOwnState,
       timeout,
       triggerRender,
       vizType,
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
index db491c134ba..de6f59cd854 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
@@ -171,7 +171,9 @@ interface ExploreSlice {
 
 interface ExploreState {
   charts?: Record<number, ChartState>;
-  explore?: ExploreSlice;
+  explore?: ExploreSlice & {
+    chartStates?: Record<number, JsonObject>;
+  };
   common?: {
     conf?: {
       CSV_STREAMING_ROW_THRESHOLD?: number;
@@ -220,6 +222,15 @@ export const useExploreAdditionalActionsMenu = (
       state.common?.conf?.CSV_STREAMING_ROW_THRESHOLD ||
       DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
   );
+  const exploreChartState = useSelector<
+    ExploreState,
+    JsonObject | undefined
+  >(state => {
+    const chartKey = state.explore ? getChartKey(state.explore) : undefined;
+    return chartKey != null
+      ? state.explore?.chartStates?.[chartKey]
+      : undefined;
+  });
 
   // Streaming export state and handlers
   const [isStreamingModalVisible, setIsStreamingModalVisible] = useState(false);
@@ -273,6 +284,9 @@ export const useExploreAdditionalActionsMenu = (
     'EXPORT_CURRENT_VIEW' as Behavior,
   );
 
+  const permalinkChartState = (exploreChartState as { state?: JsonObject })
+    ?.state;
+
   const shareByEmail = useCallback(async () => {
     try {
       const subject = t('Superset Chart');
@@ -281,6 +295,8 @@ export const useExploreAdditionalActionsMenu = (
       }
       const result = await getChartPermalink(
         latestQueryFormData as Pick<QueryFormData, 'datasource'>,
+        undefined,
+        permalinkChartState,
       );
       if (!result?.url) {
         throw new Error('Failed to generate permalink');
@@ -292,7 +308,7 @@ export const useExploreAdditionalActionsMenu = (
     } catch (error) {
       addDangerToast(t('Sorry, something went wrong. Try again later.'));
     }
-  }, [addDangerToast, latestQueryFormData]);
+  }, [addDangerToast, latestQueryFormData, permalinkChartState]);
 
   const exportCSV = useCallback(() => {
     if (!canDownloadCSV) return null;
@@ -410,6 +426,8 @@ export const useExploreAdditionalActionsMenu = (
       await copyTextToClipboard(async () => {
         const result = await getChartPermalink(
           latestQueryFormData as Pick<QueryFormData, 'datasource'>,
+          undefined,
+          permalinkChartState,
         );
         if (!result?.url) {
           throw new Error('Failed to generate permalink');
@@ -420,7 +438,7 @@ export const useExploreAdditionalActionsMenu = (
     } catch (error) {
       addDangerToast(t('Sorry, something went wrong. Try again later.'));
     }
-  }, [addDangerToast, addSuccessToast, latestQueryFormData]);
+  }, [addDangerToast, addSuccessToast, latestQueryFormData, permalinkChartState]);
 
   // Minimal client-side CSV builder used for "Current View" when pagination is disabled
   const downloadClientCSV = (
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.ts b/superset-frontend/src/explore/reducers/exploreReducer.ts
index d038dd40003..47b0bf9f01b 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.ts
+++ b/superset-frontend/src/explore/reducers/exploreReducer.ts
@@ -17,7 +17,12 @@
  * under the License.
  */
 /* eslint camelcase: 0 */
-import { ensureIsArray, QueryFormData, JsonValue } from '@superset-ui/core';
+import {
+  ensureIsArray,
+  QueryFormData,
+  JsonValue,
+  JsonObject,
+} from '@superset-ui/core';
 import {
   ControlState,
   ControlStateMapping,
@@ -64,6 +69,7 @@ export interface ExploreState {
     owners?: string[] | null;
   };
   saveAction?: SaveActionType | null;
+  chartStates?: Record<number, JsonObject>;
 }
 
 // Action type definitions
@@ -163,6 +169,13 @@ interface SetForceQueryAction {
   force: boolean;
 }
 
+interface UpdateExploreChartStateAction {
+  type: typeof actions.UPDATE_EXPLORE_CHART_STATE;
+  chartId: number;
+  chartState: Record<string, unknown>;
+  lastModified: number;
+}
+
 type ExploreAction =
   | DynamicPluginControlsReadyAction
   | ToggleFaveStarAction
@@ -181,6 +194,7 @@ type ExploreAction =
   | SetStashFormDataAction
   | SliceUpdatedAction
   | SetForceQueryAction
+  | UpdateExploreChartStateAction
   | HydrateExplore;
 
 // Extended control state for dynamic form controls - uses Record for flexibility
@@ -619,10 +633,25 @@ export default function exploreReducer(
         force: typedAction.force,
       };
     },
+    [actions.UPDATE_EXPLORE_CHART_STATE]() {
+      const typedAction = action as UpdateExploreChartStateAction;
+      return {
+        ...state,
+        chartStates: {
+          ...state.chartStates,
+          [typedAction.chartId]: {
+            chartId: typedAction.chartId,
+            state: typedAction.chartState,
+            lastModified: typedAction.lastModified,
+          },
+        },
+      };
+    },
     [HYDRATE_EXPLORE]() {
       const typedAction = action as HydrateExplore;
+      const exploreData = typedAction.data.explore;
       return {
-        ...typedAction.data.explore,
+        ...exploreData,
       } as ExploreState;
     },
   };
diff --git a/superset-frontend/src/explore/types.ts b/superset-frontend/src/explore/types.ts
index 6bc6b1bb303..c3f8614ec3e 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -98,7 +98,10 @@ export interface ExplorePageInitialData {
 }
 
 export interface ExploreResponsePayload {
-  result: ExplorePageInitialData & { message: string };
+  result: ExplorePageInitialData & {
+    message: string;
+    chartState?: JsonObject;
+  };
 }
 
 export interface ExplorePageState {
diff --git a/superset-frontend/src/pages/Chart/index.tsx b/superset-frontend/src/pages/Chart/index.tsx
index 26926a1c8d6..cd8e3aa4b4c 100644
--- a/superset-frontend/src/pages/Chart/index.tsx
+++ b/superset-frontend/src/pages/Chart/index.tsx
@@ -150,11 +150,27 @@ export default function ExplorePage() {
               )
             : result.form_data;
 
+          let chartStates: Record<number, JsonObject> | undefined;
+          if (result.chartState) {
+            const sliceId =
+              getUrlParam(URL_PARAMS.sliceId) ||
+              (formData as JsonObject).slice_id ||
+              0;
+            chartStates = {
+              [sliceId]: {
+                chartId: sliceId,
+                state: result.chartState,
+                lastModified: Date.now(),
+              },
+            };
+          }
+
           dispatch(
             hydrateExplore({
               ...result,
               form_data: formData,
               saveAction,
+              chartStates,
             }),
           );
         })
diff --git a/superset-frontend/src/utils/urlUtils.ts b/superset-frontend/src/utils/urlUtils.ts
index 6c3a51a987a..4b218201144 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -195,11 +195,16 @@ async function resolvePermalinkUrl(
 export async function getChartPermalink(
   formData: Pick<QueryFormData, 'datasource'>,
   excludedUrlParams?: string[],
+  chartState?: JsonObject,
 ): Promise<PermalinkResult> {
-  const result = await getPermalink('/api/v1/explore/permalink', {
+  const payload: JsonObject = {
     formData,
     urlParams: getChartUrlParams(excludedUrlParams),
-  });
+  };
+  if (chartState && Object.keys(chartState).length > 0) {
+    payload.chartState = chartState;
+  }
+  const result = await getPermalink('/api/v1/explore/permalink', payload);
   return resolvePermalinkUrl(result);
 }
 
diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py
index 78142eb5ec1..31b10c42049 100644
--- a/superset/commands/explore/get.py
+++ b/superset/commands/explore/get.py
@@ -62,6 +62,7 @@ class GetExploreCommand(BaseCommand, ABC):
     # pylint: disable=too-many-locals,too-many-branches,too-many-statements
     def run(self) -> Optional[dict[str, Any]]:  # noqa: C901
         initial_form_data = {}
+        permalink_chart_state = None
         if self._permalink_key is not None:
             command = GetExplorePermalinkCommand(self._permalink_key)
             permalink_value = command.run()
@@ -72,6 +73,7 @@ class GetExploreCommand(BaseCommand, ABC):
             url_params = state.get("urlParams")
             if url_params:
                 initial_form_data["url_params"] = dict(url_params)
+            permalink_chart_state = state.get("chartState")
         elif self._form_data_key:
             parameters = FormDataCommandParameters(key=self._form_data_key)
             value = GetFormDataCommand(parameters).run()
@@ -168,13 +170,16 @@ class GetExploreCommand(BaseCommand, ABC):
             if slc.changed_by:
                 metadata["changed_by"] = slc.changed_by.get_full_name()
 
-        return {
+        result: dict[str, Any] = {
             "dataset": sanitize_datasource_data(datasource_data),
             "form_data": form_data,
             "slice": slc.data if slc else None,
             "message": message,
             "metadata": metadata,
         }
+        if permalink_chart_state:
+            result["chartState"] = permalink_chart_state
+        return result
 
     def validate(self) -> None:
         pass
diff --git a/superset/explore/permalink/schemas.py b/superset/explore/permalink/schemas.py
index 63466b0c1ba..aa5fef0c46b 100644
--- a/superset/explore/permalink/schemas.py
+++ b/superset/explore/permalink/schemas.py
@@ -41,6 +41,16 @@ class ExplorePermalinkStateSchema(Schema):
         allow_none=True,
         metadata={"description": "URL Parameters"},
     )
+    chartState = fields.Dict(  # noqa: N815
+        required=False,
+        allow_none=True,
+        metadata={
+            "description": (
+                "Chart-level state for stateful tables "
+                "(column filters, sorting, column order)"
+            )
+        },
+    )
 
 
 class ExplorePermalinkSchema(Schema):
diff --git a/superset/explore/permalink/types.py b/superset/explore/permalink/types.py
index 7eb4a7cb6b1..8cf8fc930ab 100644
--- a/superset/explore/permalink/types.py
+++ b/superset/explore/permalink/types.py
@@ -20,6 +20,7 @@ from typing import Any, Optional, TypedDict
 class ExplorePermalinkState(TypedDict, total=False):
     formData: dict[str, Any]
     urlParams: Optional[list[tuple[str, str]]]
+    chartState: Optional[dict[str, Any]]
 
 
 class ExplorePermalinkValue(TypedDict):