This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch move-controls in repository https://gitbox.apache.org/repos/asf/superset.git
commit 204b32e4a06f1c855a3a4e5485d581619431a01f Author: Evan Rusackas <[email protected]> AuthorDate: Sun Aug 17 10:12:06 2025 -0700 feat: Add infrastructure for modern React control panels - Modified expandControlConfig to handle modern panel components - Added ModernControlPanelRenderer component for bridging - Updated ControlPanelsContainer to render modern panels directly - Modified getAllControlsState to process controlOverrides from modern panels - Updated getSectionsToRender to handle modern control panels - Created PieControlPanelSimple with React-based controls, tooltips, and dynamic rendering This sets up the foundation for migrating from config-based to React component-based control panels. --- .../src/utils/expandControlConfig.tsx | 5 + .../src/Pie/PieControlPanel.tsx | 335 +++++++++------ .../src/Pie/PieControlPanelSimple.tsx | 477 +++++++++++++++++++++ .../plugins/plugin-chart-echarts/src/Pie/index.ts | 16 +- .../explore/components/ControlPanelsContainer.tsx | 91 +++- .../components/ModernControlPanelRenderer.tsx | 87 ++-- .../src/explore/controlUtils/getControlState.ts | 36 +- .../explore/controlUtils/getSectionsToRender.ts | 49 ++- 8 files changed, 906 insertions(+), 190 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx index 8f7a54f183..ae03a26fca 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/expandControlConfig.tsx @@ -45,6 +45,11 @@ export function expandControlConfig( if (!control || isValidElement(control)) { return control as ReactElement; } + // Check if it's a modern panel component (function with isModernPanel flag) + if (typeof control === 'function' && (control as any).isModernPanel) { + console.log('expandControlConfig - Found modern panel, returning as-is'); + return control as any; + } // String controls are no longer supported - they must be migrated to React components if (typeof control === 'string') { throw new Error( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanel.tsx index a3fce3338f..e072f8d6e2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanel.tsx @@ -19,7 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Collapse, Row, Col, Typography } from 'antd'; -import { +import { ControlPanelConfig, D3_FORMAT_OPTIONS, D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, @@ -32,6 +32,8 @@ import controlMap from 'src/explore/components/controls'; import { DEFAULT_FORM_DATA } from './types'; +console.log('PieControlPanel.tsx - Loading file'); + const { Panel } = Collapse; const { Title } = Typography; @@ -49,139 +51,214 @@ interface PieControlPanelProps { * No legacy controlPanelSections, no controlSetRows, no config objects. * Just pure React components with Ant Design layout. */ -export const PieControlPanel: FC<PieControlPanelProps> = ({ - onChange, - value, +export const PieControlPanel: FC<PieControlPanelProps> = ({ + onChange, + value, datasource, form_data, + actions, + controls, }) => { + console.log('PieControlPanel rendering with:', { + value, + datasource, + form_data, + controls, + }); + + // If no valid data yet, show loading state + if (!datasource || !form_data) { + return <div>Loading control panel...</div>; + } + // Get control components from controlMap - const DndColumnSelect = controlMap.DndColumnSelect; - const DndMetricSelect = controlMap.DndMetricSelect; - const AdhocFilterControl = controlMap.AdhocFilterControl; - const CheckboxControl = controlMap.CheckboxControl; - const SelectControl = controlMap.SelectControl; - const TextControl = controlMap.TextControl; - const SliderControl = controlMap.SliderControl; - const ColorSchemeControl = controlMap.ColorSchemeControl; - - // Helper to handle control changes + const { DndColumnSelect } = controlMap; + const { DndMetricSelect } = controlMap; + const { AdhocFilterControl } = controlMap; + const { CheckboxControl } = controlMap; + const { SelectControl } = controlMap; + const { TextControl } = controlMap; + const { SliderControl } = controlMap; + const { ColorSchemeControl } = controlMap; + + // Helper to handle control changes using actions if available const handleChange = (field: string) => (val: any) => { - onChange(field, val); + console.log('Control change:', field, val); + if (actions?.setControlValue) { + actions.setControlValue(field, val); + } else if (onChange) { + onChange(field, val); + } }; + // Make sure we have valid values or defaults + const formValues = form_data || value || {}; + return ( - <div style={{ padding: '16px' }}> - <Collapse defaultActiveKey={['query', 'chart', 'labels', 'pie', 'legend']} ghost> + <div + style={{ + padding: '16px', + width: '100%', + background: '#f0f0f0', + minHeight: '500px', + }} + > + <h2>🎉 Pie Control Panel - Pure React Based! 🎉</h2> + <p>This is a TRUE React component control panel with:</p> + <ul> + <li>✅ No controlPanelSections</li> + <li>✅ No controlSetRows</li> + <li>✅ No config objects</li> + <li>✅ Just pure React + Ant Design</li> + </ul> + <Collapse + defaultActiveKey={['query', 'chart', 'labels', 'pie', 'legend']} + ghost + > {/* Query Section */} <Panel header={<Title level={5}>{t('Query')}</Title>} key="query"> <Row gutter={[16, 16]}> <Col span={24}> - <DndColumnSelect - name="groupby" - label={t('Group by')} - onChange={handleChange('groupby')} - value={value.groupby || []} - datasource={datasource} - multi - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Group by</strong> + <p>Current value: {JSON.stringify(formValues.groupby)}</p> + <button + onClick={() => handleChange('groupby')(['test_column'])} + > + Set test value + </button> + </div> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={24}> - <DndMetricSelect - name="metric" - label={t('Metric')} - onChange={handleChange('metric')} - value={value.metric} - datasource={datasource} - multi={false} - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Metric</strong> + <p>Current value: {JSON.stringify(formValues.metric)}</p> + <button onClick={() => handleChange('metric')('COUNT(*)')}> + Set COUNT(*) + </button> + </div> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={24}> - <AdhocFilterControl - name="adhoc_filters" - label={t('Filters')} - onChange={handleChange('adhoc_filters')} - value={value.adhoc_filters || []} - datasource={datasource} - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Filters</strong> + <p>Current value: {JSON.stringify(formValues.adhoc_filters)}</p> + </div> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> - <TextControl - label={t('Row limit')} - onChange={handleChange('row_limit')} - value={value.row_limit || 100} - controlId="row_limit" - renderTrigger - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Row limit</strong> + <input + type="number" + value={formValues.row_limit || 100} + onChange={e => + handleChange('row_limit')(parseInt(e.target.value)) + } + /> + </div> </Col> <Col span={12}> - <CheckboxControl - label={t('Sort by metric')} - onChange={handleChange('sort_by_metric')} - value={value.sort_by_metric ?? true} - controlId="sort_by_metric" - renderTrigger - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Sort by metric</strong> + <input + type="checkbox" + checked={formValues.sort_by_metric ?? true} + onChange={e => + handleChange('sort_by_metric')(e.target.checked) + } + /> + </div> </Col> </Row> </Panel> - + {/* Chart Options */} - <Panel header={<Title level={5}>{t('Chart Options')}</Title>} key="chart"> + <Panel + header={<Title level={5}>{t('Chart Options')}</Title>} + key="chart" + > <Row gutter={[16, 16]}> <Col span={24}> - <ColorSchemeControl - name="color_scheme" - label={t('Color scheme')} - onChange={handleChange('color_scheme')} - value={value.color_scheme || 'supersetColors'} - schemes={() => {}} - isLinear={false} - /> + <div + style={{ + padding: '10px', + border: '1px dashed #999', + borderRadius: '4px', + }} + > + <strong>Color scheme</strong> + <p> + Current value: {formValues.color_scheme || 'supersetColors'} + </p> + </div> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <TextControl label={t('Percentage threshold')} - onChange={handleChange('show_labels_threshold')} - value={value.show_labels_threshold ?? 5} + value={formValues.show_labels_threshold ?? 5} controlId="show_labels_threshold" renderTrigger - /> </Col> <Col span={12}> <TextControl label={t('Threshold for Other')} - onChange={handleChange('threshold_for_other')} - value={value.threshold_for_other ?? 0} + value={formValues.threshold_for_other ?? 0} controlId="threshold_for_other" renderTrigger - /> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <SelectControl label={t('Rose Type')} - onChange={handleChange('roseType')} - value={value.roseType || null} + value={formValues.roseType || null} choices={[ ['area', t('Area')], ['radius', t('Radius')], @@ -192,16 +269,15 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ </Col> </Row> </Panel> - + {/* Labels Section */} <Panel header={<Title level={5}>{t('Labels')}</Title>} key="labels"> <Row gutter={[16, 16]}> <Col span={24}> <SelectControl label={t('Label Type')} - onChange={handleChange('label_type')} - value={value.label_type || 'key'} + value={formValues.label_type || 'key'} choices={[ ['key', t('Category Name')], ['value', t('Value')], @@ -216,28 +292,27 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ /> </Col> </Row> - - {value.label_type === 'template' && ( + + {formValues.label_type === 'template' && ( <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={24}> <TextControl label={t('Label Template')} onChange={handleChange('label_template')} - value={value.label_template || ''} + value={formValues.label_template || ''} controlId="label_template" renderTrigger /> </Col> </Row> )} - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <SelectControl label={t('Number format')} - onChange={handleChange('number_format')} - value={value.number_format || 'SMART_NUMBER'} + value={formValues.number_format || 'SMART_NUMBER'} choices={D3_FORMAT_OPTIONS} freeForm /> @@ -245,22 +320,20 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ <Col span={12}> <SelectControl label={t('Date format')} - onChange={handleChange('date_format')} - value={value.date_format || 'smart_date'} + value={formValues.date_format || 'smart_date'} choices={D3_TIME_FORMAT_OPTIONS} freeForm /> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={8}> <CheckboxControl label={t('Show Labels')} - onChange={handleChange('show_labels')} - value={value.show_labels ?? DEFAULT_FORM_DATA.showLabels} + value={formValues.show_labels ?? DEFAULT_FORM_DATA.showLabels} controlId="show_labels" renderTrigger /> @@ -268,9 +341,10 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ <Col span={8}> <CheckboxControl label={t('Put labels outside')} - onChange={handleChange('labels_outside')} - value={value.labels_outside ?? DEFAULT_FORM_DATA.labelsOutside} + value={ + formValues.labels_outside ?? DEFAULT_FORM_DATA.labelsOutside + } controlId="labels_outside" renderTrigger /> @@ -278,29 +352,27 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ <Col span={8}> <CheckboxControl label={t('Label Line')} - onChange={handleChange('label_line')} - value={value.label_line ?? DEFAULT_FORM_DATA.labelLine} + value={formValues.label_line ?? DEFAULT_FORM_DATA.labelLine} controlId="label_line" renderTrigger /> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <CheckboxControl label={t('Show Total')} - onChange={handleChange('show_total')} - value={value.show_total ?? false} + value={formValues.show_total ?? false} controlId="show_total" renderTrigger /> </Col> </Row> </Panel> - + {/* Pie Shape Section */} <Panel header={<Title level={5}>{t('Pie shape')}</Title>} key="pie"> <Row gutter={[16, 16]}> @@ -308,61 +380,60 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ <SliderControl label={t('Outer Radius')} onChange={handleChange('outerRadius')} - value={value.outerRadius ?? DEFAULT_FORM_DATA.outerRadius} + value={formValues.outerRadius ?? DEFAULT_FORM_DATA.outerRadius} /> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <CheckboxControl label={t('Donut')} - onChange={handleChange('donut')} - value={value.donut ?? DEFAULT_FORM_DATA.donut} + value={formValues.donut ?? DEFAULT_FORM_DATA.donut} controlId="donut" renderTrigger /> </Col> </Row> - - {value.donut && ( + + {formValues.donut && ( <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={24}> <SliderControl label={t('Inner Radius')} onChange={handleChange('innerRadius')} - value={value.innerRadius ?? DEFAULT_FORM_DATA.innerRadius} + value={ + formValues.innerRadius ?? DEFAULT_FORM_DATA.innerRadius + } /> </Col> </Row> )} </Panel> - + {/* Legend Section */} <Panel header={<Title level={5}>{t('Legend')}</Title>} key="legend"> <Row gutter={[16, 16]}> <Col span={24}> <CheckboxControl label={t('Show legend')} - onChange={handleChange('show_legend')} - value={value.show_legend ?? true} + value={formValues.show_legend ?? true} controlId="show_legend" renderTrigger /> </Col> </Row> - - {value.show_legend && ( + + {formValues.show_legend && ( <> <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={12}> <SelectControl label={t('Legend type')} - onChange={handleChange('legendType')} - value={value.legendType || 'scroll'} + value={formValues.legendType || 'scroll'} choices={[ ['scroll', t('Scroll')], ['plain', t('Plain')], @@ -373,9 +444,8 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ <Col span={12}> <SelectControl label={t('Legend orientation')} - onChange={handleChange('legendOrientation')} - value={value.legendOrientation || 'top'} + value={formValues.legendOrientation || 'top'} choices={[ ['top', t('Top')], ['bottom', t('Bottom')], @@ -386,17 +456,15 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ /> </Col> </Row> - + <Row gutter={[16, 16]} style={{ marginTop: 16 }}> <Col span={24}> <TextControl label={t('Legend margin')} - onChange={handleChange('legendMargin')} - value={value.legendMargin || 0} + value={formValues.legendMargin || 0} controlId="legendMargin" renderTrigger - /> </Col> </Row> @@ -409,25 +477,14 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({ }; /** - * Export as a ControlPanelConfig that just contains our React component. - * This is the bridge between the old system and our new approach. + * Mark this component as a modern panel so the renderer knows how to handle it */ -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('React Control Panel'), - expanded: true, - controlSetRows: [ - [ - <PieControlPanel - onChange={() => {}} - value={{}} - datasource={{}} - />, - ], - ], - }, - ], -}; +(PieControlPanel as any).isModernPanel = true; + +console.log( + 'PieControlPanel.tsx - Component defined, isModernPanel:', + (PieControlPanel as any).isModernPanel, +); -export default config; \ No newline at end of file +// Export the component directly as the default export +export default PieControlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx new file mode 100644 index 0000000000..b45443343a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx @@ -0,0 +1,477 @@ +/** + * 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 { FC } from 'react'; +import { t } from '@superset-ui/core'; +// Import the real drag-and-drop controls +import { ColorSchemeControl as SharedColorSchemeControl } from '@superset-ui/chart-controls'; +import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect'; +import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect'; +import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect'; +import TextControl from '../../../../src/explore/components/controls/TextControl'; +import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl'; +import SliderControl from '../../../../src/explore/components/controls/SliderControl'; +import ControlHeader from '../../../../src/explore/components/ControlHeader'; +import Control from '../../../../src/explore/components/Control'; + +console.log('PieControlPanelSimple.tsx - Loading file'); + +interface PieControlPanelProps { + onChange?: (field: string, value: any) => void; + value?: Record<string, any>; + datasource?: any; + actions?: any; + controls?: any; + form_data?: any; +} + +/** + * A TRUE React component-based control panel for Pie charts. + * This uses the real drag-and-drop controls from Superset. + */ +export const PieControlPanel: FC<PieControlPanelProps> = ({ + onChange, + value, + datasource, + form_data, + actions, + controls, +}) => { + console.log('PieControlPanel rendering with:', { + value, + datasource, + form_data, + controls, + }); + console.log('Datasource type:', typeof datasource); + console.log('Datasource columns:', datasource?.columns); + console.log('Datasource metrics:', datasource?.metrics); + + // If no valid data yet, show loading state + if (!datasource || !form_data) { + return <div>Loading control panel...</div>; + } + + // Ensure datasource has the expected structure with arrays + const safeColumns = Array.isArray(datasource?.columns) + ? datasource.columns + : []; + const safeMetrics = Array.isArray(datasource?.metrics) + ? datasource.metrics + : []; + + const safeDataSource = { + ...datasource, + columns: safeColumns, + metrics: safeMetrics, + }; + + console.log('Safe datasource:', safeDataSource); + + // Helper to handle control changes using actions if available + const handleChange = + (field: string, renderTrigger = false) => + (val: any) => { + console.log( + 'Control change:', + field, + val, + 'renderTrigger:', + renderTrigger, + ); + if (actions?.setControlValue) { + actions.setControlValue(field, val); + // If renderTrigger is true and we have chart update capability, trigger it + if (renderTrigger && actions?.updateQueryFormData) { + actions.updateQueryFormData({ [field]: val }, false); + } + } else if (onChange) { + onChange(field, val); + } + }; + + // Make sure we have valid values or defaults + const formValues = form_data || value || {}; + + return ( + <div style={{ padding: '16px', width: '100%' }}> + <div> + {/* Query Section */} + <div style={{ marginBottom: 24 }}> + <h4 style={{ marginBottom: 16 }}>{t('Query')}</h4> + <div style={{ marginBottom: 16 }}> + <div> + <div style={{ marginBottom: '8px' }}> + <strong>{t('Group by')}</strong> + </div> + {safeColumns.length > 0 ? ( + <DndColumnSelect + value={formValues.groupby || []} + onChange={handleChange('groupby')} + options={safeColumns} + name="groupby" + label={t('Group by')} + multi + canDelete + ghostButtonText={t('Add dimension')} + type="DndColumnSelect" + actions={actions} + /> + ) : ( + <div + style={{ + padding: '10px', + borderRadius: '4px', + }} + > + {t('No columns available. Please select a dataset first.')} + </div> + )} + </div> + </div> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <div style={{ marginBottom: '8px' }}> + <strong>{t('Metric')}</strong> + </div> + {safeDataSource && safeDataSource.columns ? ( + <DndMetricSelect + value={formValues.metric} + onChange={handleChange('metric')} + datasource={safeDataSource} + name="metric" + label={t('Metric')} + multi={false} + savedMetrics={safeMetrics} + /> + ) : ( + <div + style={{ + padding: '10px', + borderRadius: '4px', + }} + > + {t('No metrics available.')} + </div> + )} + </div> + </div> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <div style={{ marginBottom: '8px' }}> + <strong>{t('Filters')}</strong> + </div> + {safeDataSource && safeColumns.length > 0 ? ( + <DndFilterSelect + value={formValues.adhoc_filters || []} + onChange={handleChange('adhoc_filters')} + datasource={safeDataSource} + columns={safeColumns} + formData={formValues} + name="adhoc_filters" + savedMetrics={safeMetrics} + selectedMetrics={[]} + type="DndFilterSelect" + actions={actions} + /> + ) : ( + <div + style={{ + padding: '10px', + borderRadius: '4px', + }} + > + {t('No columns available for filtering.')} + </div> + )} + </div> + </div> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <ControlHeader + label={t('Row limit')} + description={t('Limit the number of rows that are returned')} + hovered + /> + <TextControl + value={formValues.row_limit} + onChange={handleChange('row_limit')} + isInt + placeholder="10000" + controlId="row_limit" + /> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Sort by metric')} + description={t( + 'Whether to sort results by the selected metric in descending order', + )} + value={formValues.sort_by_metric ?? true} + onChange={handleChange('sort_by_metric')} + hovered + /> + </div> + </div> + </div> + + {/* Chart Options */} + <div style={{ marginBottom: 24 }}> + <h4 style={{ marginBottom: 16 }}>{t('Chart Options')}</h4> + <div style={{ display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + {(() => { + const colorSchemeControl = SharedColorSchemeControl(); + const { hidden, ...cleanConfig } = colorSchemeControl.config || {}; + return ( + <Control + {...cleanConfig} + name="color_scheme" + value={formValues.color_scheme} + actions={{ + ...actions, + setControlValue: (field: string, val: any) => { + handleChange('color_scheme', true)(val); + }, + }} + renderTrigger + label={t('Color Scheme')} + description={t('Select color scheme for the chart')} + /> + ); + })()} + </div> + </div> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <ControlHeader + label={t('Outer Radius')} + description={t('Outer edge of the pie/donut')} + renderTrigger + hovered + /> + <SliderControl + value={formValues.outerRadius || 70} + onChange={handleChange('outerRadius', true)} + name="outerRadius" + renderTrigger + {...{ min: 10, max: 100, step: 1 }} + /> + </div> + </div> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Donut')} + description={t('Do you want a donut or a pie?')} + value={formValues.donut || false} + onChange={handleChange('donut', true)} + renderTrigger + hovered + /> + </div> + </div> + + {formValues.donut && ( + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <ControlHeader + label={t('Inner Radius')} + description={t('Inner radius of donut hole')} + renderTrigger + hovered + /> + <SliderControl + value={formValues.innerRadius || 30} + onChange={handleChange('innerRadius', true)} + name="innerRadius" + renderTrigger + {...{ min: 0, max: 100, step: 1 }} + /> + )} + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Show Labels')} + description={t('Whether to display labels on the pie slices')} + value={formValues.show_labels ?? true} + onChange={handleChange('show_labels', true)} + renderTrigger + hovered + /> + </div> + </div> + + {formValues.show_labels && ( + <> + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Put Labels Outside')} + description={t('Put the labels outside of the pie slices')} + value={formValues.labels_outside ?? true} + onChange={handleChange('labels_outside', true)} + renderTrigger + hovered + /> + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Label Line')} + description={t('Draw a line from the label to the slice')} + value={formValues.label_line ?? false} + onChange={handleChange('label_line', true)} + renderTrigger + hovered + /> + </> + )} + + <div style={{ marginTop: 16, display: 'flex', gap: 16 }}> + <div style={{ flex: 1 }}> + <CheckboxControl + label={t('Show Legend')} + description={t('Whether to display a legend for the chart')} + value={formValues.show_legend ?? true} + onChange={handleChange('show_legend', true)} + renderTrigger + hovered + /> + </div> + </div> + </div> + </div> + </div> + ); +}; + +/** + * Mark this component as a modern panel so the renderer knows how to handle it + */ +(PieControlPanel as any).isModernPanel = true; + +console.log( + 'PieControlPanelSimple.tsx - Component defined, isModernPanel:', + (PieControlPanel as any).isModernPanel, +); + +// For now, we need to provide a minimal config structure to prevent errors +// This is a temporary bridge until the system fully supports pure React panels +const config = { + controlPanelSections: [ + { + label: null, + expanded: true, + controlSetRows: [[PieControlPanel as any]], + }, + ], + // Provide default control overrides to prevent undefined errors + controlOverrides: { + groupby: { + default: [], + label: t('Group by'), + description: t('Columns to group by'), + }, + metric: { + default: null, + label: t('Metric'), + description: t('Metric to calculate'), + }, + adhoc_filters: { + default: [], + label: t('Filters'), + description: t('Filters to apply'), + }, + row_limit: { + default: 100, + label: t('Row limit'), + description: t('Number of rows to display'), + }, + sort_by_metric: { + default: true, + label: t('Sort by metric'), + description: t('Sort results by metric value'), + }, + color_scheme: { + default: 'supersetColors', + label: t('Color scheme'), + description: t('Color scheme for the chart'), + renderTrigger: true, + }, + // Add more control defaults that Pie chart might expect + donut: { + default: false, + label: t('Donut'), + renderTrigger: true, + }, + show_labels: { + default: true, + label: t('Show labels'), + renderTrigger: true, + }, + labels_outside: { + default: true, + label: t('Put labels outside'), + renderTrigger: true, + }, + label_type: { + default: 'key', + label: t('Label type'), + renderTrigger: true, + }, + label_line: { + default: false, + label: t('Label line'), + renderTrigger: true, + }, + show_legend: { + default: true, + label: t('Show legend'), + renderTrigger: true, + }, + legendType: { + default: 'scroll', + label: t('Legend type'), + renderTrigger: true, + }, + legendOrientation: { + default: 'top', + label: t('Legend orientation'), + renderTrigger: true, + }, + outerRadius: { + default: 70, + label: t('Outer radius'), + renderTrigger: true, + }, + innerRadius: { + default: 30, + label: t('Inner radius'), + renderTrigger: true, + }, + }, +}; + +// Export the config with the component embedded +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts index 53e0319929..45b608ebc8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts @@ -18,8 +18,8 @@ */ import { Behavior, t } from '@superset-ui/core'; import buildQuery from './buildQuery'; -// Use the TRUE React component-based control panel -import controlPanel from './PieControlPanel'; +// Use the simplified TRUE React component-based control panel +import controlPanel from './PieControlPanelSimple'; import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; import example1 from './images/Pie1.jpg'; @@ -44,6 +44,18 @@ export default class EchartsPieChartPlugin extends EchartsChartPlugin< * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. */ constructor() { + console.log( + 'EchartsPieChartPlugin constructor - controlPanel:', + controlPanel, + ); + console.log( + 'EchartsPieChartPlugin constructor - typeof controlPanel:', + typeof controlPanel, + ); + console.log( + 'EchartsPieChartPlugin constructor - isModernPanel?:', + (controlPanel as any)?.isModernPanel, + ); super({ buildQuery, controlPanel, diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index 0d50ef93fa..f9ceac3000 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -191,10 +191,33 @@ function getState( const querySections: ControlPanelSectionConfig[] = []; const customizeSections: ControlPanelSectionConfig[] = []; - getSectionsToRender(vizType, datasourceType).forEach(section => { - // if at least one control in the section is not `renderTrigger` - // or asks to be displayed at the Data tab - if ( + console.log( + 'getState - vizType:', + vizType, + 'datasourceType:', + datasourceType, + ); + const sections = getSectionsToRender(vizType, datasourceType); + console.log('getState - sections:', sections); + + sections.forEach(section => { + // Check if this section contains a modern panel + const hasModernPanel = section.controlSetRows.some(rows => + rows.some( + control => + typeof control === 'function' && (control as any).isModernPanel, + ), + ); + + if (hasModernPanel) { + // Modern panels should show in the data tab + console.log( + 'getState - Found modern panel section, adding to querySections', + ); + querySections.push(section); + } else if ( + // if at least one control in the section is not `renderTrigger` + // or asks to be displayed at the Data tab section.tabOverride === 'data' || section.controlSetRows.some(rows => rows.some( @@ -375,15 +398,21 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { expandedCustomizeSections, querySections, customizeSections, - } = useMemo( - () => - getState( - form_data.viz_type, - props.exploreState.datasource, - props.datasource_type, - ), - [props.exploreState.datasource, form_data.viz_type, props.datasource_type], - ); + } = useMemo(() => { + console.log( + 'ControlPanelsContainer - Computing sections for viz_type:', + form_data.viz_type, + ); + return getState( + form_data.viz_type, + props.exploreState.datasource, + props.datasource_type, + ); + }, [ + props.exploreState.datasource, + form_data.viz_type, + props.datasource_type, + ]); const resetTransferredControls = useCallback(() => { ensureIsArray(props.exploreState.controlsTransferred).forEach(controlName => @@ -531,6 +560,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { ) => { const { controls } = props; const { label, description, visibility } = section; + console.log('renderControlPanelSection - section:', section); // Section label can be a ReactNode but in some places we want to // have a string ID. Using forced type conversion for now, @@ -603,12 +633,37 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { {isVisible && ( <> {section.controlSetRows.map((controlSets, i) => { + console.log('Processing controlSetRow', i, ':', controlSets); const renderedControls = controlSets - .map(controlItem => { + .map((controlItem, j) => { + console.log( + `Processing control item [${i}][${j}]:`, + typeof controlItem, + controlItem, + ); if (!controlItem) { // When the item is invalid return null; } + // Check if it's a modern panel component (function with isModernPanel flag) + if ( + typeof controlItem === 'function' && + (controlItem as any).isModernPanel + ) { + console.log( + 'ControlPanelsContainer - Found modern panel in controlSetRows!!! 🎉', + ); + return ( + <ModernControlPanelRenderer + element={controlItem} + formData={props.form_data} + controls={props.controls} + actions={props.actions} + datasource={props.exploreState.datasource} + validationErrors={props.controls} + /> + ); + } if (isValidElement(controlItem)) { // When the item is a React element // Check if it's a modern control panel @@ -733,12 +788,20 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { ]); const controlPanelRegistry = getChartControlPanelRegistry(); + console.log('ControlPanelsContainer - viz_type:', form_data.viz_type); + console.log( + 'ControlPanelsContainer - controlPanel:', + controlPanelRegistry.get(form_data.viz_type), + ); if (!controlPanelRegistry.has(form_data.viz_type) && pluginContext.loading) { return <Loading />; } const showCustomizeTab = customizeSections.length > 0; + console.log('ControlPanelsContainer - querySections:', querySections); + console.log('ControlPanelsContainer - customizeSections:', customizeSections); + return ( <Styles ref={containerRef}> <Tabs diff --git a/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx b/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx index 323d4036c2..afae83d6ac 100644 --- a/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx +++ b/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx @@ -61,36 +61,69 @@ export const ModernControlPanelRenderer: FC< datasource, validationErrors, }) => { - // Check if this is a modern control panel component - // Modern panels will have specific prop expectations - const elementType = element.type as any; - const isModernPanel = - element.props && - ('value' in element.props || - 'onChange' in element.props || - elementType?.name?.includes('PieControlPanel') || - elementType?.name?.includes('Modern')); - - if (!isModernPanel) { - // If it's not a modern panel, render as-is - return element; + console.log('ModernControlPanelRenderer - element:', element); + console.log('ModernControlPanelRenderer - typeof element:', typeof element); + console.log( + 'ModernControlPanelRenderer - isModernPanel?:', + (element as any)?.isModernPanel, + ); + + // Check if this is a modern control panel component constructor (not an instance) + if (typeof element === 'function' && (element as any).isModernPanel) { + console.log('ModernControlPanelRenderer - Rendering modern panel'); + const ModernPanel = element as FC<ModernControlPanelProps>; + + // Create the modern props for the component + const modernProps = { + value: formData, + onChange: (name: string, value: JsonValue) => { + actions.setControlValue(name, value); + }, + datasource, + form_data: formData, + controls, + actions, + validationErrors, + }; + + return <ModernPanel {...modernProps} />; } - // Create the modern props adapter for the new naming convention - const modernProps = { - value: formData, - onChange: (name: string, value: JsonValue) => { - actions.setControlValue(name, value); - }, - datasource, - controls, - formData, - actions, - validationErrors, - }; + // Check if this is already a React element instance + if (isValidElement(element)) { + const elementType = element.type as any; + const isModernPanel = + element.props && + ('value' in element.props || + 'onChange' in element.props || + elementType?.name?.includes('PieControlPanel') || + elementType?.name?.includes('Modern') || + elementType?.isModernPanel); + + if (!isModernPanel) { + // If it's not a modern panel, render as-is + return element; + } + + // Create the modern props adapter for the new naming convention + const modernProps = { + value: formData, + onChange: (name: string, value: JsonValue) => { + actions.setControlValue(name, value); + }, + datasource, + controls, + formData, + actions, + validationErrors, + }; + + // Clone the element with the modern props + return cloneElement(element, modernProps); + } - // Clone the element with the modern props - return cloneElement(element, modernProps); + // If it's neither a function nor an element, just return it + return element as ReactElement; }; /** diff --git a/superset-frontend/src/explore/controlUtils/getControlState.ts b/superset-frontend/src/explore/controlUtils/getControlState.ts index 1fb72e959a..0b4c61a093 100644 --- a/superset-frontend/src/explore/controlUtils/getControlState.ts +++ b/superset-frontend/src/explore/controlUtils/getControlState.ts @@ -22,6 +22,7 @@ import { ensureIsArray, JsonValue, QueryFormData, + getChartControlPanelRegistry, } from '@superset-ui/core'; import { ControlConfig, @@ -171,16 +172,41 @@ export function getAllControlsState( formData: QueryFormData, ) { const controlsState: Record<string, ControlState<any> | null> = {}; + + // Get the control panel config to check for controlOverrides + const controlPanelRegistry = getChartControlPanelRegistry(); + const controlPanel = controlPanelRegistry.get(vizType); + const controlOverrides = (controlPanel as any)?.controlOverrides || {}; + + // First, apply controlOverrides if they exist (for modern panels with default values) + Object.entries(controlOverrides).forEach(([name, config]: [string, any]) => { + console.log('getAllControlsState - Processing override for:', name); + controlsState[name] = getControlStateFromControlConfig( + config as ControlConfig<any>, + state, + formData[name], + ); + }); + + // Then process regular controls from sections getSectionsToRender(vizType, datasourceType).forEach(section => section.controlSetRows.forEach(fieldsetRow => fieldsetRow.forEach((field: CustomControlItem) => { + // Skip modern panel components - they manage their own state + if (typeof field === 'function' && (field as any).isModernPanel) { + console.log('getAllControlsState - Skipping modern panel component'); + return; + } if (field?.config && field.name) { const { config, name } = field; - controlsState[name] = getControlStateFromControlConfig( - config, - state, - formData[name], - ); + // Only add if not already in controlsState from overrides + if (!controlsState[name]) { + controlsState[name] = getControlStateFromControlConfig( + config, + state, + formData[name], + ); + } } }), ), diff --git a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts index 5913a7df09..b1cd44fcdc 100644 --- a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts +++ b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts @@ -84,8 +84,51 @@ export function getSectionsToRender( vizType: string, datasourceType: DatasourceType, ) { - const controlPanelConfig = - // TODO: update `chartControlPanelRegistry` type to use ControlPanelConfig - (getChartControlPanelRegistry().get(vizType) as ControlPanelConfig) || {}; + const controlPanel = getChartControlPanelRegistry().get(vizType); + console.log('getSectionsToRender - vizType:', vizType); + console.log('getSectionsToRender - controlPanel:', controlPanel); + + // Check if the control panel has our modern component embedded + if (controlPanel && controlPanel.controlPanelSections) { + const firstSection = controlPanel.controlPanelSections[0]; + if ( + firstSection && + firstSection.controlSetRows && + firstSection.controlSetRows[0] + ) { + const firstControl = firstSection.controlSetRows[0][0]; + console.log('First control in panel:', typeof firstControl, firstControl); + if ( + typeof firstControl === 'function' && + (firstControl as any).isModernPanel + ) { + console.log('Found embedded modern panel! 🎉'); + // Return the existing structure which already has our modern panel + return getMemoizedSectionsToRender( + datasourceType, + controlPanel as ControlPanelConfig, + ); + } + } + } + + // Check if this is a modern React component at the top level + if ( + typeof controlPanel === 'function' && + (controlPanel as any).isModernPanel + ) { + // For modern panels, return a single section containing the component + console.log('Returning modern panel section (top level)'); + return [ + { + label: null, + expanded: true, + controlSetRows: [[controlPanel as any]], + }, + ]; + } + + // Otherwise, treat it as a traditional ControlPanelConfig + const controlPanelConfig = (controlPanel as ControlPanelConfig) || {}; return getMemoizedSectionsToRender(datasourceType, controlPanelConfig); }
