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 <[email protected]> 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):
