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;