This is an automated email from the ASF dual-hosted git repository.
amaan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 9215eb5e45c fix(ag-grid): persist AG Grid column filters in explore
permalinks (#38393)
9215eb5e45c is described below
commit 9215eb5e45c19212a851e6b1004c569fe1764aaf
Author: amaannawab923 <[email protected]>
AuthorDate: Wed Mar 11 01:56:24 2026 +0530
fix(ag-grid): persist AG Grid column filters in explore permalinks (#38393)
---
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 cfcbd8a524e..1863acaebbb 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 0ceb1259b7b..0639ff9f949 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,
FeatureFlag,
getCategoricalSchemeRegistry,
@@ -60,7 +62,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();
@@ -224,12 +231,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 c692e65bfd1..b7bd508f959 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/translation';
import {
@@ -33,6 +34,11 @@ import {
import { Alert } from '@apache-superset/core/components';
import { css, styled, useTheme } from '@apache-superset/core/theme';
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,
@@ -43,6 +49,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';
@@ -126,6 +133,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,
@@ -145,8 +174,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,
@@ -259,7 +314,7 @@ const ExploreChartPanel = ({
<ChartContainer
width={Math.floor(chartPanelWidth)}
height={chartPanelHeight}
- ownState={ownState}
+ ownState={mergedOwnState}
annotationData={chart.annotationData}
chartId={chart.id}
triggerRender={triggerRender}
@@ -277,6 +332,7 @@ const ExploreChartPanel = ({
timeout={timeout}
triggerQuery={chart.triggerQuery}
vizType={vizType}
+ onChartStateChange={handleChartStateChange}
{...(chart.chartAlert && { chartAlert: chart.chartAlert })}
{...(chart.chartStackTrace && {
chartStackTrace: chart.chartStackTrace,
@@ -304,8 +360,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 ed02bd183a7..cb728be3c7b 100644
---
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
+++
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.tsx
@@ -172,7 +172,9 @@ interface ExploreSlice {
interface ExploreState {
charts?: Record<number, ChartState>;
- explore?: ExploreSlice;
+ explore?: ExploreSlice & {
+ chartStates?: Record<number, JsonObject>;
+ };
common?: {
conf?: {
CSV_STREAMING_ROW_THRESHOLD?: number;
@@ -221,6 +223,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);
@@ -274,6 +285,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');
@@ -282,6 +296,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');
@@ -293,7 +309,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;
@@ -411,6 +427,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');
@@ -421,7 +439,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 e940699e32c..faffbfac5fa 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,
@@ -66,6 +71,7 @@ export interface ExploreState {
owners?: string[] | null;
};
saveAction?: SaveActionType | null;
+ chartStates?: Record<number, JsonObject>;
}
// Action type definitions
@@ -165,6 +171,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
@@ -183,6 +196,7 @@ type ExploreAction =
| SetStashFormDataAction
| SliceUpdatedAction
| SetForceQueryAction
+ | UpdateExploreChartStateAction
| HydrateExplore;
// Extended control state for dynamic form controls - uses Record for
flexibility
@@ -621,10 +635,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 a032de3dd76..d1420fbb3fd 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 9f04ade2328..948abdc47f9 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):