This is an automated email from the ASF dual-hosted git repository. diegopucci pushed a commit to branch feat/disable-controls-sections in repository https://gitbox.apache.org/repos/asf/superset.git
commit 36bccaba7008a42ed60798b9fa15c1bd93e97e59 Author: geido <[email protected]> AuthorDate: Wed Dec 14 18:27:50 2022 +0100 Implement ability to disable sections --- .../src/sections/advancedAnalytics.tsx | 12 +- .../src/sections/forecastInterval.tsx | 6 + .../superset-ui-chart-controls/src/types.ts | 2 + .../utils/{index.ts => disabledSectionRules.ts} | 35 +++- .../superset-ui-chart-controls/src/utils/index.ts | 1 + .../test/utils/disabledSectionRules.test.ts | 75 ++++++++ .../components/ControlPanelSection.test.tsx | 91 +++++++++ .../src/explore/components/ControlPanelSection.tsx | 206 +++++++++++++++++++++ .../explore/components/ControlPanelsContainer.tsx | 134 +++----------- 9 files changed, 444 insertions(+), 118 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 c67018f1fe..eeb3e9a8da 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 @@ -19,7 +19,11 @@ import React from 'react'; import { t, RollingType, ComparisionType } from '@superset-ui/core'; import { ControlPanelSectionConfig } from '../types'; -import { formatSelectOptions } from '../utils'; +import { + formatSelectOptions, + isSectionDisabled, + SectionRuleType, +} from '../utils'; export const advancedAnalyticsControls: ControlPanelSectionConfig = { label: t('Advanced analytics'), @@ -195,4 +199,10 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = { }, ], ], + expanded: false, + setDisabled: ({ exploreState }) => + isSectionDisabled(SectionRuleType.X_AXIS_TEMPORAL, exploreState), + disabledTooltipText: t( + 'These controls are only available if a temporal x-axis is selected', + ), }; 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..23715cbcb0 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 { isSectionDisabled, SectionRuleType } from '../utils'; export const FORECAST_DEFAULT_DATA = { forecastEnabled: false, @@ -134,4 +135,9 @@ export const forecastIntervalControls: ControlPanelSectionConfig = { }, ], ], + setDisabled: ({ exploreState }) => + isSectionDisabled(SectionRuleType.X_AXIS_TEMPORAL, exploreState), + disabledTooltipText: t( + 'These controls are only available if a temporal x-axis is selected', + ), }; 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 60cda2ede8..9c279ea7d1 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -370,6 +370,8 @@ export interface ControlPanelSectionConfig { expanded?: boolean; tabOverride?: TabOverride; controlSetRows: ControlSetRow[]; + setDisabled?: (props: ControlPanelsContainerProps) => boolean; + disabledTooltipText?: string; } 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/disabledSectionRules.ts similarity index 51% copy from superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts copy to superset-frontend/packages/superset-ui-chart-controls/src/utils/disabledSectionRules.ts index 4fa4243c1e..fb9ae54aa0 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/disabledSectionRules.ts @@ -16,12 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -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 { ensureIsArray, GenericDataType } from '@superset-ui/core'; + +export enum SectionRuleType { + X_AXIS_TEMPORAL = 'XAxisTemporal', +} + +export function isSectionDisabled(rule: SectionRuleType, exploreState: any) { + switch (rule) { + case SectionRuleType.X_AXIS_TEMPORAL: { + if (exploreState?.form_data?.x_axis) { + const { datasource, form_data } = exploreState; + const xAxis = ensureIsArray(form_data?.x_axis)[0]; + const column = ensureIsArray(datasource.columns).find( + (col: { column_name: string }) => col?.column_name === xAxis, + ); + if (column?.type_generic !== GenericDataType.TEMPORAL) { + return true; + } + } + return false; + } + default: + 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 4fa4243c1e..e7fb232ef0 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 @@ -25,3 +25,4 @@ export { default as columnChoices } from './columnChoices'; export * from './defineSavedMetrics'; export * from './getStandardizedControls'; export * from './getTemporalColumns'; +export * from './disabledSectionRules'; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/disabledSectionRules.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/disabledSectionRules.test.ts new file mode 100644 index 0000000000..b3369a49d0 --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/disabledSectionRules.test.ts @@ -0,0 +1,75 @@ +/** + * 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 { GenericDataType } from '@superset-ui/core'; +import { isSectionDisabled, SectionRuleType } from '../../src'; + +const nonTemporal = { + datasource: { + columns: [ + { + column_name: 'test', + type_generic: GenericDataType.NUMERIC, + }, + ], + }, + form_data: { + x_axis: 'test', + }, +}; + +const temporal = { + datasource: { + columns: [ + { + column_name: 'test', + type_generic: GenericDataType.TEMPORAL, + }, + ], + }, + form_data: { + x_axis: 'test', + }, +}; + +test('disables the section when X axis is not temporal', () => { + expect( + isSectionDisabled(SectionRuleType.X_AXIS_TEMPORAL, nonTemporal), + ).toEqual(true); +}); + +test('enables the section when X axis is not temporal', () => { + expect(isSectionDisabled(SectionRuleType.X_AXIS_TEMPORAL, temporal)).toEqual( + false, + ); +}); + +test('enables the section when X axis is not available', () => { + expect( + isSectionDisabled(SectionRuleType.X_AXIS_TEMPORAL, { + ...nonTemporal, + form_data: {}, + }), + ).toEqual(false); +}); + +test('shows by default', () => { + // @ts-ignore + expect(isSectionDisabled(undefined, temporal)).toEqual(false); +}); diff --git a/superset-frontend/src/explore/components/ControlPanelSection.test.tsx b/superset-frontend/src/explore/components/ControlPanelSection.test.tsx new file mode 100644 index 0000000000..32c4dae61c --- /dev/null +++ b/superset-frontend/src/explore/components/ControlPanelSection.test.tsx @@ -0,0 +1,91 @@ +/** + * 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 { promiseTimeout } from '@superset-ui/core'; +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import Collapse from 'src/components/Collapse'; +import { ControlPanelSection } from './ControlPanelSection'; + +const defaultProps = { + sectionId: 'query', + hasErrors: false, + errorColor: '#E04355', + section: { + label: 'mock section', + controlSetRows: [], + }, + isVisible: true, + renderControl: jest.fn(), + actions: { + setControlValue: jest.fn(), + }, +}; + +const setup = (overrides = {}) => ( + <Collapse defaultActiveKey={['query']}> + <ControlPanelSection {...defaultProps} {...overrides} /> + </Collapse> +); + +it('should render component', () => { + render(setup()); + expect(screen.getByText('mock section')).toBeInTheDocument(); +}); + +it('should render as disabled if isDisabled is true', () => { + render( + setup({ + isDisabled: true, + section: { + ...defaultProps.section, + disabledTooltipText: 'Test tooltip', + }, + }), + ); + expect(screen.getByTestId('disabled-section-tooltip')).toBeInTheDocument(); +}); + +it('should call renderControl properly', () => { + render( + setup({ + section: { + label: 'mock section', + controlSetRows: [ + [null], + [<div />], + [{ name: 'control', config: { type: 'CheckboxControl' } }], + [{ name: 'datasource', config: {} }], + ], + }, + }), + ); + expect(defaultProps.renderControl).toBeCalledTimes(1); +}); + +it('should call setControlValue if isDisabled is false', () => { + render( + setup({ + isDisabled: false, + }), + ); + + promiseTimeout(() => { + expect(defaultProps.actions.setControlValue).toBeCalled(); + }, 100); +}); diff --git a/superset-frontend/src/explore/components/ControlPanelSection.tsx b/superset-frontend/src/explore/components/ControlPanelSection.tsx new file mode 100644 index 0000000000..b0e9bc9812 --- /dev/null +++ b/superset-frontend/src/explore/components/ControlPanelSection.tsx @@ -0,0 +1,206 @@ +/** + * 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, { useCallback, useEffect } from 'react'; +import { + ControlPanelSectionConfig, + CustomControlItem, + ExpandedControlItem, +} from '@superset-ui/chart-controls'; +import { ensureIsArray, SupersetTheme, t, css } from '@superset-ui/core'; +import Collapse from 'src/components/Collapse'; +import { Tooltip } from 'src/components/Tooltip'; +import { kebabCase } from 'lodash'; +import Icons from 'src/components/Icons'; +import { usePrevious } from 'src/hooks/usePrevious'; +import { ExploreActions } from 'src/explore/actions/exploreActions'; +import ControlRow from './ControlRow'; + +export type ExpandedControlPanelSectionConfig = Omit< + ControlPanelSectionConfig, + 'controlSetRows' +> & { + controlSetRows: ExpandedControlItem[][]; +}; + +export const iconStyles = css` + &.anticon { + font-size: unset; + .anticon { + line-height: unset; + vertical-align: unset; + } + } +`; + +type IControlPanelSectionProps = { + actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>; + sectionId: string; + section: ExpandedControlPanelSectionConfig; + hasErrors: boolean; + errorColor: string; + renderControl: (item: CustomControlItem) => JSX.Element; + isDisabled?: boolean; +}; + +export function ControlPanelSection({ + actions: { setControlValue }, + sectionId, + section, + hasErrors, + errorColor, + renderControl, + isDisabled, + ...restProps // https://github.com/react-component/collapse/issues/73#issuecomment-323626120 +}: IControlPanelSectionProps) { + const { label, description, disabledTooltipText } = section; + const wasDisabled = usePrevious(isDisabled); + const clearDisabledSectionControls = useCallback(() => { + ensureIsArray(section.controlSetRows).forEach(controlSets => { + controlSets.forEach(controlItem => { + if ( + controlItem && + !React.isValidElement(controlItem) && + controlItem.name && + controlItem.config && + controlItem.name !== 'datasource' + ) { + setControlValue?.(controlItem.name, controlItem.config.default); + } + }); + }); + }, [setControlValue, section]); + + useEffect(() => { + if (wasDisabled === false && isDisabled) { + clearDisabledSectionControls(); + } + }, [wasDisabled, isDisabled]); + + const PanelHeader = () => ( + <span data-test="collapsible-control-panel-header"> + <span + css={(theme: SupersetTheme) => css` + font-size: ${theme.typography.sizes.m}px; + line-height: 1.3; + `} + > + {label} + </span>{' '} + {!isDisabled && description && ( + <Tooltip id={sectionId} title={description}> + <Icons.InfoCircleOutlined css={iconStyles} /> + </Tooltip> + )} + {!isDisabled && hasErrors && ( + <Tooltip + id={`${kebabCase('validation-errors')}-tooltip`} + title={t('This section contains validation errors')} + > + <Icons.InfoCircleOutlined + css={css` + ${iconStyles}; + color: ${errorColor}; + `} + /> + </Tooltip> + )} + {isDisabled && disabledTooltipText && ( + <Tooltip + id={`${kebabCase('disabled-section')}-tooltip`} + title={disabledTooltipText} + > + <Icons.InfoCircleOutlined + data-test="disabled-section-tooltip" + css={css` + ${iconStyles}; + `} + /> + </Tooltip> + )} + </span> + ); + + return ( + <Collapse.Panel + {...restProps} + collapsible={isDisabled ? 'disabled' : undefined} + showArrow={!isDisabled} + 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; + } + + span.label { + display: inline-block; + } + ${!section.label && + ` + .ant-collapse-header { + display: none; + } + `} + `} + header={<PanelHeader />} + key={sectionId} + > + {!isDisabled && + 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; + } + return ( + <ControlRow + key={`controlsetrow-${i}`} + controls={renderedControls} + /> + ); + })} + </Collapse.Panel> + ); +} diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index 7b0ffeb3b6..a638389943 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -49,7 +49,6 @@ import { } from '@superset-ui/chart-controls'; import { useSelector } from 'react-redux'; import { rgba } from 'emotion-rgba'; -import { kebabCase } from 'lodash'; import Collapse from 'src/components/Collapse'; import Tabs from 'src/components/Tabs'; @@ -62,10 +61,10 @@ import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ChartState, ExplorePageState } from 'src/explore/types'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; -import ControlRow from './ControlRow'; import Control from './Control'; import { ExploreAlert } from './ExploreAlert'; import { RunQueryButton } from './RunQueryButton'; +import { ControlPanelSection } from './ControlPanelSection'; export type ControlPanelsContainerProps = { exploreState: ExplorePageState['explore']; @@ -191,21 +190,21 @@ const hasTimeColumn = (datasource: Dataset): boolean => datasource?.columns?.some(c => c.is_dttm); const sectionsToExpand = ( sections: ControlPanelSectionConfig[], - datasource: Dataset, + exploreState: ExplorePageState['explore'], ): string[] => // avoid expanding time section if datasource doesn't include time column - sections.reduce( - (acc, section) => - (section.expanded || !section.label) && - (!isTimeSection(section) || hasTimeColumn(datasource)) - ? [...acc, String(section.label)] - : acc, - [] as string[], - ); + sections.reduce((acc, section) => { + const isDisabled = section?.setDisabled?.call(section, exploreState); + const shouldExpand = isDisabled + ? false + : (section.expanded || !section.label) && + (!isTimeSection(section) || hasTimeColumn(exploreState.datasource)); + return shouldExpand ? [...acc, String(section.label)] : acc; + }, [] as string[]); function getState( vizType: string, - datasource: Dataset, + exploreState: ExplorePageState['explore'], datasourceType: DatasourceType, ) { const querySections: ControlPanelSectionConfig[] = []; @@ -235,11 +234,11 @@ function getState( }); const expandedQuerySections: string[] = sectionsToExpand( querySections, - datasource, + exploreState, ); const expandedCustomizeSections: string[] = sectionsToExpand( customizeSections, - datasource, + exploreState, ); return { expandedQuerySections, @@ -347,7 +346,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { () => getState( props.form_data.viz_type, - props.exploreState.datasource, + props.exploreState, props.datasource_type, ), [ @@ -453,8 +452,9 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { const renderControlPanelSection = ( section: ExpandedControlPanelSectionConfig, ) => { - const { controls } = props; - const { label, description } = section; + const { label, setDisabled } = section; + const { controls, actions } = props; + const isDisabled = setDisabled ? setDisabled.call(section, props) : false; // Section label can be a ReactNode but in some places we want to // have a string ID. Using forced type conversion for now, @@ -486,99 +486,17 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { ? colors.error.base : colors.alert.base; - const PanelHeader = () => ( - <span data-test="collapsible-control-panel-header"> - <span - css={(theme: SupersetTheme) => css` - font-size: ${theme.typography.sizes.m}px; - line-height: 1.3; - `} - > - {label} - </span>{' '} - {description && ( - <Tooltip id={sectionId} title={description}> - <Icons.InfoCircleOutlined css={iconStyles} /> - </Tooltip> - )} - {hasErrors && ( - <Tooltip - id={`${kebabCase('validation-errors')}-tooltip`} - title={t('This section contains validation errors')} - > - <Icons.InfoCircleOutlined - css={css` - ${iconStyles}; - color: ${errorColor}; - `} - /> - </Tooltip> - )} - </span> - ); - return ( - <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; - } - - span.label { - display: inline-block; - } - ${!section.label && - ` - .ant-collapse-header { - display: none; - } - `} - `} - header={<PanelHeader />} + <ControlPanelSection 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; - } - return ( - <ControlRow - key={`controlsetrow-${i}`} - controls={renderedControls} - /> - ); - })} - </Collapse.Panel> + sectionId={sectionId} + actions={actions} + section={section} + hasErrors={hasErrors} + errorColor={errorColor} + isDisabled={isDisabled} + renderControl={renderControl} + /> ); };
