,
+) {
+ 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 333d3ec799c22..37bdfb4fc0552 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 {Object.keys(formData).join(':')}
;
+};
+
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(
+ <>
+
+
+ >,
+ {
+ 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 d47c1abaf8e43..06a9072aae5aa 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 (
-
+
+
+
);
};
@@ -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 (
- css`
- margin-bottom: 0;
- box-shadow: none;
-
- &:last-child {
- padding-bottom: ${theme.gridUnit * 16}px;
- border-bottom: 0;
- }
+ <>
+
+ item && typeof item === 'object'
+ ? 'name' in item
+ ? item.name
+ : ''
+ : String(item || ''),
+ )
+ .filter(Boolean)}
+ />
+ {isVisible && (
+ 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={}
- key={sectionId}
- >
- {section.controlSetRows.map((controlSets, i) => {
- const renderedControls = controlSets
- .map(controlItem => {
- if (!controlItem) {
- // When the item is invalid
+ `}
+ header={}
+ 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 (
-
- );
- })}
-
+ return (
+
+ );
+ })}
+
+ )}
+ >
);
};
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index 1aeb45cb15729..fe5d277242beb 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 0000000000000..13164119458ef
--- /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 {Object.keys(formData).join(':')}
;
+};
+
+test('should stash form data from fieldNames', () => {
+ const { rerender, container } = render(
+
+
+ ,
+ {
+ useRedux: true,
+ initialState: { explore: { form_data: defaultState.form_data } },
+ },
+ );
+ expect(container.querySelector('div')).toHaveTextContent('granularity_sqla');
+
+ rerender(
+
+
+ ,
+ );
+ 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 0000000000000..9684ddf774951
--- /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;
+};
+
+const StashFormDataContainer: React.FC = ({
+ 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 1797c57637d2d..9eddd2f678cf5 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 ee249e0fc3170..51b3013233ea6 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;
slice: Slice;
controlsTransferred: string[];
standalone: boolean;