mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
fix(ag-grid): persist AG Grid column filters in explore permalinks (#38393)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -98,7 +98,10 @@ export interface ExplorePageInitialData {
|
||||
}
|
||||
|
||||
export interface ExploreResponsePayload {
|
||||
result: ExplorePageInitialData & { message: string };
|
||||
result: ExplorePageInitialData & {
|
||||
message: string;
|
||||
chartState?: JsonObject;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExplorePageState {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user