diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index c65b5dfb59..e6d8b5676e 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -111,7 +111,7 @@ describe('VizType control', () => { // should load mathjs for line chart cy.get('script[src*="mathjs"]').should('have.length', 1); cy.get('script').then(nodes => { - expect(nodes.length).to.eq(numScripts); + expect(nodes.length).to.greaterThan(numScripts); }); cy.get('button[data-test="run-query-button"]').click(); diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index 9181a568c7..a0d1f4410c 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { + ensureIsArray, t, styled, getChartControlPanelRegistry, @@ -31,8 +32,10 @@ import { ControlPanelSectionConfig, ControlState, CustomControlItem, + DatasourceMeta, ExpandedControlItem, InfoTooltipWithTrigger, + sections, } from '@superset-ui/chart-controls'; import Collapse from 'src/components/Collapse'; @@ -102,29 +105,129 @@ const ControlPanelsTabs = styled(Tabs)` } `; -export class ControlPanelsContainer extends React.Component { +type ControlPanelsContainerState = { + expandedQuerySections: string[]; + expandedCustomizeSections: string[]; + querySections: ControlPanelSectionConfig[]; + customizeSections: ControlPanelSectionConfig[]; +}; + +const isTimeSection = (section: ControlPanelSectionConfig): boolean => + !!section.label && + (sections.legacyRegularTime.label === section.label || + sections.legacyTimeseriesTime.label === section.label); + +const hasTimeColumn = (datasource: DatasourceMeta): boolean => + datasource?.columns?.some(c => c.is_dttm) || + datasource.type === DatasourceType.Druid; + +const sectionsToExpand = ( + sections: ControlPanelSectionConfig[], + datasource: DatasourceMeta, +): string[] => + // avoid expanding time section if datasource doesn't include time column + sections.reduce( + (acc, section) => + section.expanded && (!isTimeSection(section) || hasTimeColumn(datasource)) + ? [...acc, String(section.label)] + : acc, + [] as string[], + ); + +function getState( + props: ControlPanelsContainerProps, +): ControlPanelsContainerState { + const { + exploreState: { datasource }, + } = props; + + const querySections: ControlPanelSectionConfig[] = []; + const customizeSections: ControlPanelSectionConfig[] = []; + + getSectionsToRender(props.form_data.viz_type, props.datasource_type).forEach( + section => { + // if at least one control in the section is not `renderTrigger` + // or asks to be displayed at the Data tab + if ( + section.tabOverride === 'data' || + section.controlSetRows.some(rows => + rows.some( + control => + control && + typeof control === 'object' && + 'config' in control && + control.config && + (!control.config.renderTrigger || + control.config.tabOverride === 'data'), + ), + ) + ) { + querySections.push(section); + } else { + customizeSections.push(section); + } + }, + ); + const expandedQuerySections: string[] = sectionsToExpand( + querySections, + datasource, + ); + const expandedCustomizeSections: string[] = sectionsToExpand( + customizeSections, + datasource, + ); + return { + expandedQuerySections, + expandedCustomizeSections, + querySections, + customizeSections, + }; +} + +export class ControlPanelsContainer extends React.Component< + ControlPanelsContainerProps, + ControlPanelsContainerState +> { // trigger updates to the component when async plugins load static contextType = PluginContext; constructor(props: ControlPanelsContainerProps) { super(props); + this.state = { + expandedQuerySections: [], + expandedCustomizeSections: [], + querySections: [], + customizeSections: [], + }; this.renderControl = this.renderControl.bind(this); this.renderControlPanelSection = this.renderControlPanelSection.bind(this); } - sectionsToRender(): ExpandedControlPanelSectionConfig[] { - return getSectionsToRender( - this.props.form_data.viz_type, - this.props.datasource_type, - ); + static getDerivedStateFromProps( + props: ControlPanelsContainerProps, + state: ControlPanelsContainerState, + ): ControlPanelsContainerState { + // only update the sections, not the expanded/collapsed state + const newState = getState(props); + return { + ...state, + customizeSections: newState.customizeSections, + querySections: newState.querySections, + }; } - sectionsToExpand(sections: ControlPanelSectionConfig[]) { - return sections.reduce( - (acc, section) => - section.expanded ? [...acc, String(section.label)] : acc, - [] as string[], - ); + componentDidUpdate(prevProps: ControlPanelsContainerProps) { + if ( + this.props.form_data.datasource !== prevProps.form_data.datasource || + this.props.form_data.viz_type !== prevProps.form_data.viz_type + ) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState(getState(this.props)); + } + } + + componentDidMount() { + this.setState(getState(this.props)); } renderControl({ name, config }: CustomControlItem) { @@ -260,36 +363,7 @@ export class ControlPanelsContainer extends React.Component; } - const querySectionsToRender: ExpandedControlPanelSectionConfig[] = []; - const displaySectionsToRender: ExpandedControlPanelSectionConfig[] = []; - this.sectionsToRender().forEach(section => { - // if at least one control in the section is not `renderTrigger` - // or asks to be displayed at the Data tab - if ( - section.tabOverride === 'data' || - section.controlSetRows.some(rows => - rows.some( - control => - control && - typeof control === 'object' && - 'config' in control && - control.config && - (!control.config.renderTrigger || - control.config.tabOverride === 'data'), - ), - ) - ) { - querySectionsToRender.push(section); - } else { - displaySectionsToRender.push(section); - } - }); - - const showCustomizeTab = displaySectionsToRender.length > 0; - const expandedQuerySections = this.sectionsToExpand(querySectionsToRender); - const expandedCustomSections = this.sectionsToExpand( - displaySectionsToRender, - ); + const showCustomizeTab = this.state.customizeSections.length > 0; return ( { + this.setState({ + expandedQuerySections: ensureIsArray(selection), + }); + }} ghost > - {querySectionsToRender.map(this.renderControlPanelSection)} + {this.state.querySections.map(this.renderControlPanelSection)} {showCustomizeTab && ( { + this.setState({ + expandedCustomizeSections: ensureIsArray(selection), + }); + }} ghost > - {displaySectionsToRender.map(this.renderControlPanelSection)} + {this.state.customizeSections.map( + this.renderControlPanelSection, + )} )}