You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2025/08/18 23:02:52 UTC
(superset) branch move-controls updated: fix: Import Tabs from antd instead of @superset-ui/core in Funnel control panel
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
The following commit(s) were added to refs/heads/move-controls by this push:
new 544236ff20 fix: Import Tabs from antd instead of @superset-ui/core in Funnel control panel
544236ff20 is described below
commit 544236ff20dc3d37db74f12efe7857703daa5a9e
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Mon Aug 18 16:00:56 2025 -0700
fix: Import Tabs from antd instead of @superset-ui/core in Funnel control panel
The @superset-ui/core package doesn't export a Tabs component, causing
'Element type is invalid' error. Fixed by importing Tabs directly from antd,
matching the pattern used in PieControlPanelSimple.tsx.
Also fixed React hooks order issue in PieControlPanelSimple.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <no...@anthropic.com>
---
CONTROL_PANEL_MIGRATION_AGENT.md | 723 +++++++++++++++++++++
.../src/Funnel/FunnelControlPanelSimple.tsx | 572 ++++++++++++++++
.../plugin-chart-echarts/src/Funnel/index.ts | 2 +-
.../src/Pie/PieControlPanelSimple.tsx | 13 +-
4 files changed, 1302 insertions(+), 8 deletions(-)
diff --git a/CONTROL_PANEL_MIGRATION_AGENT.md b/CONTROL_PANEL_MIGRATION_AGENT.md
new file mode 100644
index 0000000000..04cc2141e4
--- /dev/null
+++ b/CONTROL_PANEL_MIGRATION_AGENT.md
@@ -0,0 +1,723 @@
+# Control Panel Migration Agent
+
+A comprehensive guide for migrating Apache Superset control panels from the legacy config-based approach to the new React-based approach.
+
+## Overview
+
+This migration transforms control panels from complex string-referenced configurations (`controlPanelSections`/`controlSetRows`) to pure React components. The new approach provides:
+
+- **Direct React components** instead of config objects
+- **Full TypeScript support** with proper type safety
+- **Simplified architecture** with no JSON intermediary
+- **Better developer experience** with IDE autocomplete and refactoring support
+
+## Architecture Comparison
+
+### Legacy Architecture
+```typescript
+// Old approach: Config-based with string references and config objects
+const config: ControlPanelConfig = {
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ [GroupByControl()], // String reference
+ [MetricControl()], // String reference
+ [
+ {
+ name: 'show_labels',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Show Labels'),
+ renderTrigger: true,
+ default: true,
+ },
+ },
+ ], // Config object
+ ],
+ },
+ ],
+};
+```
+
+### New React-Based Architecture
+```typescript
+// New approach: Pure React components with direct JSX
+export const PieControlPanel: FC<PieControlPanelProps> = ({ ... }) => {
+ return (
+ <div>
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ // ... other props
+ />
+ <CheckboxControl
+ label={t('Show Labels')}
+ value={formValues.show_labels ?? true}
+ onChange={handleChange('show_labels')}
+ renderTrigger
+ />
+ </div>
+ );
+};
+
+// Mark as modern panel
+(PieControlPanel as any).isModernPanel = true;
+```
+
+## Migration Steps
+
+### 1. File Structure
+
+Keep the existing file location but change from config to React component:
+- `controlPanel.ts` → `[ChartName]ControlPanelSimple.tsx`
+- The file remains in the same directory as the original
+
+### 2. Update Imports
+
+**From (Legacy):**
+```typescript
+import {
+ ControlPanelConfig,
+ sharedControls,
+ GroupByControl,
+ MetricControl
+} from '@superset-ui/chart-controls';
+```
+
+**To (Modern):**
+```typescript
+import { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+} from '@superset-ui/chart-controls';
+
+// Direct component imports
+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 SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+```
+
+### 3. Component Structure Template
+
+Create a React component with this structure:
+
+```typescript
+interface [ChartName]ControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+export const [ChartName]ControlPanel: FC<[ChartName]ControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // Safety checks for datasource
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure safe data structures
+ const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
+ const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
+
+ // Helper for control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Tab state (if using tabs)
+ const [activeTab, setActiveTab] = useState('data');
+
+ // Component implementation here...
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Your controls here */}
+ </div>
+ );
+};
+
+// CRITICAL: Mark as modern panel
+([ChartName]ControlPanel as any).isModernPanel = true;
+
+// Export wrapper config for compatibility
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[[ChartName]ControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ // Move all defaults here
+ field_name: {
+ default: defaultValue,
+ label: t('Field Label'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
+```
+
+### 4. Control Mapping Reference
+
+#### String References → React Components
+
+| Legacy String | Modern React Component | Notes |
+|---------------|----------------------|--------|
+| `['groupby']` | `<DndColumnSelect ... />` | Multi-select columns |
+| `['metric']` | `<DndMetricSelect ... />` | Single metric select |
+| `['metrics']` | `<DndMetricSelect ... />` | Multi metric select |
+| `['adhoc_filters']` | `<DndFilterSelect ... />` | Advanced filters |
+| `['row_limit']` | `<TextControl isInt ... />` | Numeric input |
+| `['color_scheme']` | `ColorSchemeControl()` with `Control` wrapper | Special handling needed |
+
+#### Config Objects → React Components
+
+| Legacy Config | Modern Component | Example |
+|---------------|------------------|---------|
+| `{ type: 'TextControl', ... }` | `<TextControl ... />` | `<TextControl value={val} onChange={fn} />` |
+| `{ type: 'CheckboxControl', ... }` | `<CheckboxControl ... />` | `<CheckboxControl label="..." value={val} />` |
+| `{ type: 'SelectControl', ... }` | `<SelectControl ... />` | `<SelectControl choices={[...]} value={val} />` |
+| `{ type: 'SliderControl', ... }` | `<SliderControl ... />` | `<SliderControl {...{min: 0, max: 100}} />` |
+
+### 5. Props Mapping Guide
+
+| Legacy Config Property | Modern React Prop | Notes |
+|----------------------|------------------|-------|
+| `label` | `label` prop OR `<ControlHeader>` | Use ControlHeader for tooltips |
+| `description` | `description` prop OR `<ControlHeader>` | ControlHeader for complex descriptions |
+| `default` | Move to `controlOverrides` | Don't set as component prop |
+| `renderTrigger: true` | `renderTrigger` prop | Controls instant chart updates |
+| `visibility` | Conditional rendering | `{condition && <Control />}` |
+| `choices` | `choices` prop | For SelectControl |
+| `min/max/step` | Spread object | `{...{ min: 10, max: 100, step: 1 }}` |
+
+### 6. Implementing Tabbed Layout
+
+Most modern control panels use a Data/Customize tab structure:
+
+```typescript
+const [activeTab, setActiveTab] = useState('data');
+
+const dataTabContent = (
+ <div>
+ {/* Query-related controls: columns, metrics, filters, row limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label="" // Avoid duplicate labels
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+ {/* More data controls... */}
+ </div>
+);
+
+const customizeTabContent = (
+ <div>
+ {/* Styling and display controls */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+ {/* Styling controls... */}
+ </div>
+ </div>
+);
+
+const tabItems = [
+ { key: 'data', label: t('Data'), children: dataTabContent },
+ { key: 'customize', label: t('Customize'), children: customizeTabContent },
+];
+
+return (
+ <div style={{ padding: '16px' }}>
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+);
+```
+
+### 7. Common Control Patterns
+
+#### Drag-and-Drop Controls
+```typescript
+{/* Group By - Column Selection */}
+<DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label="" // Empty to avoid duplicate with ControlHeader
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+/>
+
+{/* Metric Selection */}
+<DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+/>
+
+{/* Filters */}
+<DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+/>
+```
+
+#### Color Scheme Control (Special Case)
+```typescript
+{/* Color Scheme requires special Control wrapper */}
+{(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ 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')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+})()}
+```
+
+#### Basic Controls
+```typescript
+{/* Text Input */}
+<TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="100"
+ controlId="row_limit"
+/>
+
+{/* Checkbox */}
+<CheckboxControl
+ label={t('Show Labels')}
+ description={t('Whether to display the labels.')}
+ value={formValues.show_labels ?? true}
+ onChange={handleChange('show_labels')}
+ renderTrigger
+ hovered
+/>
+
+{/* Select Dropdown */}
+<SelectControl
+ label={t('Label Type')}
+ description={t('What should be shown on the label?')}
+ value={formValues.label_type || 'key'}
+ onChange={handleChange('label_type')}
+ choices={[
+ ['key', t('Category Name')],
+ ['value', t('Value')],
+ ['percent', t('Percentage')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+/>
+
+{/* Slider */}
+<SliderControl
+ value={formValues.outerRadius || 70}
+ onChange={handleChange('outerRadius')}
+ {...{ min: 10, max: 100, step: 1 }}
+/>
+```
+
+#### Control Headers with Tooltips
+```typescript
+<ControlHeader
+ label={t('Percentage threshold')}
+ description={t('Minimum threshold in percentage points for showing labels.')}
+ renderTrigger
+ hovered
+/>
+```
+
+#### Conditional Rendering
+```typescript
+{/* Show control only when condition is met */}
+{formValues.show_labels && (
+ <CheckboxControl
+ label={t('Put labels outside')}
+ description={t('Put the labels outside of the pie?')}
+ value={formValues.labels_outside ?? true}
+ onChange={handleChange('labels_outside')}
+ renderTrigger
+ hovered
+ />
+)}
+
+{/* Nested conditional rendering */}
+{formValues.label_type === 'template' && (
+ <TextControl
+ value={formValues.label_template || ''}
+ onChange={handleChange('label_template')}
+ placeholder="{name}: {value}"
+ controlId="label_template"
+ />
+)}
+```
+
+### 8. Section Organization
+
+Use HTML headers and spacing for logical groupings:
+
+```typescript
+{/* Chart Options Section */}
+<div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Controls for this section */}
+ <div style={{ marginBottom: 16 }}>
+ {/* Individual control */}
+ </div>
+</div>
+
+{/* Labels Section */}
+<div style={{ marginBottom: 24 }}>
+ <h4>{t('Labels')}</h4>
+
+ {/* Label-related controls */}
+</div>
+```
+
+### 9. Control Defaults in controlOverrides
+
+Move all default values to the `controlOverrides` section:
+
+```typescript
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[PieControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ groupby: {
+ default: [],
+ label: t('Group by'),
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ show_labels: {
+ default: true,
+ label: t('Show labels'),
+ renderTrigger: true,
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ // ... all other defaults
+ },
+};
+```
+
+### 10. Chart Plugin Integration
+
+Update the chart plugin to use the new control panel:
+
+```typescript
+// In your chart's index.ts file
+import controlPanel from './PieControlPanelSimple'; // New React-based panel
+
+export default class EchartsPieChartPlugin extends EchartsChartPlugin {
+ constructor() {
+ super({
+ controlPanel,
+ // ... other config
+ });
+ }
+}
+```
+
+## Testing Your Migration
+
+### 1. Visual Validation
+- [ ] All controls render properly in the UI
+- [ ] Tab navigation works (if using tabs)
+- [ ] Control layout matches the original
+- [ ] Conditional controls show/hide correctly
+
+### 2. Functional Testing
+- [ ] Control changes update the chart immediately (if `renderTrigger: true`)
+- [ ] Form values persist when switching between tabs
+- [ ] Drag-and-drop controls work with datasource
+- [ ] Error states display appropriately
+- [ ] Default values apply correctly
+
+### 3. Integration Testing
+- [ ] Control panel works in Explore view
+- [ ] Values save correctly when creating charts
+- [ ] Dashboard filters work with the controls
+- [ ] Chart reloading preserves control values
+
+## Common Issues & Solutions
+
+### Issue: Double Labels on Controls
+**Problem:** Control shows both ControlHeader label and control's built-in label
+**Solution:** Set `label=""` on the control when using ControlHeader:
+```typescript
+<ControlHeader label={t('Group by')} />
+<DndColumnSelect
+ label="" // Empty to prevent duplicate
+ // ... other props
+/>
+```
+
+### Issue: Slider Min/Max Not Working
+**Problem:** Slider doesn't respect min/max values
+**Solution:** Use spread operator with object literal:
+```typescript
+<SliderControl
+ value={formValues.outerRadius || 70}
+ onChange={handleChange('outerRadius')}
+ {...{ min: 10, max: 100, step: 1 }} // Use spread with object
+/>
+```
+
+### Issue: Controls Not Triggering Chart Updates
+**Problem:** Chart doesn't refresh when controls change
+**Solution:** Ensure `renderTrigger` is set where needed:
+```typescript
+<CheckboxControl
+ // ... other props
+ renderTrigger // Add this for instant updates
+/>
+```
+
+### Issue: "Cannot read properties of undefined"
+**Problem:** Attempting to access undefined datasource or form_data
+**Solution:** Add safety checks and fallbacks:
+```typescript
+// Safety checks at component start
+if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+}
+
+// Safe array access
+const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
+```
+
+### Issue: Color Scheme Control Not Working
+**Problem:** ColorSchemeControl doesn't integrate properly
+**Solution:** Use the special Control wrapper pattern:
+```typescript
+{(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ 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')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+})()}
+```
+
+## Migration Checklist
+
+### Pre-Migration
+- [ ] Identify all controls in the legacy control panel
+- [ ] Note any conditional control visibility rules
+- [ ] Check for custom control configurations
+- [ ] Understand the chart's specific requirements
+
+### During Migration
+- [ ] Create new `[ChartName]ControlPanelSimple.tsx` file
+- [ ] Implement component structure with proper interface
+- [ ] Map all legacy controls to React components
+- [ ] Add safety checks for datasource/form_data
+- [ ] Implement tab structure (Data/Customize)
+- [ ] Add all control defaults to `controlOverrides`
+- [ ] Mark component as modern with `isModernPanel = true`
+- [ ] Update chart plugin to import new control panel
+
+### Post-Migration Testing
+- [ ] Test all control interactions
+- [ ] Verify chart updates on control changes
+- [ ] Check conditional control visibility
+- [ ] Validate default values
+- [ ] Test with different datasources
+- [ ] Run pre-commit hooks: `pre-commit run`
+- [ ] Test in Explore and Dashboard contexts
+
+## Advanced Patterns
+
+### Dynamic Control Visibility
+```typescript
+// Show additional controls based on current selection
+{formValues.chart_type === 'pie' && (
+ <div>
+ {/* Pie-specific controls */}
+ <CheckboxControl
+ label={t('Show as Donut')}
+ value={formValues.donut ?? false}
+ onChange={handleChange('donut')}
+ />
+
+ {formValues.donut && (
+ <SliderControl
+ value={formValues.innerRadius || 30}
+ onChange={handleChange('innerRadius')}
+ {...{ min: 0, max: 100, step: 1 }}
+ />
+ )}
+ </div>
+)}
+```
+
+### Complex Control Groups
+```typescript
+{/* Side-by-side controls */}
+<div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader label={t('Min Value')} />
+ <TextControl
+ value={formValues.min_value}
+ onChange={handleChange('min_value')}
+ isFloat
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader label={t('Max Value')} />
+ <TextControl
+ value={formValues.max_value}
+ onChange={handleChange('max_value')}
+ isFloat
+ />
+ </div>
+</div>
+```
+
+### Custom Validation
+```typescript
+// Add validation logic to handleChange
+const handleChange = (field: string) => (val: any) => {
+ // Custom validation
+ if (field === 'row_limit' && val && val < 1) {
+ console.warn('Row limit must be positive');
+ return;
+ }
+
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+};
+```
+
+## Reference Implementation
+
+The Pie chart control panel migration (`PieControlPanelSimple.tsx`) serves as the definitive reference implementation showing:
+
+- Complete tab-based layout (Data/Customize)
+- All major control types (DndColumnSelect, CheckboxControl, SelectControl, SliderControl, etc.)
+- Conditional control rendering
+- Proper safety checks and error handling
+- Color scheme integration
+- Control grouping and organization
+- Modern React patterns and TypeScript usage
+
+Study this implementation for best practices and patterns that can be applied to any chart control panel migration.
+
+## Summary
+
+The new React-based control panel approach provides:
+
+1. **Better Developer Experience** - Direct React components with TypeScript
+2. **Improved Maintainability** - Clear component structure and patterns
+3. **Enhanced Flexibility** - Easy conditional rendering and dynamic controls
+4. **Type Safety** - Full TypeScript support with proper interfaces
+5. **Simplified Architecture** - No complex config intermediaries
+
+The migration process involves converting string references and config objects to direct React components, implementing proper safety checks, and organizing controls in a logical tab-based structure. The key is to maintain compatibility with the existing Superset infrastructure while providing a more modern and maintainable development experience.
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/FunnelControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/FunnelControlPanelSimple.tsx
new file mode 100644
index 0000000000..21dfe162c4
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/FunnelControlPanelSimple.tsx
@@ -0,0 +1,572 @@
+/**
+ * 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, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ CurrencyFormatControl,
+ D3_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+} from '@superset-ui/chart-controls';
+
+// Direct component imports
+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 SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+
+import { EchartsFunnelLabelTypeType, PercentCalcType } from './types';
+
+interface FunnelControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+export const FunnelControlPanel: FC<FunnelControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // Tab state - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // Safety checks for datasource
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure safe data structures
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper for control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ const dataTabContent = (
+ <div>
+ {/* Group By */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('The metric for calculation')}
+ hovered
+ />
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters for the dataset')}
+ hovered
+ />
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ </div>
+
+ {/* Row Limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limits the number of rows')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="10"
+ controlId="row_limit"
+ />
+ </div>
+
+ {/* Sort by Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <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')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Percent Calculation Type */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('% calculation')}
+ description={t(
+ 'Display percents in the label and tooltip as the percent of the total value, from the first step of the funnel, or from the previous step in the funnel.',
+ )}
+ hovered
+ />
+ <SelectControl
+ value={
+ formValues.percent_calculation_type || PercentCalcType.FirstStep
+ }
+ onChange={handleChange('percent_calculation_type')}
+ choices={[
+ [PercentCalcType.FirstStep, t('Calculate from first step')],
+ [PercentCalcType.PreviousStep, t('Calculate from previous step')],
+ [PercentCalcType.Total, t('Percent of total')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ );
+
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ 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')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+ </div>
+
+ {/* Legend Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Legend')}</h4>
+
+ {/* Show Legend */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Legend Type - conditional */}
+ {formValues.show_legend && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Legend Type')}
+ description={t('Legend type')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.legendType || 'scroll'}
+ onChange={handleChange('legendType')}
+ choices={[
+ ['scroll', t('Scroll')],
+ ['plain', t('Plain')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Legend Orientation - conditional */}
+ {formValues.show_legend && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Legend Orientation')}
+ description={t('Legend Orientation')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.legendOrientation || 'top'}
+ onChange={handleChange('legendOrientation')}
+ choices={[
+ ['top', t('Top')],
+ ['bottom', t('Bottom')],
+ ['left', t('Left')],
+ ['right', t('Right')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Legend Margin - conditional */}
+ {formValues.show_legend && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Legend Margin')}
+ description={t('Additional padding for legend.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.legendMargin}
+ onChange={handleChange('legendMargin')}
+ isInt
+ controlId="legendMargin"
+ />
+ </div>
+ )}
+ </div>
+
+ {/* Labels Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Labels')}</h4>
+
+ {/* Label Contents */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Label Contents')}
+ description={t('What should be shown as the label')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.label_type || EchartsFunnelLabelTypeType.Key}
+ onChange={handleChange('label_type')}
+ choices={[
+ [EchartsFunnelLabelTypeType.Key, t('Category Name')],
+ [EchartsFunnelLabelTypeType.Value, t('Value')],
+ [EchartsFunnelLabelTypeType.Percent, t('Percentage')],
+ [EchartsFunnelLabelTypeType.KeyValue, t('Category and Value')],
+ [
+ EchartsFunnelLabelTypeType.KeyPercent,
+ t('Category and Percentage'),
+ ],
+ [
+ EchartsFunnelLabelTypeType.KeyValuePercent,
+ t('Category, Value and Percentage'),
+ ],
+ [
+ EchartsFunnelLabelTypeType.ValuePercent,
+ t('Value and Percentage'),
+ ],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Tooltip Contents */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Tooltip Contents')}
+ description={t('What should be shown as the tooltip label')}
+ hovered
+ />
+ <SelectControl
+ value={
+ formValues.tooltip_label_type ||
+ EchartsFunnelLabelTypeType.KeyValuePercent
+ }
+ onChange={handleChange('tooltip_label_type')}
+ choices={[
+ [EchartsFunnelLabelTypeType.Key, t('Category Name')],
+ [EchartsFunnelLabelTypeType.Value, t('Value')],
+ [EchartsFunnelLabelTypeType.Percent, t('Percentage')],
+ [EchartsFunnelLabelTypeType.KeyValue, t('Category and Value')],
+ [
+ EchartsFunnelLabelTypeType.KeyPercent,
+ t('Category and Percentage'),
+ ],
+ [
+ EchartsFunnelLabelTypeType.KeyValuePercent,
+ t('Category, Value and Percentage'),
+ ],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Number Format */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ hovered
+ />
+ <SelectControl
+ value={formValues.number_format || 'SMART_NUMBER'}
+ onChange={handleChange('number_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Currency Format */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const currencyControl = CurrencyFormatControl();
+ const { hidden, ...cleanConfig } = currencyControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="currency_format"
+ value={formValues.currency_format}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('currency_format')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Show Labels */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Labels')}
+ description={t('Whether to display the labels.')}
+ value={formValues.show_labels ?? true}
+ onChange={handleChange('show_labels')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Tooltip Labels */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Tooltip Labels')}
+ description={t('Whether to display the tooltip labels.')}
+ value={formValues.show_tooltip_labels ?? true}
+ onChange={handleChange('show_tooltip_labels')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ const tabItems = [
+ { key: 'data', label: t('Data'), children: dataTabContent },
+ { key: 'customize', label: t('Customize'), children: customizeTabContent },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// CRITICAL: Mark as modern panel
+(FunnelControlPanel as any).isModernPanel = true;
+
+// Export wrapper config for compatibility
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[FunnelControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ groupby: {
+ default: [],
+ label: t('Group by'),
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 10,
+ label: t('Row limit'),
+ renderTrigger: true,
+ },
+ sort_by_metric: {
+ default: true,
+ label: t('Sort by metric'),
+ renderTrigger: true,
+ },
+ percent_calculation_type: {
+ default: PercentCalcType.FirstStep,
+ label: t('% calculation'),
+ renderTrigger: true,
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ 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,
+ },
+ legendMargin: {
+ default: null,
+ label: t('Legend margin'),
+ renderTrigger: true,
+ },
+ label_type: {
+ default: EchartsFunnelLabelTypeType.Key,
+ label: t('Label type'),
+ renderTrigger: true,
+ },
+ tooltip_label_type: {
+ default: EchartsFunnelLabelTypeType.KeyValuePercent,
+ label: t('Tooltip label type'),
+ renderTrigger: true,
+ },
+ number_format: {
+ default: 'SMART_NUMBER',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: null,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ show_labels: {
+ default: true,
+ label: t('Show labels'),
+ renderTrigger: true,
+ },
+ show_tooltip_labels: {
+ default: true,
+ label: t('Show tooltip labels'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
index 0d3948e207..e8c1f053d2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
@@ -18,7 +18,7 @@
*/
import { Behavior, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './FunnelControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example from './images/example.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx
index 8e6a1f02cf..39c35854bb 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/PieControlPanelSimple.tsx
@@ -57,6 +57,9 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({
actions,
controls,
}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
// If no valid data yet, show loading state
if (!datasource || !form_data) {
return <div>Loading control panel...</div>;
@@ -88,9 +91,6 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({
// Get form values
const formValues = form_data || value || {};
- // State for active tab
- const [activeTab, setActiveTab] = useState('data');
-
// Data tab content
const dataTabContent = (
<div>
@@ -509,7 +509,7 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
{t('Pie Chart')}
</div>
- <div style={{ fontSize: '12px', color: '#666' }}>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
{t('Visualize data as a pie or donut chart')}
</div>
</div>
@@ -528,7 +528,7 @@ export const PieControlPanel: FC<PieControlPanelProps> = ({
// Mark this component as a modern panel
(PieControlPanel as any).isModernPanel = true;
-// Provide a minimal config structure to prevent errors
+// Create a config that wraps our React component
const config = {
controlPanelSections: [
{
@@ -537,7 +537,6 @@ const config = {
controlSetRows: [[PieControlPanel as any]],
},
],
- // Provide default control overrides
controlOverrides: {
groupby: {
default: [],
@@ -647,4 +646,4 @@ const config = {
},
};
-export default config;
\ No newline at end of file
+export default config;