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 df772a9afa6adb9c8270dc54865aa8438d7056fa Author: Evan Rusackas <[email protected]> AuthorDate: Thu Aug 14 09:51:09 2025 -0700 feat: Implement nuclear approach for control panel migration - Create empty control panel placeholder for non-migrated charts - Switch Bar and Line charts to empty panels temporarily - Update Pie chart to use traditional control config structure - Remove experimental React-based PieControlPanel component - Fix TypeScript errors in control panel configurations - Simplify migration path by focusing on one working chart first This approach allows us to get one chart (Pie) working properly before migrating others, avoiding complex dual-architecture issues. --- superset-frontend/package-lock.json | 2 +- .../components/ModernControlPanelExample.tsx | 282 -------- .../components/ReactControlWrappers.tsx | 370 ---------- .../src/shared-controls/components/index.tsx | 3 - .../src/Pie/controlPanelModern.tsx | 775 +++++++++------------ .../plugins/plugin-chart-echarts/src/Pie/index.ts | 3 +- .../src/Timeseries/Regular/Bar/index.ts | 2 +- .../src/Timeseries/Regular/Line/index.ts | 2 +- .../plugin-chart-echarts/src/emptyControlPanel.ts | 36 + .../components/ModernControlPanelRenderer.tsx | 20 +- 10 files changed, 387 insertions(+), 1108 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index ad6ac17411..5442463535 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -61644,7 +61644,7 @@ "@storybook/types": "8.4.7", "@types/react-loadable": "^5.5.11", "core-js": "3.40.0", - "gh-pages": "^6.2.0", + "gh-pages": "^6.3.0", "jquery": "^3.7.1", "memoize-one": "^5.2.1", "react": "^17.0.2", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx deleted file mode 100644 index 89140363df..0000000000 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx +++ /dev/null @@ -1,282 +0,0 @@ -/** - * 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 { Row, Col } from '@superset-ui/core/components'; -import { - ControlSection, - SingleControlRow, - TwoColumnRow, - ThreeColumnRow, -} from './ControlPanelLayout'; -import { - GroupBy, - Metrics, - AdhocFilters, - ColorScheme, -} from './ReactControlWrappers'; - -/** - * Example of a modern control panel that uses React components directly - * instead of the legacy controlSetRows structure. - * - * This demonstrates how to: - * 1. Use Ant Design's Row/Col for layout - * 2. Use our layout utility components - * 3. Structure sections with React components - * 4. Avoid the nested array structure of controlSetRows - */ - -interface ModernControlPanelProps { - values: Record<string, any>; - onChange: (name: string, value: any) => void; - datasource?: any; -} - -export const ModernControlPanelExample: FC<ModernControlPanelProps> = ({ - values, - onChange, - datasource, -}) => ( - <div className="modern-control-panel"> - {/* Query Section - Always expanded */} - <ControlSection label={t('Query')} expanded> - {/* Single control in full width */} - <SingleControlRow> - <GroupBy value={values.groupby} onChange={onChange} /> - </SingleControlRow> - - {/* Two controls side by side */} - <TwoColumnRow - left={<Metrics value={values.metrics} onChange={onChange} />} - right={ - <AdhocFilters value={values.adhoc_filters} onChange={onChange} /> - } - /> - - {/* Three controls in a row */} - <ThreeColumnRow - left={ - <div> - <label>{t('Row Limit')}</label> - <input - type="number" - value={values.row_limit || 100} - onChange={e => - onChange('row_limit', parseInt(e.target.value, 10)) - } - /> - </div> - } - center={ - <div> - <label>{t('Sort By')}</label> - <select - value={values.sort_by || 'metric'} - onChange={e => onChange('sort_by', e.target.value)} - > - <option value="metric">Metric</option> - <option value="alpha">Alphabetical</option> - </select> - </div> - } - right={ - <div> - <label>{t('Order')}</label> - <select - value={values.order || 'desc'} - onChange={e => onChange('order', e.target.value)} - > - <option value="desc">Descending</option> - <option value="asc">Ascending</option> - </select> - </div> - } - /> - </ControlSection> - - {/* Appearance Section - Collapsible */} - <ControlSection - label={t('Appearance')} - description={t('Customize chart appearance')} - expanded={false} - > - {/* Using Row/Col directly for custom layouts */} - <Row gutter={[16, 16]}> - <Col span={24}> - <ColorScheme value={values.color_scheme} onChange={onChange} /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={8}> - <label>{t('Opacity')}</label> - <input - type="range" - min="0" - max="1" - step="0.1" - value={values.opacity || 1} - onChange={e => onChange('opacity', parseFloat(e.target.value))} - /> - </Col> - <Col span={8}> - <label>{t('Show Legend')}</label> - <input - type="checkbox" - checked={values.show_legend ?? true} - onChange={e => onChange('show_legend', e.target.checked)} - /> - </Col> - <Col span={8}> - <label>{t('Show Labels')}</label> - <input - type="checkbox" - checked={values.show_labels ?? false} - onChange={e => onChange('show_labels', e.target.checked)} - /> - </Col> - </Row> - - {/* Conditional controls */} - {values.show_labels && ( - <Row gutter={[16, 16]}> - <Col span={12}> - <label>{t('Label Type')}</label> - <select - value={values.label_type || 'value'} - onChange={e => onChange('label_type', e.target.value)} - > - <option value="value">Value</option> - <option value="percent">Percentage</option> - <option value="key">Category</option> - </select> - </Col> - <Col span={12}> - <label>{t('Label Position')}</label> - <select - value={values.label_position || 'inside'} - onChange={e => onChange('label_position', e.target.value)} - > - <option value="inside">Inside</option> - <option value="outside">Outside</option> - </select> - </Col> - </Row> - )} - </ControlSection> - - {/* Advanced Section */} - <ControlSection label={t('Advanced')} expanded={false}> - <Row gutter={[16, 16]}> - <Col span={24}> - <label>{t('Custom CSS')}</label> - <textarea - value={values.custom_css || ''} - onChange={e => onChange('custom_css', e.target.value)} - rows={4} - style={{ width: '100%' }} - placeholder={t('Enter custom CSS styles')} - /> - </Col> - </Row> - </ControlSection> - </div> -); - -/** - * Alternative approach using a configuration object - * This could be used to generate the UI dynamically - */ -export const modernPanelConfig = { - sections: [ - { - id: 'query', - label: t('Query'), - expanded: true, - rows: [ - { - type: 'single', - control: { type: 'groupby', name: 'groupby' }, - }, - { - type: 'double', - left: { type: 'metrics', name: 'metrics' }, - right: { type: 'adhoc_filters', name: 'adhoc_filters' }, - }, - { - type: 'triple', - left: { type: 'row_limit', name: 'row_limit' }, - center: { type: 'sort_by', name: 'sort_by' }, - right: { type: 'order', name: 'order' }, - }, - ], - }, - { - id: 'appearance', - label: t('Appearance'), - description: t('Customize chart appearance'), - expanded: false, - rows: [ - { - type: 'single', - control: { type: 'color_scheme', name: 'color_scheme' }, - }, - { - type: 'custom', - render: (values: any, onChange: any) => ( - <Row gutter={[16, 16]}> - <Col span={8}> - <label>{t('Opacity')}</label> - <input - type="range" - min="0" - max="1" - step="0.1" - value={values.opacity || 1} - onChange={e => - onChange('opacity', parseFloat(e.target.value)) - } - /> - </Col> - <Col span={8}> - <label>{t('Show Legend')}</label> - <input - type="checkbox" - checked={values.show_legend ?? true} - onChange={e => onChange('show_legend', e.target.checked)} - /> - </Col> - <Col span={8}> - <label>{t('Show Labels')}</label> - <input - type="checkbox" - checked={values.show_labels ?? false} - onChange={e => onChange('show_labels', e.target.checked)} - /> - </Col> - </Row> - ), - }, - ], - }, - ], -}; - -export default ModernControlPanelExample; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ReactControlWrappers.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ReactControlWrappers.tsx deleted file mode 100644 index 635b507574..0000000000 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ReactControlWrappers.tsx +++ /dev/null @@ -1,370 +0,0 @@ -/** - * 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'; - -/** - * React component wrappers for control panel controls. - * These components wrap the underlying control implementations - * to provide a React component interface for modern control panels. - */ - -interface ControlProps { - value?: any; - onChange: (name: string, value: any) => void; - datasource?: any; - [key: string]: any; -} - -/** - * GroupBy control component - */ -export const GroupBy: FC<ControlProps> = ({ value, onChange, ...props }) => ( - // This would normally render the actual DndColumnSelect component - // For now, return a placeholder - <div className="control-wrapper"> - <label>{t('Group by')}</label> - <div className="groupby-control"> - {/* DndColumnSelect would go here */} - <input - type="text" - value={JSON.stringify(value || [])} - onChange={e => { - try { - onChange('groupby', JSON.parse(e.target.value)); - } catch { - // Invalid JSON - } - }} - placeholder={t('Select columns')} - /> - </div> - <small className="text-muted">{t('One or many columns to group by')}</small> - </div> -); - -/** - * Metrics control component - */ -export const Metrics: FC<ControlProps> = ({ value, onChange, ...props }) => ( - <div className="control-wrapper"> - <label>{t('Metrics')}</label> - <div className="metrics-control"> - {/* DndMetricSelect would go here */} - <input - type="text" - value={JSON.stringify(value || [])} - onChange={e => { - try { - onChange('metrics', JSON.parse(e.target.value)); - } catch { - // Invalid JSON - } - }} - placeholder={t('Select metrics')} - /> - </div> - <small className="text-muted">{t('One or many metrics to display')}</small> - </div> -); - -/** - * AdhocFilters control component - */ -export const AdhocFilters: FC<ControlProps> = ({ - value, - onChange, - ...props -}) => ( - <div className="control-wrapper"> - <label>{t('Filters')}</label> - <div className="adhoc-filters-control"> - {/* AdhocFilterControl would go here */} - <input - type="text" - value={JSON.stringify(value || [])} - onChange={e => { - try { - onChange('adhoc_filters', JSON.parse(e.target.value)); - } catch { - // Invalid JSON - } - }} - placeholder={t('Add filters')} - /> - </div> - <small className="text-muted">{t('Filters to apply to the data')}</small> - </div> -); - -/** - * RowLimit control component - */ -export const RowLimit: FC<ControlProps> = ({ value, onChange, ...props }) => ( - <div className="control-wrapper"> - <label>{t('Row limit')}</label> - <input - type="number" - value={value || 100} - onChange={e => onChange('row_limit', parseInt(e.target.value, 10))} - min={1} - max={100000} - /> - <small className="text-muted"> - {t('Maximum number of rows to display')} - </small> - </div> -); - -/** - * ColorScheme control component - */ -export const ColorScheme: FC<ControlProps> = ({ - value, - onChange, - ...props -}) => ( - // This would normally render the actual ColorSchemeControlWrapper - <div className="control-wrapper"> - <label>{t('Color scheme')}</label> - <select - value={value || 'supersetColors'} - onChange={e => onChange('color_scheme', e.target.value)} - > - <option value="supersetColors">Superset Colors</option> - <option value="googleCategory10c">Google Category 10c</option> - <option value="d3Category10">D3 Category 10</option> - <option value="d3Category20">D3 Category 20</option> - <option value="d3Category20b">D3 Category 20b</option> - <option value="d3Category20c">D3 Category 20c</option> - </select> - <small className="text-muted">{t('Color scheme for the chart')}</small> - </div> -); - -/** - * CurrencyFormat control component - */ -export const CurrencyFormat: FC<ControlProps> = ({ - value, - onChange, - ...props -}) => ( - <div className="control-wrapper"> - <label>{t('Currency format')}</label> - <select - value={value || 'USD'} - onChange={e => onChange('currency_format', e.target.value)} - > - <option value="USD">USD ($)</option> - <option value="EUR">EUR (€)</option> - <option value="GBP">GBP (£)</option> - <option value="JPY">JPY (¥)</option> - <option value="CNY">CNY (¥)</option> - <option value="INR">INR (₹)</option> - </select> - <small className="text-muted">{t('Currency to use for formatting')}</small> - </div> -); - -/** - * CheckboxControl component - */ -export const CheckboxControl: FC<{ - name: string; - label: string; - value?: boolean; - onChange: (name: string, value: any) => void; - description?: string; - disabled?: boolean; -}> = ({ name, label, value, onChange, description, disabled }) => ( - <div className="control-wrapper"> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <input - type="checkbox" - checked={value ?? false} - onChange={e => onChange(name, e.target.checked)} - disabled={disabled} - /> - {label} - </label> - {description && <small className="text-muted">{description}</small>} - </div> -); - -/** - * NumberControl component - */ -export const NumberControl: FC<{ - name: string; - label: string; - value?: number; - onChange: (name: string, value: any) => void; - description?: string; - min?: number; - max?: number; - step?: number; -}> = ({ name, label, value, onChange, description, min, max, step }) => ( - <div className="control-wrapper"> - <label>{label}</label> - <input - type="number" - value={value ?? 0} - onChange={e => onChange(name, parseFloat(e.target.value))} - min={min} - max={max} - step={step} - /> - {description && <small className="text-muted">{description}</small>} - </div> -); - -/** - * SelectControl component - */ -export const SelectControl: FC<{ - name: string; - label: string; - value?: any; - onChange: (name: string, value: any) => void; - description?: string; - choices?: Array<[any, string]>; - freeForm?: boolean; - tokenSeparators?: string[]; - disabled?: boolean; -}> = ({ - name, - label, - value, - onChange, - description, - choices = [], - disabled, -}) => ( - <div className="control-wrapper"> - <label>{label}</label> - <select - value={value ?? ''} - onChange={e => onChange(name, e.target.value)} - disabled={disabled} - > - <option value="">Select...</option> - {choices.map(([val, text]) => ( - <option key={val} value={val}> - {text} - </option> - ))} - </select> - {description && <small className="text-muted">{description}</small>} - </div> -); - -/** - * SliderControl component - */ -export const SliderControl: FC<{ - name: string; - label: string; - value?: number; - onChange: (name: string, value: any) => void; - description?: string; - min?: number; - max?: number; - step?: number; -}> = ({ - name, - label, - value, - onChange, - description, - min = 0, - max = 100, - step = 1, -}) => ( - <div className="control-wrapper"> - <label> - {label}: {value ?? min} - </label> - <input - type="range" - value={value ?? min} - onChange={e => onChange(name, parseFloat(e.target.value))} - min={min} - max={max} - step={step} - style={{ width: '100%' }} - /> - {description && <small className="text-muted">{description}</small>} - </div> -); - -/** - * TextControl component - */ -export const TextControl: FC<{ - name: string; - label: string; - value?: string; - onChange: (name: string, value: any) => void; - description?: string; - placeholder?: string; - isFloat?: boolean; - disabled?: boolean; -}> = ({ - name, - label, - value, - onChange, - description, - placeholder, - isFloat, - disabled, -}) => ( - <div className="control-wrapper"> - <label>{label}</label> - <input - type="text" - value={value ?? ''} - onChange={e => { - const val = e.target.value; - onChange(name, isFloat ? parseFloat(val) || val : val); - }} - placeholder={placeholder} - disabled={disabled} - /> - {description && <small className="text-muted">{description}</small>} - </div> -); - -/** - * Export all control components - */ -export default { - GroupBy, - Metrics, - AdhocFilters, - RowLimit, - ColorScheme, - CurrencyFormat, - CheckboxControl, - NumberControl, - SelectControl, - SliderControl, - TextControl, -}; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx index 52bdac1858..b64952ae4d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx @@ -54,7 +54,4 @@ export { ReactControlPanel } from './ReactControlPanel'; // Export control panel layout components export * from './ControlPanelLayout'; -// Export React control wrappers for modern panels -export * from './ReactControlWrappers'; - // Inline control functions are exported from SharedControlComponents diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx index 9556f32f00..f600713c74 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { FC } from 'react'; -import { ensureIsInt, t, validateNonEmpty } from '@superset-ui/core'; -import { Row, Col, Collapse } from '@superset-ui/core/components'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, D3_FORMAT_DOCS, @@ -27,464 +25,357 @@ import { D3_TIME_FORMAT_OPTIONS, getStandardizedControls, sharedControls, - // Import the actual React components - CheckboxControl, - NumberControl, - SelectControl, - SliderControl, - TextControl, - // Import React control wrappers - GroupBy, - Metrics, - AdhocFilters, - RowLimit, - ColorScheme, - CurrencyFormat, } from '@superset-ui/chart-controls'; import { DEFAULT_FORM_DATA } from './types'; -const { - donut, - innerRadius, - labelsOutside, - labelType, - labelLine, - outerRadius, - numberFormat, - showLabels, - roseType, -} = DEFAULT_FORM_DATA; - -/** - * Modern React-based control panel configuration - */ -interface ModernPieControlPanelProps { - values: Record<string, any>; - onChange: (name: string, value: any) => void; - datasource?: any; - formData?: any; - validationErrors?: Record<string, string[]>; -} - -/** - * Query Section Component - */ -const QuerySection: FC<ModernPieControlPanelProps> = ({ values, onChange }) => ( - <> - <Row gutter={[16, 16]}> - <Col span={24}> - <GroupBy value={values.groupby} onChange={onChange} /> - </Col> - </Row> - <Row gutter={[16, 16]}> - <Col span={24}> - <Metrics value={values.metrics} onChange={onChange} /> - </Col> - </Row> - <Row gutter={[16, 16]}> - <Col span={24}> - <AdhocFilters value={values.adhoc_filters} onChange={onChange} /> - </Col> - </Row> - <Row gutter={[16, 16]}> - <Col span={12}> - <RowLimit value={values.row_limit} onChange={onChange} /> - </Col> - <Col span={12}> - <CheckboxControl - name="sort_by_metric" - label={t('Sort by Metric')} - value={values.sort_by_metric ?? true} - onChange={onChange} - description={t('Sort series by metric values')} - /> - </Col> - </Row> - </> -); - /** - * Chart Options Section Component + * Modern Pie Chart Control Panel using the existing control infrastructure + * This version creates individual control items that work with the current system */ -const ChartOptionsSection: FC<ModernPieControlPanelProps> = ({ - values, - onChange, -}) => ( - <> - <Row gutter={[16, 16]}> - <Col span={24}> - <ColorScheme value={values.color_scheme} onChange={onChange} /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={12}> - <TextControl - name="show_labels_threshold" - label={t('Percentage threshold')} - value={values.show_labels_threshold ?? 5} - onChange={onChange} - description={t( - 'Minimum threshold in percentage points for showing labels.', - )} - isFloat - /> - </Col> - <Col span={12}> - <NumberControl - name="threshold_for_other" - label={t('Threshold for Other')} - value={values.threshold_for_other ?? 0} - onChange={onChange} - min={0} - max={100} - step={0.5} - description={t( - 'Values less than this percentage will be grouped into the Other category.', - )} - /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={12}> - <SelectControl - name="roseType" - label={t('Rose Type')} - value={values.roseType ?? roseType} - onChange={onChange} - choices={[ - ['area', t('Area')], - ['radius', t('Radius')], - [null, t('None')], - ]} - description={t('Whether to show as Nightingale chart.')} - /> - </Col> - </Row> - </> -); - -/** - * Legend Section Component - */ -const LegendSection: FC<ModernPieControlPanelProps> = ({ - values, - onChange, -}) => ( - <> - <Row gutter={[16, 16]}> - <Col span={24}> - <CheckboxControl - name="show_legend" - label={t('Show legend')} - value={values.show_legend} - onChange={onChange} - description={t('Whether to display a legend for the chart')} - /> - </Col> - </Row> - - {values.show_legend && ( - <> - <Row gutter={[16, 16]}> - <Col span={12}> - <SelectControl - name="legendType" - label={t('Legend type')} - value={values.legendType} - onChange={onChange} - choices={[ +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [ + { + name: 'groupby', + config: sharedControls.groupby || {}, + }, + ], + [ + { + name: 'metric', + config: sharedControls.metrics || {}, + }, + ], + [ + { + name: 'adhoc_filters', + config: sharedControls.adhoc_filters || {}, + }, + ], + [ + { + name: 'row_limit', + config: { + ...sharedControls.row_limit, + default: 100, + }, + }, + ], + [ + { + name: 'sort_by_metric', + config: sharedControls.sort_by_metric || {}, + }, + ], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'color_scheme', + config: sharedControls.color_scheme || {}, + }, + ], + [ + { + name: 'show_labels_threshold', + config: { + type: 'TextControl', + label: t('Percentage threshold'), + renderTrigger: true, + isFloat: true, + default: 5, + description: t( + 'Minimum threshold in percentage points for showing labels.', + ), + }, + }, + { + name: 'threshold_for_other', + config: { + type: 'TextControl', + label: t('Threshold for Other'), + renderTrigger: true, + isFloat: true, + default: 0, + description: t( + 'Values less than this percentage will be grouped into the Other category.', + ), + }, + }, + ], + [ + { + name: 'roseType', + config: { + type: 'SelectControl', + label: t('Rose Type'), + description: t('Whether to show as Nightingale chart.'), + renderTrigger: true, + choices: [ + ['area', t('Area')], + ['radius', t('Radius')], + [null, t('None')], + ], + default: null, + clearable: false, + }, + }, + ], + ], + }, + { + label: t('Labels'), + expanded: true, + controlSetRows: [ + [ + { + name: 'label_type', + config: { + type: 'SelectControl', + label: t('Label Type'), + default: 'key', + renderTrigger: true, + choices: [ + ['key', t('Category Name')], + ['value', t('Value')], + ['percent', t('Percentage')], + ['key_value', t('Category and Value')], + ['key_percent', t('Category and Percentage')], + ['key_value_percent', t('Category, Value and Percentage')], + ['value_percent', t('Value and Percentage')], + ['template', t('Template')], + ], + description: t('What should be shown on the label?'), + clearable: false, + }, + }, + ], + [ + { + name: 'label_template', + config: { + type: 'TextControl', + label: t('Label Template'), + renderTrigger: true, + description: t( + 'Format data labels. Use variables: {name}, {value}, {percent}.', + ), + visibility: ({ controls }) => + controls?.label_type?.value === 'template', + }, + }, + ], + [ + { + name: 'number_format', + config: { + type: 'SelectControl', + freeForm: true, + label: t('Number format'), + renderTrigger: true, + default: 'SMART_NUMBER', + choices: D3_FORMAT_OPTIONS, + description: D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, + }, + }, + { + name: 'date_format', + config: { + type: 'SelectControl', + freeForm: true, + label: t('Date format'), + renderTrigger: true, + choices: D3_TIME_FORMAT_OPTIONS, + default: 'smart_date', + description: D3_FORMAT_DOCS, + }, + }, + ], + [ + { + name: 'show_labels', + config: { + type: 'CheckboxControl', + label: t('Show Labels'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.showLabels, + description: t('Whether to display the labels.'), + }, + }, + { + name: 'labels_outside', + config: { + type: 'CheckboxControl', + label: t('Put labels outside'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.labelsOutside, + description: t('Put the labels outside of the pie?'), + }, + }, + { + name: 'label_line', + config: { + type: 'CheckboxControl', + label: t('Label Line'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.labelLine, + description: t( + 'Draw line from Pie to label when labels outside?', + ), + }, + }, + ], + [ + { + name: 'show_total', + config: { + type: 'CheckboxControl', + label: t('Show Total'), + renderTrigger: true, + default: false, + description: t('Whether to display the aggregate count'), + }, + }, + ], + ], + }, + { + label: t('Pie shape'), + expanded: true, + controlSetRows: [ + [ + { + name: 'outerRadius', + config: { + type: 'SliderControl', + label: t('Outer Radius'), + renderTrigger: true, + min: 10, + max: 100, + step: 1, + default: DEFAULT_FORM_DATA.outerRadius, + description: t('Outer edge of Pie chart'), + }, + }, + ], + [ + { + name: 'donut', + config: { + type: 'CheckboxControl', + label: t('Donut'), + renderTrigger: true, + default: DEFAULT_FORM_DATA.donut, + description: t('Do you want a donut or a pie?'), + }, + }, + ], + [ + { + name: 'innerRadius', + config: { + type: 'SliderControl', + label: t('Inner Radius'), + renderTrigger: true, + min: 0, + max: 100, + step: 1, + default: DEFAULT_FORM_DATA.innerRadius, + description: t('Inner radius of donut hole'), + visibility: ({ controls }) => Boolean(controls?.donut?.value), + }, + }, + ], + ], + }, + { + label: t('Legend'), + expanded: true, + controlSetRows: [ + [ + { + name: 'show_legend', + config: { + type: 'CheckboxControl', + label: t('Show legend'), + renderTrigger: true, + default: true, + description: t('Whether to display a legend for the chart'), + }, + }, + ], + [ + { + name: 'legendType', + config: { + type: 'SelectControl', + label: t('Legend type'), + renderTrigger: true, + choices: [ ['scroll', t('Scroll')], ['plain', t('Plain')], - ]} - description={t('Legend type')} - /> - </Col> - <Col span={12}> - <SelectControl - name="legendOrientation" - label={t('Legend orientation')} - value={values.legendOrientation} - onChange={onChange} - choices={[ + ], + default: 'scroll', + clearable: false, + description: t('Legend type'), + visibility: ({ controls }) => + Boolean(controls?.show_legend?.value), + }, + }, + { + name: 'legendOrientation', + config: { + type: 'SelectControl', + label: t('Legend orientation'), + renderTrigger: true, + choices: [ ['top', t('Top')], ['bottom', t('Bottom')], ['left', t('Left')], ['right', t('Right')], - ]} - description={t('Legend orientation')} - /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={24}> - <NumberControl - name="legendMargin" - label={t('Legend margin')} - value={values.legendMargin} - onChange={onChange} - min={0} - max={100} - description={t( - 'Additional margin to add between legend and chart', - )} - /> - </Col> - </Row> - </> - )} - </> -); - -/** - * Labels Section Component - */ -const LabelsSection: FC<ModernPieControlPanelProps> = ({ - values, - onChange, -}) => ( - <> - <Row gutter={[16, 16]}> - <Col span={24}> - <SelectControl - name="label_type" - label={t('Label Type')} - value={values.label_type ?? labelType} - onChange={onChange} - choices={[ - ['key', t('Category Name')], - ['value', t('Value')], - ['percent', t('Percentage')], - ['key_value', t('Category and Value')], - ['key_percent', t('Category and Percentage')], - ['key_value_percent', t('Category, Value and Percentage')], - ['value_percent', t('Value and Percentage')], - ['template', t('Template')], - ]} - description={t('What should be shown on the label?')} - /> - </Col> - </Row> - - {values.label_type === 'template' && ( - <Row gutter={[16, 16]}> - <Col span={24}> - <TextControl - name="label_template" - label={t('Label Template')} - value={values.label_template} - onChange={onChange} - description={t( - 'Format data labels. ' + - 'Use variables: {name}, {value}, {percent}. ' + - '\\n represents a new line. ' + - 'ECharts compatibility:\n' + - '{a} (series), {b} (name), {c} (value), {d} (percentage)', - )} - /> - </Col> - </Row> - )} - - <Row gutter={[16, 16]}> - <Col span={12}> - <SelectControl - name="number_format" - label={t('Number format')} - value={values.number_format ?? numberFormat} - onChange={onChange} - choices={D3_FORMAT_OPTIONS} - freeForm - tokenSeparators={['\n', '\t', ';']} - description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`} - /> - </Col> - <Col span={12}> - <CurrencyFormat value={values.currency_format} onChange={onChange} /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={12}> - <SelectControl - name="date_format" - label={t('Date format')} - value={values.date_format ?? 'smart_date'} - onChange={onChange} - choices={D3_TIME_FORMAT_OPTIONS} - freeForm - description={D3_FORMAT_DOCS} - /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={8}> - <CheckboxControl - name="show_labels" - label={t('Show Labels')} - value={values.show_labels ?? showLabels} - onChange={onChange} - description={t('Whether to display the labels.')} - /> - </Col> - <Col span={8}> - <CheckboxControl - name="labels_outside" - label={t('Put labels outside')} - value={values.labels_outside ?? labelsOutside} - onChange={onChange} - description={t('Put the labels outside of the pie?')} - disabled={!values.show_labels} - /> - </Col> - <Col span={8}> - <CheckboxControl - name="label_line" - label={t('Label Line')} - value={values.label_line ?? labelLine} - onChange={onChange} - description={t('Draw line from Pie to label when labels outside?')} - disabled={!values.show_labels} - /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={12}> - <CheckboxControl - name="show_total" - label={t('Show Total')} - value={values.show_total ?? false} - onChange={onChange} - description={t('Whether to display the aggregate count')} - /> - </Col> - </Row> - </> -); - -/** - * Pie Shape Section Component - */ -const PieShapeSection: FC<ModernPieControlPanelProps> = ({ - values, - onChange, -}) => ( - <> - <Row gutter={[16, 16]}> - <Col span={24}> - <SliderControl - name="outerRadius" - label={t('Outer Radius')} - value={values.outerRadius ?? outerRadius} - onChange={onChange} - min={10} - max={100} - step={1} - description={t('Outer edge of Pie chart')} - /> - </Col> - </Row> - - <Row gutter={[16, 16]}> - <Col span={12}> - <CheckboxControl - name="donut" - label={t('Donut')} - value={values.donut ?? donut} - onChange={onChange} - description={t('Do you want a donut or a pie?')} - /> - </Col> - </Row> - - {values.donut && ( - <Row gutter={[16, 16]}> - <Col span={24}> - <SliderControl - name="innerRadius" - label={t('Inner Radius')} - value={values.innerRadius ?? innerRadius} - onChange={onChange} - min={0} - max={100} - step={1} - description={t('Inner radius of donut hole')} - /> - </Col> - </Row> - )} - </> -); - -/** - * Main Modern Pie Control Panel Component - */ -export const ModernPieControlPanel: FC<ModernPieControlPanelProps> = props => ( - <div className="modern-pie-control-panel"> - <Collapse defaultActiveKey={['query', 'chart-options']} ghost> - <Collapse.Panel header={t('Query')} key="query"> - <QuerySection {...props} /> - </Collapse.Panel> - - <Collapse.Panel header={t('Chart Options')} key="chart-options"> - <ChartOptionsSection {...props} /> - - <div style={{ marginTop: 24 }}> - <h4>{t('Legend')}</h4> - <LegendSection {...props} /> - </div> - - <div style={{ marginTop: 24 }}> - <h4>{t('Labels')}</h4> - <LabelsSection {...props} /> - </div> - - <div style={{ marginTop: 24 }}> - <h4>{t('Pie shape')}</h4> - <PieShapeSection {...props} /> - </div> - </Collapse.Panel> - </Collapse> - </div> -); - -/** - * Create a backward-compatible control panel config - * This allows the modern panel to work with the existing system - */ -export const createBackwardCompatibleConfig = (): ControlPanelConfig => ({ - controlPanelSections: [ - { - label: t('Modern Control Panel'), - expanded: true, - controlSetRows: [ + ], + default: 'top', + clearable: false, + description: t('Legend orientation'), + visibility: ({ controls }) => + Boolean(controls?.show_legend?.value), + }, + }, + ], [ - // Wrap the entire modern panel as a single React element - <ModernPieControlPanel values={{}} onChange={() => {}} />, + { + name: 'legendMargin', + config: { + type: 'TextControl', + label: t('Legend margin'), + renderTrigger: true, + isInt: true, + default: 0, + description: t( + 'Additional margin to add between legend and chart', + ), + visibility: ({ controls }) => + Boolean(controls?.show_legend?.value), + }, + }, ], ], }, ], - controlOverrides: { - series: { - validators: [validateNonEmpty], - clearable: false, - }, - row_limit: { - default: 100, - }, - }, formDataOverrides: formData => ({ ...formData, metric: getStandardizedControls().shiftMetric(), groupby: getStandardizedControls().popAllColumns(), - row_limit: - ensureIsInt(formData.row_limit, 100) >= 100 ? 100 : formData.row_limit, + row_limit: formData.row_limit ?? 100, }), -}); +}; -export default createBackwardCompatibleConfig(); +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 88d6e598b3..faaf3569b3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts @@ -18,7 +18,8 @@ */ import { Behavior, t } from '@superset-ui/core'; import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; +// Use the modern control panel with proper Redux integration +import controlPanel from './controlPanelModern'; import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; import example1 from './images/Pie1.jpg'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts index f2a9155550..f0e371b841 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts @@ -24,7 +24,7 @@ import { } from '../../types'; import { EchartsChartPlugin } from '../../../types'; import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; +import controlPanel from '../../../emptyControlPanel'; import transformProps from '../../transformProps'; import thumbnail from './images/thumbnail.png'; import example1 from './images/Bar1.png'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts index a63168617d..c309f167cf 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts @@ -23,7 +23,7 @@ import { EchartsTimeseriesSeriesType, } from '../../types'; import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; +import controlPanel from '../../../emptyControlPanel'; import transformProps from '../../transformProps'; import thumbnail from './images/thumbnail.png'; import example1 from './images/Line1.png'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/emptyControlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/emptyControlPanel.ts new file mode 100644 index 0000000000..dbc06dc11f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/emptyControlPanel.ts @@ -0,0 +1,36 @@ +/** + * 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 { t } from '@superset-ui/core'; +import { ControlPanelConfig } from '@superset-ui/chart-controls'; + +/** + * Temporary empty control panel for migration. + * This is a placeholder while we migrate to the new React-based control system. + */ +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Migration in Progress'), + expanded: true, + controlSetRows: [], + }, + ], +}; + +export default config; diff --git a/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx b/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx index 2ecdb40cb0..323d4036c2 100644 --- a/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx +++ b/superset-frontend/src/explore/components/ModernControlPanelRenderer.tsx @@ -63,25 +63,29 @@ export const ModernControlPanelRenderer: FC< }) => { // 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 && - ('values' in element.props || + ('value' in element.props || 'onChange' in element.props || - element.type?.name?.includes('Modern')); + elementType?.name?.includes('PieControlPanel') || + elementType?.name?.includes('Modern')); if (!isModernPanel) { // If it's not a modern panel, render as-is return element; } - // Create the modern props adapter - const modernProps: ModernControlPanelProps = { - values: formData, + // 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, }; @@ -98,10 +102,12 @@ export const isModernControlPanel = (element: any): boolean => { } const elementType = element.type as any; + const props = element.props as any; return ( + elementType?.name?.includes('PieControlPanel') || elementType?.name?.includes('Modern') || elementType?.displayName?.includes('Modern') || - element.props?.isModernPanel === true + props?.isModernPanel === true ); }; @@ -111,7 +117,7 @@ export const isModernControlPanel = (element: any): boolean => { export function withModernPanelMarker<P extends ModernControlPanelProps>( Component: FC<P>, ): FC<P> { - const WrappedComponent: FC<P> = props => <Component {...props} />; + const WrappedComponent: FC<P> = props => <Component {...(props as P)} />; WrappedComponent.displayName = `Modern${Component.displayName || Component.name}`;
