This is an automated email from the ASF dual-hosted git repository.
michaelsmolina 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 07cd1d89d0 fix(explore): hide advanced analytics for non temporal
xaxis (#28312)
07cd1d89d0 is described below
commit 07cd1d89d0c57b2987e1d9aeb23c2aad518a3dc2
Author: JUST.in DO IT <[email protected]>
AuthorDate: Wed May 8 13:40:28 2024 -0700
fix(explore): hide advanced analytics for non temporal xaxis (#28312)
---
.../src/sections/advancedAnalytics.tsx | 3 +-
.../src/sections/forecastInterval.tsx | 2 +
.../src/shared-controls/sharedControls.tsx | 22 +--
.../superset-ui-chart-controls/src/types.ts | 4 +
.../{index.ts => displayTimeRelatedControls.ts} | 32 +++--
.../superset-ui-chart-controls/src/utils/index.ts | 1 +
.../test/utils/displayTimeRelatedControls.test.ts | 118 ++++++++++++++++
.../src/explore/actions/exploreActions.test.js | 21 +++
.../src/explore/actions/exploreActions.ts | 13 ++
.../components/ControlPanelsContainer.test.tsx | 62 ++++++++-
.../explore/components/ControlPanelsContainer.tsx | 155 ++++++++++++---------
.../components/ExploreViewContainer/index.jsx | 9 +-
.../StashFormDataContainer.test.tsx | 57 ++++++++
.../components/StashFormDataContainer/index.tsx | 50 +++++++
.../src/explore/reducers/exploreReducer.js | 25 ++++
superset-frontend/src/explore/types.ts | 1 +
16 files changed, 475 insertions(+), 100 deletions(-)
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
index 326e26fd5d..926488f51e 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx
@@ -21,7 +21,7 @@ import { t, RollingType, ComparisonType } from
'@superset-ui/core';
import { ControlSubSectionHeader } from
'../components/ControlSubSectionHeader';
import { ControlPanelSectionConfig } from '../types';
-import { formatSelectOptions } from '../utils';
+import { formatSelectOptions, displayTimeRelatedControls } from '../utils';
export const advancedAnalyticsControls: ControlPanelSectionConfig = {
label: t('Advanced analytics'),
@@ -31,6 +31,7 @@ export const advancedAnalyticsControls:
ControlPanelSectionConfig = {
'that allow for advanced analytical post processing ' +
'of query results',
),
+ visibility: displayTimeRelatedControls,
controlSetRows: [
[<ControlSubSectionHeader>{t('Rolling window')}</ControlSubSectionHeader>],
[
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
index 1dff19b83c..67c64725c0 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx
@@ -22,6 +22,7 @@ import {
t,
} from '@superset-ui/core';
import { ControlPanelSectionConfig } from '../types';
+import { displayTimeRelatedControls } from '../utils';
export const FORECAST_DEFAULT_DATA = {
forecastEnabled: false,
@@ -35,6 +36,7 @@ export const FORECAST_DEFAULT_DATA = {
export const forecastIntervalControls: ControlPanelSectionConfig = {
label: t('Predictive Analytics'),
expanded: false,
+ visibility: displayTimeRelatedControls,
controlSetRows: [
[
{
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index 2be91cf5d4..c01b4052d1 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -41,8 +41,6 @@ import {
SequentialScheme,
legacyValidateInteger,
ComparisonType,
- isAdhocColumn,
- isPhysicalColumn,
ensureIsArray,
isDefined,
NO_TIME_RANGE,
@@ -51,6 +49,7 @@ import {
import {
formatSelectOptions,
+ displayTimeRelatedControls,
D3_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
@@ -62,7 +61,6 @@ import { DEFAULT_MAX_ROW, TIME_FILTER_LABELS } from
'../constants';
import {
SharedControlConfig,
Dataset,
- ColumnMeta,
ControlState,
ControlPanelState,
} from '../types';
@@ -203,23 +201,7 @@ const time_grain_sqla:
SharedControlConfig<'SelectControl'> = {
mapStateToProps: ({ datasource }) => ({
choices: (datasource as Dataset)?.time_grain_sqla || [],
}),
- visibility: ({ controls }) => {
- if (!controls?.x_axis) {
- return true;
- }
-
- const xAxis = controls?.x_axis;
- const xAxisValue = xAxis?.value;
- if (isAdhocColumn(xAxisValue)) {
- return true;
- }
- if (isPhysicalColumn(xAxisValue)) {
- return !!(xAxis?.options ?? []).find(
- (col: ColumnMeta) => col?.column_name === xAxisValue,
- )?.is_dttm;
- }
- return false;
- },
+ visibility: displayTimeRelatedControls,
};
const time_range: SharedControlConfig<'DateFilterControl'> = {
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 3d149b1299..fa8154677d 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -376,6 +376,10 @@ export interface ControlPanelSectionConfig {
expanded?: boolean;
tabOverride?: TabOverride;
controlSetRows: ControlSetRow[];
+ visibility?: (
+ props: ControlPanelsContainerProps,
+ controlData: AnyDict,
+ ) => boolean;
}
export interface StandardizedControls {
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
similarity index 57%
copy from
superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
copy to
superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
index 208d708a96..e5e430d158 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts
@@ -16,13 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
-export * from './checkColumnType';
-export * from './selectOptions';
-export * from './D3Formatting';
-export * from './expandControlConfig';
-export * from './getColorFormatters';
-export { default as mainMetric } from './mainMetric';
-export { default as columnChoices } from './columnChoices';
-export * from './defineSavedMetrics';
-export * from './getStandardizedControls';
-export * from './getTemporalColumns';
+import { isAdhocColumn, isPhysicalColumn } from '@superset-ui/core';
+import type { ColumnMeta, ControlPanelsContainerProps } from '../types';
+
+export default function displayTimeRelatedControls({
+ controls,
+}: ControlPanelsContainerProps) {
+ if (!controls?.x_axis) {
+ return true;
+ }
+
+ const xAxis = controls?.x_axis;
+ const xAxisValue = xAxis?.value;
+ if (isAdhocColumn(xAxisValue)) {
+ return true;
+ }
+ if (isPhysicalColumn(xAxisValue)) {
+ return !!(xAxis?.options ?? []).find(
+ (col: ColumnMeta) => col?.column_name === xAxisValue,
+ )?.is_dttm;
+ }
+ return false;
+}
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
index 208d708a96..fb829ea057 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts
@@ -26,3 +26,4 @@ export { default as columnChoices } from './columnChoices';
export * from './defineSavedMetrics';
export * from './getStandardizedControls';
export * from './getTemporalColumns';
+export { default as displayTimeRelatedControls } from
'./displayTimeRelatedControls';
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
new file mode 100644
index 0000000000..f96049293f
--- /dev/null
+++
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { displayTimeRelatedControls } from '../../src';
+
+const mockData = {
+ actions: {
+ setDatasource: jest.fn(),
+ },
+ controls: {
+ x_axis: {
+ type: 'SelectControl' as const,
+ value: 'not_temporal',
+ options: [
+ { column_name: 'not_temporal', is_dttm: false },
+ { column_name: 'ds', is_dttm: true },
+ ],
+ },
+ },
+ exportState: {},
+ form_data: {
+ datasource: '22__table',
+ viz_type: 'table',
+ },
+};
+
+test('returns true when no x-axis exists', () => {
+ expect(
+ displayTimeRelatedControls({
+ ...mockData,
+ controls: {
+ control_options: {
+ type: 'SelectControl',
+ value: 'not_temporal',
+ options: [],
+ },
+ },
+ }),
+ ).toBeTruthy();
+});
+
+test('returns false when x-axis value is not temporal', () => {
+ expect(displayTimeRelatedControls(mockData)).toBeFalsy();
+});
+test('returns true when x-axis value is temporal', () => {
+ expect(
+ displayTimeRelatedControls({
+ ...mockData,
+ controls: {
+ x_axis: {
+ ...mockData.controls.x_axis,
+ value: 'ds',
+ },
+ },
+ }),
+ ).toBeTruthy();
+});
+
+test('returns false when x-axis value without options', () => {
+ expect(
+ displayTimeRelatedControls({
+ ...mockData,
+ controls: {
+ x_axis: {
+ type: 'SelectControl' as const,
+ value: 'not_temporal',
+ },
+ },
+ }),
+ ).toBeFalsy();
+});
+
+test('returns true when x-axis is ad-hoc column', () => {
+ expect(
+ displayTimeRelatedControls({
+ ...mockData,
+ controls: {
+ x_axis: {
+ ...mockData.controls.x_axis,
+ value: {
+ sqlExpression: 'ds',
+ label: 'ds',
+ expressionType: 'SQL',
+ },
+ },
+ },
+ }),
+ ).toBeTruthy();
+});
+
+test('returns false when the x-axis is neither an ad-hoc column nor a physical
column', () => {
+ expect(
+ displayTimeRelatedControls({
+ ...mockData,
+ controls: {
+ x_axis: {
+ ...mockData.controls.x_axis,
+ value: {},
+ },
+ },
+ }),
+ ).toBeFalsy();
+});
diff --git a/superset-frontend/src/explore/actions/exploreActions.test.js
b/superset-frontend/src/explore/actions/exploreActions.test.js
index 54cf8f16c5..700987a0e5 100644
--- a/superset-frontend/src/explore/actions/exploreActions.test.js
+++ b/superset-frontend/src/explore/actions/exploreActions.test.js
@@ -217,4 +217,25 @@ describe('reducers', () => {
expectedColumnConfig,
);
});
+
+ test('setStashFormData works as expected with fieldNames', () => {
+ const newState = exploreReducer(
+ defaultState,
+ actions.setStashFormData(true, ['y_axis_format']),
+ );
+ expect(newState.hiddenFormData).toEqual({
+ y_axis_format: defaultState.form_data.y_axis_format,
+ });
+ expect(newState.form_data.y_axis_format).toBeFalsy();
+ const updatedState = exploreReducer(
+ newState,
+ actions.setStashFormData(false, ['y_axis_format']),
+ );
+ expect(updatedState.hiddenFormData).toEqual({
+ y_axis_format: defaultState.form_data.y_axis_format,
+ });
+ expect(updatedState.form_data.y_axis_format).toEqual(
+ defaultState.form_data.y_axis_format,
+ );
+ });
});
diff --git a/superset-frontend/src/explore/actions/exploreActions.ts
b/superset-frontend/src/explore/actions/exploreActions.ts
index 36300b4a12..da702ac16f 100644
--- a/superset-frontend/src/explore/actions/exploreActions.ts
+++ b/superset-frontend/src/explore/actions/exploreActions.ts
@@ -152,6 +152,18 @@ export function setForceQuery(force: boolean) {
};
}
+export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA';
+export function setStashFormData(
+ isHidden: boolean,
+ fieldNames: ReadonlyArray<string>,
+) {
+ return {
+ type: SET_STASH_FORM_DATA,
+ isHidden,
+ fieldNames,
+ };
+}
+
export const exploreActions = {
...toastActions,
fetchDatasourcesStarted,
@@ -161,6 +173,7 @@ export const exploreActions = {
saveFaveStar,
setControlValue,
setExploreControls,
+ setStashFormData,
updateChartTitle,
createNewSlice,
sliceUpdated,
diff --git
a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
index 333d3ec799..37bdfb4fc0 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
+import { useSelector } from 'react-redux';
import userEvent from '@testing-library/user-event';
import { render, screen } from 'spec/helpers/testing-library';
import {
@@ -24,13 +25,22 @@ import {
getChartControlPanelRegistry,
t,
} from '@superset-ui/core';
-import { defaultControls } from 'src/explore/store';
+import { defaultControls, defaultState } from 'src/explore/store';
+import { ExplorePageState } from 'src/explore/types';
import { getFormDataFromControls } from 'src/explore/controlUtils';
import {
ControlPanelsContainer,
ControlPanelsContainerProps,
} from 'src/explore/components/ControlPanelsContainer';
+const FormDataMock = () => {
+ const formData = useSelector(
+ (state: ExplorePageState) => state.explore.form_data,
+ );
+
+ return <div
data-test="mock-formdata">{Object.keys(formData).join(':')}</div>;
+};
+
describe('ControlPanelsContainer', () => {
beforeAll(() => {
getChartControlPanelRegistry().registerValue('table', {
@@ -144,4 +154,54 @@ describe('ControlPanelsContainer', () => {
await screen.findAllByTestId('collapsible-control-panel-header'),
).toHaveLength(2);
});
+
+ test('visibility of panels is correctly applied', async () => {
+ getChartControlPanelRegistry().registerValue('table', {
+ controlPanelSections: [
+ {
+ label: t('Advanced analytics'),
+ description: t('Advanced analytics post processing'),
+ expanded: true,
+ controlSetRows: [['groupby'], ['metrics'], ['percent_metrics']],
+ visibility: () => false,
+ },
+ {
+ label: t('Chart Title'),
+ visibility: () => true,
+ controlSetRows: [['timeseries_limit_metric', 'row_limit']],
+ },
+ {
+ label: t('Chart Options'),
+ controlSetRows: [['include_time', 'order_desc']],
+ },
+ ],
+ });
+ const { getByTestId } = render(
+ <>
+ <ControlPanelsContainer {...getDefaultProps()} />
+ <FormDataMock />
+ </>,
+ {
+ useRedux: true,
+ initialState: { explore: { form_data: defaultState.form_data } },
+ },
+ );
+
+ const disabledSection = screen.queryByRole('button', {
+ name: /advanced analytics/i,
+ });
+ expect(disabledSection).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: /chart title/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: /chart options/i }),
+ ).toBeInTheDocument();
+
+ expect(getByTestId('mock-formdata')).not.toHaveTextContent('groupby');
+ expect(getByTestId('mock-formdata')).not.toHaveTextContent('metrics');
+ expect(getByTestId('mock-formdata')).not.toHaveTextContent(
+ 'percent_metrics',
+ );
+ });
});
diff --git
a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index d47c1abaf8..06a9072aae 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -71,6 +71,7 @@ import { ExploreAlert } from './ExploreAlert';
import { RunQueryButton } from './RunQueryButton';
import { Operators } from '../constants';
import { Clauses } from './controls/FilterControl/types';
+import StashFormDataContainer from './StashFormDataContainer';
const { confirm } = Modal;
@@ -521,16 +522,22 @@ export const ControlPanelsContainer = (props:
ControlPanelsContainerProps) => {
}
return (
- <Control
- key={`control-${name}`}
- name={name}
- label={label}
- description={description}
- validationErrors={validationErrors}
- actions={props.actions}
- isVisible={isVisible}
- {...restProps}
- />
+ <StashFormDataContainer
+ shouldStash={isVisible === false}
+ fieldNames={[name]}
+ key={`control-container-${name}`}
+ >
+ <Control
+ key={`control-${name}`}
+ name={name}
+ label={label}
+ description={description}
+ validationErrors={validationErrors}
+ actions={props.actions}
+ isVisible={isVisible}
+ {...restProps}
+ />
+ </StashFormDataContainer>
);
};
@@ -543,13 +550,13 @@ export const ControlPanelsContainer = (props:
ControlPanelsContainerProps) => {
section: ExpandedControlPanelSectionConfig,
) => {
const { controls } = props;
- const { label, description } = section;
+ const { label, description, visibility } = section;
// Section label can be a ReactNode but in some places we want to
// have a string ID. Using forced type conversion for now,
// should probably add a `id` field to sections in the future.
const sectionId = String(label);
-
+ const isVisible = visibility?.call(this, props, controls) !== false;
const hasErrors = section.controlSetRows.some(rows =>
rows.some(item => {
const controlName =
@@ -607,67 +614,85 @@ export const ControlPanelsContainer = (props:
ControlPanelsContainerProps) => {
);
return (
- <Collapse.Panel
- css={theme => css`
- margin-bottom: 0;
- box-shadow: none;
-
- &:last-child {
- padding-bottom: ${theme.gridUnit * 16}px;
- border-bottom: 0;
- }
+ <>
+ <StashFormDataContainer
+ key={`sectionId-${sectionId}`}
+ shouldStash={!isVisible}
+ fieldNames={section.controlSetRows
+ .flat()
+ .map(item =>
+ item && typeof item === 'object'
+ ? 'name' in item
+ ? item.name
+ : ''
+ : String(item || ''),
+ )
+ .filter(Boolean)}
+ />
+ {isVisible && (
+ <Collapse.Panel
+ css={theme => css`
+ margin-bottom: 0;
+ box-shadow: none;
+
+ &:last-child {
+ padding-bottom: ${theme.gridUnit * 16}px;
+ border-bottom: 0;
+ }
- .panel-body {
- margin-left: ${theme.gridUnit * 4}px;
- padding-bottom: 0;
- }
+ .panel-body {
+ margin-left: ${theme.gridUnit * 4}px;
+ padding-bottom: 0;
+ }
- span.label {
- display: inline-block;
- }
- ${!section.label &&
- `
+ span.label {
+ display: inline-block;
+ }
+ ${!section.label &&
+ `
.ant-collapse-header {
display: none;
}
`}
- `}
- header={<PanelHeader />}
- key={sectionId}
- >
- {section.controlSetRows.map((controlSets, i) => {
- const renderedControls = controlSets
- .map(controlItem => {
- if (!controlItem) {
- // When the item is invalid
+ `}
+ header={<PanelHeader />}
+ key={sectionId}
+ >
+ {section.controlSetRows.map((controlSets, i) => {
+ const renderedControls = controlSets
+ .map(controlItem => {
+ if (!controlItem) {
+ // When the item is invalid
+ return null;
+ }
+ if (React.isValidElement(controlItem)) {
+ // When the item is a React element
+ return controlItem;
+ }
+ if (
+ controlItem.name &&
+ controlItem.config &&
+ controlItem.name !== 'datasource'
+ ) {
+ return renderControl(controlItem);
+ }
+ return null;
+ })
+ .filter(x => x !== null);
+ // don't show the row if it is empty
+ if (renderedControls.length === 0) {
return null;
}
- if (React.isValidElement(controlItem)) {
- // When the item is a React element
- return controlItem;
- }
- if (
- controlItem.name &&
- controlItem.config &&
- controlItem.name !== 'datasource'
- ) {
- return renderControl(controlItem);
- }
- return null;
- })
- .filter(x => x !== null);
- // don't show the row if it is empty
- if (renderedControls.length === 0) {
- return null;
- }
- return (
- <ControlRow
- key={`controlsetrow-${i}`}
- controls={renderedControls}
- />
- );
- })}
- </Collapse.Panel>
+ return (
+ <ControlRow
+ key={`controlsetrow-${i}`}
+ controls={renderedControls}
+ />
+ );
+ })}
+ </Collapse.Panel>
+ )}
+ </>
);
};
diff --git
a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index 1aeb45cb15..fe5d277242 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -31,7 +31,7 @@ import {
useComponentDidMount,
usePrevious,
} from '@superset-ui/core';
-import { debounce, pick } from 'lodash';
+import { debounce, omit, pick } from 'lodash';
import { Resizable } from 're-resizable';
import { usePluginContext } from 'src/components/DynamicPlugins';
import { Global } from '@emotion/react';
@@ -715,8 +715,11 @@ function mapStateToProps(state) {
user,
saveModal,
} = state;
- const { controls, slice, datasource, metadata } = explore;
- const form_data = getFormDataFromControls(controls);
+ const { controls, slice, datasource, metadata, hiddenFormData } = explore;
+ const form_data = omit(
+ getFormDataFromControls(controls),
+ Object.keys(hiddenFormData ?? {}),
+ );
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved
chart
form_data.extra_form_data = mergeExtraFormData(
{ ...form_data.extra_form_data },
diff --git
a/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
new file mode 100644
index 0000000000..1316411945
--- /dev/null
+++
b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { defaultState } from 'src/explore/store';
+import { render } from 'spec/helpers/testing-library';
+import { useSelector } from 'react-redux';
+import { ExplorePageState } from 'src/explore/types';
+import StashFormDataContainer from '.';
+
+const FormDataMock = () => {
+ const formData = useSelector(
+ (state: ExplorePageState) => state.explore.form_data,
+ );
+
+ return <div>{Object.keys(formData).join(':')}</div>;
+};
+
+test('should stash form data from fieldNames', () => {
+ const { rerender, container } = render(
+ <StashFormDataContainer
+ shouldStash={false}
+ fieldNames={['granularity_sqla']}
+ >
+ <FormDataMock />
+ </StashFormDataContainer>,
+ {
+ useRedux: true,
+ initialState: { explore: { form_data: defaultState.form_data } },
+ },
+ );
+ expect(container.querySelector('div')).toHaveTextContent('granularity_sqla');
+
+ rerender(
+ <StashFormDataContainer shouldStash fieldNames={['granularity_sqla']}>
+ <FormDataMock />
+ </StashFormDataContainer>,
+ );
+ expect(container.querySelector('div')).not.toHaveTextContent(
+ 'granularity_sqla',
+ );
+});
diff --git
a/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx
b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx
new file mode 100644
index 0000000000..9684ddf774
--- /dev/null
+++ b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { useEffect, useRef } from 'react';
+import { useDispatch } from 'react-redux';
+import { setStashFormData } from 'src/explore/actions/exploreActions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+
+type Props = {
+ shouldStash: boolean;
+ fieldNames: ReadonlyArray<string>;
+};
+
+const StashFormDataContainer: React.FC<Props> = ({
+ shouldStash,
+ fieldNames,
+ children,
+}) => {
+ const dispatch = useDispatch();
+ const isMounted = useRef(false);
+ const onVisibleUpdate = useEffectEvent((shouldStash: boolean) =>
+ dispatch(setStashFormData(shouldStash, fieldNames)),
+ );
+ useEffect(() => {
+ if (!isMounted.current && !shouldStash) {
+ isMounted.current = true;
+ } else {
+ onVisibleUpdate(shouldStash);
+ }
+ }, [shouldStash, onVisibleUpdate]);
+
+ return <>{children}</>;
+};
+
+export default StashFormDataContainer;
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js
b/superset-frontend/src/explore/reducers/exploreReducer.js
index 1797c57637..9eddd2f678 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -18,6 +18,7 @@
*/
/* eslint camelcase: 0 */
import { ensureIsArray } from '@superset-ui/core';
+import { omit, pick } from 'lodash';
import { DYNAMIC_PLUGIN_CONTROLS_READY } from
'src/components/Chart/chartAction';
import { getControlsState } from 'src/explore/store';
import {
@@ -245,6 +246,30 @@ export default function exploreReducer(state = {}, action)
{
can_overwrite: action.can_overwrite,
};
},
+ [actions.SET_STASH_FORM_DATA]() {
+ const { form_data, hiddenFormData } = state;
+ const { fieldNames, isHidden } = action;
+ if (isHidden) {
+ return {
+ ...state,
+ hiddenFormData: {
+ ...hiddenFormData,
+ ...pick(form_data, fieldNames),
+ },
+ form_data: omit(form_data, fieldNames),
+ };
+ }
+
+ const restoredField = pick(hiddenFormData, fieldNames);
+ return {
+ ...state,
+ form_data: {
+ ...form_data,
+ ...restoredField,
+ },
+ hiddenFormData,
+ };
+ },
[actions.SLICE_UPDATED]() {
return {
...state,
diff --git a/superset-frontend/src/explore/types.ts
b/superset-frontend/src/explore/types.ts
index ee249e0fc3..51b3013233 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -109,6 +109,7 @@ export interface ExplorePageState {
datasource: Dataset;
controls: ControlStateMapping;
form_data: QueryFormData;
+ hiddenFormData?: Partial<QueryFormData>;
slice: Slice;
controlsTransferred: string[];
standalone: boolean;