Compare commits

..

2 Commits

Author SHA1 Message Date
Maxime Beauchemin
5d32c8834d touch file to trigger 2025-07-23 01:18:21 -07:00
Maxime Beauchemin
40164300e5 feat: optimize setup-backend to go faster on py 3.12 2025-07-23 01:13:36 -07:00
753 changed files with 12991 additions and 54485 deletions

View File

@@ -1,40 +0,0 @@
# Claude Code RC for move-controls
This is a claudette-managed Apache Superset development environment.
## Project: move-controls
- Worktree Path: /Users/evan_1/.claudette/worktrees/move-controls
- Frontend Port: 9004
- Frontend URL: http://localhost:9004
## Quick Commands
Start services:
```bash
claudette docker up
```
Access frontend:
```bash
open http://localhost:9004
```
Run tests:
```bash
# Backend
pytest tests/unit_tests/
# Frontend
cd superset-frontend && npm test
```
## Environment Details
- Python venv: `.venv/` (auto-activated in claudette shell)
- Node modules: `superset-frontend/node_modules/`
- Docker prefix: `move-controls_`
## Development Tips
- Always use `claudette shell` to work in this project
- Run `pre-commit run --all-files` before committing
- Use `claudette docker` instead of docker-compose directly
- The frontend dev server runs on port 9004 to avoid conflicts

View File

@@ -1,20 +0,0 @@
# Keep this in sync with the base image in the main Dockerfile (ARG PY_VER)
FROM python:3.11.13-bookworm AS base
# Install system dependencies that Superset needs
# This layer will be cached across Codespace sessions
RUN apt-get update && apt-get install -y \
libsasl2-dev \
libldap2-dev \
libpq-dev \
tmux \
gh \
&& rm -rf /var/lib/apt/lists/*
# Install uv for fast Python package management
# This will also be cached in the image
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
echo 'export PATH="/root/.cargo/bin:$PATH"' >> /etc/bash.bashrc
# Set the cargo/bin directory in PATH for all users
ENV PATH="/root/.cargo/bin:${PATH}"

View File

@@ -1,16 +0,0 @@
# Superset Development with GitHub Codespaces
For complete documentation on using GitHub Codespaces with Apache Superset, please see:
**[Setting up a Development Environment - GitHub Codespaces](https://superset.apache.org/docs/contributing/development#github-codespaces-cloud-development)**
## Pre-installed Development Environment
When you create a new Codespace from this repository, it automatically:
1. **Creates a Python virtual environment** using `uv venv`
2. **Installs all development dependencies** via `uv pip install -r requirements/development.txt`
3. **Sets up pre-commit hooks** with `pre-commit install`
4. **Activates the virtual environment** automatically in all terminals
The virtual environment is located at `/workspaces/{repository-name}/.venv` and is automatically activated through environment variables set in the devcontainer configuration.

View File

@@ -1,62 +0,0 @@
# Superset Codespaces environment setup
# This file is appended to ~/.bashrc during Codespace setup
# Find the workspace directory (handles both 'superset' and 'superset-2' names)
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
if [ -n "$WORKSPACE_DIR" ]; then
# Check if virtual environment exists
if [ -d "$WORKSPACE_DIR/.venv" ]; then
# Activate the virtual environment
source "$WORKSPACE_DIR/.venv/bin/activate"
echo "✅ Python virtual environment activated"
# Verify pre-commit is installed and set up
if command -v pre-commit &> /dev/null; then
echo "✅ pre-commit is available ($(pre-commit --version))"
# Install git hooks if not already installed
if [ -d "$WORKSPACE_DIR/.git" ] && [ ! -f "$WORKSPACE_DIR/.git/hooks/pre-commit" ]; then
echo "🪝 Installing pre-commit hooks..."
cd "$WORKSPACE_DIR" && pre-commit install
fi
else
echo "⚠️ pre-commit not found. Run: pip install pre-commit"
fi
else
echo "⚠️ Python virtual environment not found at $WORKSPACE_DIR/.venv"
echo " Run: cd $WORKSPACE_DIR && .devcontainer/setup-dev.sh"
fi
# Always cd to the workspace directory for convenience
cd "$WORKSPACE_DIR"
fi
# Add helpful aliases for Superset development
alias start-superset="$WORKSPACE_DIR/.devcontainer/start-superset.sh"
alias setup-dev="$WORKSPACE_DIR/.devcontainer/setup-dev.sh"
# Show helpful message on login
echo ""
echo "🚀 Superset Codespaces Environment"
echo "=================================="
# Check if Superset is running
if docker ps 2>/dev/null | grep -q "superset"; then
echo "✅ Superset is running!"
echo " - Check the 'Ports' tab for your live Superset URL"
echo " - Initial startup takes 10-20 minutes"
echo " - Login: admin/admin"
else
echo "⚠️ Superset is not running. Use: start-superset"
# Check if there's a startup log
if [ -f "/tmp/superset-startup.log" ]; then
echo " 📋 Startup log found: cat /tmp/superset-startup.log"
fi
fi
echo ""
echo "Quick commands:"
echo " start-superset - Start Superset with Docker Compose"
echo " setup-dev - Set up Python environment (if not already done)"
echo " pre-commit run - Run pre-commit checks on staged files"
echo ""

View File

@@ -1,20 +0,0 @@
#!/bin/bash
# Script to build and push the devcontainer image to GitHub Container Registry
# This allows caching the image between Codespace sessions
# You'll need to run this with appropriate GitHub permissions
# gh auth login --scopes write:packages
REGISTRY="ghcr.io"
OWNER="apache"
REPO="superset"
TAG="devcontainer-base"
echo "Building devcontainer image..."
docker build -t $REGISTRY/$OWNER/$REPO:$TAG .devcontainer/
echo "Pushing to GitHub Container Registry..."
docker push $REGISTRY/$OWNER/$REPO:$TAG
echo "Done! Update .devcontainer/devcontainer.json to use:"
echo " \"image\": \"$REGISTRY/$OWNER/$REPO:$TAG\""

View File

@@ -1,66 +0,0 @@
{
"name": "Apache Superset Development",
// Option 1: Use pre-built image directly
// "image": "ghcr.io/apache/superset:devcontainer-base",
// Option 2: Build from Dockerfile with cache (current approach)
"build": {
"dockerfile": "Dockerfile",
"context": ".",
// Cache from the Apache registry image
"cacheFrom": ["ghcr.io/apache/superset:devcontainer-base"]
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true,
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"configureZshAsDefaultShell": true
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
},
// Forward ports for development
"forwardPorts": [9001],
"portsAttributes": {
"9001": {
"label": "Superset (via Webpack Dev Server)",
"onAutoForward": "notify",
"visibility": "public"
}
},
// Run commands after container is created
"postCreateCommand": "bash .devcontainer/setup-dev.sh || echo '⚠️ Setup had issues - run .devcontainer/setup-dev.sh manually'",
// Auto-start Superset after ensuring Docker is ready
// Run in foreground to see any errors, but don't block on failures
"postStartCommand": "bash -c 'echo \"Waiting 30s for services to initialize...\"; sleep 30; .devcontainer/start-superset.sh || echo \"⚠️ Auto-start failed - run start-superset manually\"'",
// Set environment variables
"remoteEnv": {
// Removed automatic venv activation to prevent startup issues
// The setup script will handle this
},
// VS Code customizations
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}

View File

@@ -1,78 +0,0 @@
#!/bin/bash
# Setup script for Superset Codespaces development environment
echo "🔧 Setting up Superset development environment..."
# System dependencies and uv are now pre-installed in the Docker image
# This speeds up Codespace creation significantly!
# Create virtual environment using uv
echo "🐍 Creating Python virtual environment..."
if ! uv venv; then
echo "❌ Failed to create virtual environment"
exit 1
fi
# Install Python dependencies
echo "📦 Installing Python dependencies..."
if ! uv pip install -r requirements/development.txt; then
echo "❌ Failed to install Python dependencies"
echo "💡 You may need to run this manually after the Codespace starts"
exit 1
fi
# Install pre-commit hooks
echo "🪝 Installing pre-commit hooks..."
if source .venv/bin/activate && pre-commit install; then
echo "✅ Pre-commit hooks installed"
else
echo "⚠️ Pre-commit hooks installation failed (non-critical)"
fi
# Install Claude Code CLI via npm
echo "🤖 Installing Claude Code..."
if npm install -g @anthropic-ai/claude-code; then
echo "✅ Claude Code installed"
else
echo "⚠️ Claude Code installation failed (non-critical)"
fi
# Make the start script executable
chmod +x .devcontainer/start-superset.sh
# Add bashrc additions for automatic venv activation
echo "🔧 Setting up automatic environment activation..."
if [ -f ~/.bashrc ]; then
# Check if we've already added our additions
if ! grep -q "Superset Codespaces environment setup" ~/.bashrc; then
echo "" >> ~/.bashrc
cat .devcontainer/bashrc-additions >> ~/.bashrc
echo "✅ Added automatic venv activation to ~/.bashrc"
else
echo "✅ Bashrc additions already present"
fi
else
# Create bashrc if it doesn't exist
cat .devcontainer/bashrc-additions > ~/.bashrc
echo "✅ Created ~/.bashrc with automatic venv activation"
fi
# Also add to zshrc since that's the default shell
if [ -f ~/.zshrc ] || [ -n "$ZSH_VERSION" ]; then
if ! grep -q "Superset Codespaces environment setup" ~/.zshrc; then
echo "" >> ~/.zshrc
cat .devcontainer/bashrc-additions >> ~/.zshrc
echo "✅ Added automatic venv activation to ~/.zshrc"
fi
fi
echo "✅ Development environment setup complete!"
echo ""
echo "📝 The virtual environment will be automatically activated in new terminals"
echo ""
echo "🔄 To activate in this terminal, run:"
echo " source ~/.bashrc"
echo ""
echo "🚀 To start Superset:"
echo " start-superset"
echo ""

View File

@@ -1,108 +0,0 @@
#!/bin/bash
# Startup script for Superset in Codespaces
# Log to a file for debugging
LOG_FILE="/tmp/superset-startup.log"
echo "[$(date)] Starting Superset startup script" >> "$LOG_FILE"
echo "[$(date)] User: $(whoami), PWD: $(pwd)" >> "$LOG_FILE"
echo "🚀 Starting Superset in Codespaces..."
echo "🌐 Frontend will be available at port 9001"
# Find the workspace directory (Codespaces clones as 'superset', not 'superset-2')
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
if [ -n "$WORKSPACE_DIR" ]; then
cd "$WORKSPACE_DIR"
echo "📁 Working in: $WORKSPACE_DIR"
else
echo "📁 Using current directory: $(pwd)"
fi
# Wait for Docker to be available
echo "⏳ Waiting for Docker to start..."
echo "[$(date)] Waiting for Docker..." >> "$LOG_FILE"
max_attempts=30
attempt=0
while ! docker info > /dev/null 2>&1; do
if [ $attempt -eq $max_attempts ]; then
echo "❌ Docker failed to start after $max_attempts attempts"
echo "[$(date)] Docker failed to start after $max_attempts attempts" >> "$LOG_FILE"
echo "🔄 Please restart the Codespace or run this script manually later"
exit 1
fi
echo " Attempt $((attempt + 1))/$max_attempts..."
echo "[$(date)] Docker check attempt $((attempt + 1))/$max_attempts" >> "$LOG_FILE"
sleep 2
attempt=$((attempt + 1))
done
echo "✅ Docker is ready!"
echo "[$(date)] Docker is ready" >> "$LOG_FILE"
# Check if Superset containers are already running
if docker ps | grep -q "superset"; then
echo "✅ Superset containers are already running!"
echo ""
echo "🌐 To access Superset:"
echo " 1. Click the 'Ports' tab at the bottom of VS Code"
echo " 2. Find port 9001 and click the globe icon to open"
echo " 3. Wait 10-20 minutes for initial startup"
echo ""
echo "📝 Login credentials: admin/admin"
exit 0
fi
# Clean up any existing containers
echo "🧹 Cleaning up existing containers..."
docker-compose -f docker-compose-light.yml down
# Start services
echo "🏗️ Starting Superset in background (daemon mode)..."
echo ""
# Start in detached mode
docker-compose -f docker-compose-light.yml up -d
echo ""
echo "✅ Docker Compose started successfully!"
echo ""
echo "📋 Important information:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "⏱️ Initial startup takes 10-20 minutes"
echo "🌐 Check the 'Ports' tab for your Superset URL (port 9001)"
echo "👤 Login: admin / admin"
echo ""
echo "📊 Useful commands:"
echo " docker-compose -f docker-compose-light.yml logs -f # Follow logs"
echo " docker-compose -f docker-compose-light.yml ps # Check status"
echo " docker-compose -f docker-compose-light.yml down # Stop services"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💤 Keeping terminal open for 60 seconds to test persistence..."
sleep 60
echo "✅ Test complete - check if this terminal is still visible!"
# Show final status
docker-compose -f docker-compose-light.yml ps
EXIT_CODE=$?
# If it failed, provide helpful instructions
if [ $EXIT_CODE -ne 0 ] && [ $EXIT_CODE -ne 130 ]; then # 130 is Ctrl+C
echo ""
echo "❌ Superset startup failed (exit code: $EXIT_CODE)"
echo ""
echo "🔄 To restart Superset, run:"
echo " .devcontainer/start-superset.sh"
echo ""
echo "🔧 For troubleshooting:"
echo " # View logs:"
echo " docker-compose -f docker-compose-light.yml logs"
echo ""
echo " # Clean restart (removes volumes):"
echo " docker-compose -f docker-compose-light.yml down -v"
echo " .devcontainer/start-superset.sh"
echo ""
echo " # Common issues:"
echo " - Network timeouts: Just retry, often transient"
echo " - Port conflicts: Check 'docker ps'"
echo " - Database issues: Try clean restart with -v"
fi

View File

@@ -28,7 +28,6 @@ runs:
if [ "${{ inputs.python-version }}" = "current" ]; then
echo "PYTHON_VERSION=3.11" >> $GITHUB_ENV
elif [ "${{ inputs.python-version }}" = "next" ]; then
# currently disabled in GHA matrixes because of library compatibility issues
echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV
elif [ "${{ inputs.python-version }}" = "previous" ]; then
echo "PYTHON_VERSION=3.10" >> $GITHUB_ENV
@@ -40,7 +39,17 @@ runs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: ${{ inputs.cache }}
- name: Cache uv packages
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-python${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements/development.txt', 'requirements/base.txt') }}
restore-keys: |
uv-${{ runner.os }}-python${{ env.PYTHON_VERSION }}-
- name: Install dependencies
env:
UV_CACHE_DIR: ~/.cache/uv
UV_PREFER_BINARY: "1"
run: |
if [ "${{ inputs.install-superset }}" = "true" ]; then
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
@@ -48,11 +57,11 @@ runs:
pip install --upgrade pip setuptools wheel uv
if [ "${{ inputs.requirements-type }}" = "dev" ]; then
uv pip install --system -r requirements/development.txt
uv pip install --system --prefer-binary -r requirements/development.txt
elif [ "${{ inputs.requirements-type }}" = "base" ]; then
uv pip install --system -r requirements/base.txt
uv pip install --system --prefer-binary -r requirements/base.txt
fi
uv pip install --system -e .
uv pip install --system --prefer-binary -e .
fi
shell: bash

View File

@@ -51,7 +51,7 @@ jobs:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov-report= --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@v5
with:

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Welcome Message
uses: actions/first-interaction@v2
uses: actions/first-interaction@v1
continue-on-error: true
with:
repo-token: ${{ github.token }}

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ __pycache__
.cache
.bento*
.cache-loader
.claude_rc
.coverage
cover
.DS_Store

View File

@@ -52,6 +52,14 @@ repos:
- id: trailing-whitespace
exclude: ^.*\.(snap)
args: ["--markdown-linebreak-ext=md"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 # Use the sha or tag you want to point at
hooks:
- id: prettier
additional_dependencies:
- prettier@3.5.3
args: ["--ignore-path=./superset-frontend/.prettierignore", "--exclude", "site-packages"]
files: "superset-frontend"
- repo: local
hooks:
- id: eslint-frontend
@@ -68,7 +76,7 @@ repos:
files: ^docs/.*\.(js|jsx|ts|tsx)$
- id: type-checking-frontend
name: Type-Checking (Frontend)
entry: ./scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base
entry: bash -c './scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base'
language: system
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
exclude: ^superset-frontend/cypress-base\/
@@ -89,9 +97,9 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
- id: ruff-format
- repo: local
hooks:
- id: pylint
@@ -105,10 +113,9 @@ repos:
- |
TARGET_BRANCH=${GITHUB_BASE_REF:-master}
git fetch origin "$TARGET_BRANCH"
BASE=$(git merge-base origin/"$TARGET_BRANCH" HEAD)
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD | grep '^superset/.*\.py$' || true)
files=$(git diff --name-only --diff-filter=ACM origin/"$TARGET_BRANCH"..HEAD | grep '^superset/.*\.py$' || true)
if [ -n "$files" ]; then
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint --reports=no $files
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint $files
else
echo "No Python files to lint."
fi

View File

@@ -1,802 +0,0 @@
# 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);
}
};
```
## Additional Migration Patterns
### Single Column Selection
When a control expects a single column value (not an array):
```typescript
// For Sankey source/target columns
const handleSingleColumnChange = (field: string) => (val: any) => {
const singleValue = Array.isArray(val) ? val[0] : val;
actions.setControlValue(field, singleValue);
};
// Usage
<DndColumnSelect
value={formValues.source ? [formValues.source] : []}
onChange={handleSingleColumnChange('source')}
options={safeColumns}
multi={false}
/>
```
### Required Field Validation
For controls that must have values:
```typescript
import { validateNonEmpty } from '@superset-ui/core';
// In controlOverrides
source: {
validators: [validateNonEmpty],
label: t('Source Column'),
},
```
### Chart Type Descriptions
Add helpful descriptions at the top of control panels:
```typescript
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
{t('Sankey Diagram')}
</div>
<div style={{ fontSize: '12px', opacity: 0.65 }}>
{t('Visualize flow between different entities')}
</div>
</div>
```
## Common Migration Issues & Solutions
### Issue 1: "Cannot read properties of undefined (reading 'map')"
**Problem:** DndColumnSelect crashes when datasource is undefined
**Solution:** Pass `options={datasource?.columns || []}` instead of `datasource={datasource}`
### Issue 2: Tabs import error ("Element type is invalid")
**Problem:** Runtime error when loading control panel
**Solution:** Import from 'antd' directly: `import { Tabs } from 'antd';` (NOT from '@superset-ui/core')
### Issue 3: React hooks error
**Problem:** "React Hook 'useState' is called conditionally"
**Solution:** Always declare state hooks before any conditional returns:
```typescript
const [activeTab, setActiveTab] = useState('data'); // FIRST
if (!datasource) return <div>Loading...</div>; // THEN conditions
```
### Issue 4: ESLint color literal warnings
**Problem:** theme-colors/no-literal-colors ESLint rule
**Solution:** Use opacity instead of color literals:
```typescript
// Bad: style={{ color: '#666' }}
// Good: style={{ opacity: 0.65 }}
```
### Issue 5: Single value vs array handling
**Problem:** Some controls expect single values but DndColumnSelect returns arrays
**Solution:** See "Single Column Selection" pattern above
### Issue 6: antd import warnings
**Problem:** "'antd' should be listed in the project's dependencies"
**Solution:** Use `SKIP=eslint-frontend` when committing if antd is already available
## 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.

View File

@@ -1,153 +0,0 @@
# Control Panel Modernization Guide
## Current State
Apache Superset's control panels currently use a legacy `controlSetRows` structure that relies on nested arrays to define layout. This approach has several limitations:
1. **Rigid Layout**: The nested array structure makes it difficult to create responsive or complex layouts
2. **Poor Type Safety**: Arrays of arrays don't provide good TypeScript support
3. **Mixed Paradigms**: String references, configuration objects, and React components are mixed together
4. **Limited Reusability**: Layout logic is embedded in the structure rather than using composable components
## Migration Strategy
### Phase 1: Component Modernization ✅ COMPLETED
- Replaced string-based control references with React components
- Updated individual control components to use Ant Design
- Modernized the `ControlRow` component to use Ant Design's Grid
### Phase 2: Layout Utilities ✅ COMPLETED
- Created `ControlPanelLayout.tsx` with reusable layout components
- Implemented `ControlSection`, `SingleControlRow`, `TwoColumnRow`, `ThreeColumnRow`
- Updated control group components to use Ant Design Row/Col
### Phase 3: React-Based Control Panels 🚧 IN PROGRESS
- Create `ReactControlPanel` component for rendering modern panels
- Support both legacy and modern formats during transition
- Provide migration helpers and examples
### Phase 4: Gradual Migration 📋 TODO
- Migrate chart control panels one by one
- Start with simpler charts (Pie, Bar) before complex ones
- Maintain backward compatibility throughout
## Modern Control Panel Structure
### Legacy Structure (controlSetRows)
```typescript
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[GroupByControl()],
[MetricControl()],
[AdhocFiltersControl()],
[RowLimitControl()],
],
},
],
};
```
### Modern Structure (React Components)
```typescript
const modernConfig: ReactControlPanelConfig = {
sections: [
{
key: 'query',
label: t('Query'),
expanded: true,
render: ({ values, onChange }) => (
<>
<SingleControlRow>
<GroupByControl value={values.groupby} onChange={onChange} />
</SingleControlRow>
<SingleControlRow>
<MetricControl value={values.metrics} onChange={onChange} />
</SingleControlRow>
<TwoColumnRow
left={<AdhocFiltersControl value={values.adhoc_filters} onChange={onChange} />}
right={<RowLimitControl value={values.row_limit} onChange={onChange} />}
/>
</>
),
},
],
};
```
## Benefits of Modernization
1. **Better Type Safety**: Full TypeScript support with proper interfaces
2. **Flexible Layouts**: Use Ant Design's Grid system for responsive layouts
3. **Cleaner Code**: React components instead of nested arrays
4. **Improved DX**: Better IDE support and autocomplete
5. **Easier Testing**: Component-based architecture is easier to test
6. **Consistent Styling**: Leverage Ant Design's theme system
## Migration Example
To migrate a control panel:
1. **Create a modern version** alongside the existing one:
```typescript
// controlPanelModern.tsx
export const modernConfig: ReactControlPanelConfig = {
sections: [/* ... */]
};
```
2. **Use the compatibility wrapper** for backward compatibility:
```typescript
export default createReactControlPanel(modernConfig);
```
3. **Update the chart plugin** to use the new control panel:
```typescript
import controlPanel from './controlPanelModern';
```
## Layout Components Available
- `ControlSection`: Collapsible section container
- `SingleControlRow`: Full-width single control
- `TwoColumnRow`: Two controls side by side (50/50)
- `ThreeColumnRow`: Three controls in a row (33/33/33)
- `Row` and `Col` from Ant Design for custom layouts
## Files Created
1. `packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx`
- Layout utility components
2. `packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx`
- Example of modern control panel structure
3. `plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx`
- Modern version of Pie chart control panel
## Next Steps
1. **Complete the ReactControlPanel integration** with ControlPanelsContainer
2. **Create migration tooling** to help convert existing panels
3. **Document best practices** for control panel design
4. **Update chart plugin template** to use modern structure
5. **Gradually migrate all 90+ control panels** in the codebase
## Technical Debt Addressed
- Eliminates nested array layout structure
- Removes string-based control references
- Reduces coupling between layout and configuration
- Improves maintainability and testability
- Enables better code splitting and lazy loading
## Backward Compatibility
The migration maintains full backward compatibility:
- Existing control panels continue to work
- Both formats can coexist during migration
- No breaking changes to the public API
- Charts can be migrated incrementally

View File

@@ -59,7 +59,7 @@ RUN mkdir -p /app/superset/static/assets \
# NOTE: we mount packages and plugins as they are referenced in package.json as workspaces
# ideally we'd COPY only their package.json. Here npm ci will be cached as long
# as the full content of these folders don't change, yielding a decent cache reuse rate.
# Note that it's not possible to selectively COPY or mount using blobs.
# Note that's it's not possible selectively COPY of mount using blobs.
RUN --mount=type=bind,source=./superset-frontend/package.json,target=./package.json \
--mount=type=bind,source=./superset-frontend/package-lock.json,target=./package-lock.json \
--mount=type=cache,target=/root/.cache \
@@ -74,7 +74,7 @@ RUN --mount=type=bind,source=./superset-frontend/package.json,target=./package.j
COPY superset-frontend /app/superset-frontend
######################################################################
# superset-node is used for compiling frontend assets
# superset-node used for compile frontend assets
######################################################################
FROM superset-node-ci AS superset-node
@@ -90,7 +90,7 @@ RUN --mount=type=cache,target=/root/.npm \
# Copy translation files
COPY superset/translations /app/superset/translations
# Build translations if enabled, then cleanup localization files
# Build the frontend if not in dev mode
RUN if [ "$BUILD_TRANSLATIONS" = "true" ]; then \
npm run build-translation; \
fi; \

119
LLMS.md
View File

@@ -22,11 +22,6 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
- **MyPy compliance** - Run `pre-commit run mypy` to validate
- **SQLAlchemy typing** - Use proper model annotations
### UUID Migration
- **Prefer UUIDs over auto-incrementing IDs** - New models should use UUID primary keys
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
## Key Directories
```
@@ -94,10 +89,6 @@ superset/
- **`selectOption()`** - Select component helper
- **React Testing Library** - NO Enzyme (removed)
### Test Database Patterns
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
- **API tests**: Update expected columns when adding new model fields
### Running Tests
```bash
# Frontend
@@ -129,10 +120,6 @@ curl -f http://localhost:8088/health || echo "❌ Setup required - see https://s
- `pyproject.toml` - Python tooling (ruff, mypy configs)
- `requirements/` folder - Python dependencies (base.txt, development.txt)
## SQLAlchemy Query Best Practices
- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
- **Example**: `~Model.is_active` instead of `Model.is_active == False`
## Pre-commit Validation
**Use pre-commit hooks for quality validation:**
@@ -141,43 +128,13 @@ curl -f http://localhost:8088/health || echo "❌ Setup required - see https://s
# Install hooks
pre-commit install
# IMPORTANT: Stage your changes first!
git add . # Pre-commit only checks staged files
# Quick validation (faster than --all-files)
pre-commit run # Staged files only
pre-commit run # Staged files only
pre-commit run mypy # Python type checking
pre-commit run prettier # Code formatting
pre-commit run eslint # Frontend linting
```
**Important pre-commit usage notes:**
- **Stage files first**: Run `git add .` before `pre-commit run` to check only changed files (much faster)
- **Virtual environment**: Activate your Python virtual environment before running pre-commit
```bash
# Common virtual environment locations (yours may differ):
source .venv/bin/activate # if using .venv
source venv/bin/activate # if using venv
source ~/venvs/superset/bin/activate # if using a central location
```
If you get a "command not found" error, ask the user which virtual environment to activate
- **Auto-fixes**: Some hooks auto-fix issues (e.g., trailing whitespace). Re-run after fixes are applied
## Common File Patterns
### API Structure
- **`/api.py`** - REST endpoints with decorators and OpenAPI docstrings
- **`/schemas.py`** - Marshmallow validation schemas for OpenAPI spec
- **`/commands/`** - Business logic classes with @transaction() decorators
- **`/models/`** - SQLAlchemy database models
- **OpenAPI docs**: Auto-generated at `/swagger/v1` from docstrings and schemas
### Migration Files
- **Location**: `superset/migrations/versions/`
- **Naming**: `YYYY-MM-DD_HH-MM_hash_description.py`
- **Utilities**: Use helpers from `superset.migrations.shared.utils` for database compatibility
- **Pattern**: Import utilities instead of raw SQLAlchemy operations
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
@@ -189,77 +146,3 @@ pre-commit run eslint # Frontend linting
---
**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
## Control Panel Migration Guide
### New React-Based Control Panel Architecture
We've successfully migrated from the complex string-referenced control panel system to a simpler React-based approach. Here's how to migrate existing control panels:
#### Key Changes
1. **No more string references** - Use React component functions instead of strings
2. **Redux integration** - Controls connect directly to Redux via useFormData hook
3. **Type safety** - Full TypeScript support with proper types
4. **Simplified architecture** - Direct React components, no JSON intermediary
#### Migration Steps
##### 1. Update imports
Replace string references with component imports:
```typescript
// OLD
controlSetRows: [
['series'],
['metric'],
]
// NEW
import { SeriesControl, MetricControl } from '@superset-ui/chart-controls';
controlSetRows: [
[SeriesControl()],
[MetricControl()],
]
```
##### 2. Custom controls
Use InlineTextControl or InlineSelectControl for custom controls:
```typescript
// OLD
{
name: 'size_from',
config: { type: 'TextControl', ... }
}
// NEW
InlineTextControl('sizeFrom', {
label: t('Minimum Font Size'),
default: 10,
...
})
```
##### 3. Control naming
Use camelCase for new controls, but handle both in transformProps:
```typescript
// In transformProps.ts
const finalSizeFrom = sizeFrom ?? size_from ?? 10;
```
#### Available Control Components
All these return CustomControlItem and can be used in controlSetRows:
- `SeriesControl()` - Column selection
- `MetricControl()` - Metric selection
- `AdhocFiltersControl()` - Filter configuration
- `RowLimitControl()` - Row limit
- `ColorSchemeControl()` - Color scheme picker
- `InlineTextControl(name, config)` - Custom text input
- `InlineSelectControl(name, config)` - Custom select
#### Example Migration
See `plugins/plugin-chart-word-cloud/src/plugin/controlPanelFixed.ts` for a complete example.
# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.

View File

@@ -1,257 +0,0 @@
# Pie Chart Control Panel Migration - Phased Approach
## Phase 1: Parallel Implementation ✅ COMPLETED
We've created a modern control panel alongside the legacy one:
### Files Created:
1. **`controlPanelModern.tsx`** - Modern React-based control panel
2. **`ModernControlPanelRenderer.tsx`** - Bridge component for compatibility
3. **Updated `ControlPanelsContainer.tsx`** - Support for modern panels
### Key Features:
- Full React component structure (no `controlSetRows`)
- Uses Ant Design Grid directly
- Type-safe with TypeScript interfaces
- Conditional rendering based on form values
- Organized into logical sections
## Phase 2: Integration Testing 🚧 NEXT STEP
### 2.1 Update the Pie Chart Plugin
```typescript
// In plugins/plugin-chart-echarts/src/Pie/index.ts
import controlPanel from './controlPanel'; // Legacy
import controlPanelModern from './controlPanelModern'; // Modern
// Feature flag to toggle between old and new
const useModernPanel = window.featureFlags?.MODERN_CONTROL_PANELS;
export default class EchartsPieChartPlugin extends ChartPlugin {
constructor() {
super({
// ... other config
controlPanel: useModernPanel ? controlPanelModern : controlPanel,
});
}
}
```
### 2.2 Test the Modern Panel
Create test file to verify both panels produce same output:
```typescript
// controlPanel.test.tsx
describe('Pie Control Panel Migration', () => {
it('modern panel handles all legacy controls', () => {
// Test that all controls from legacy panel exist in modern
});
it('produces same form_data structure', () => {
// Verify form_data compatibility
});
it('visibility conditions work correctly', () => {
// Test conditional rendering
});
});
```
## Phase 3: Feature Flag Rollout
### 3.1 Add Feature Flag
```python
# In superset/config.py
FEATURE_FLAGS = {
"MODERN_CONTROL_PANELS": False, # Start disabled
}
```
### 3.2 Gradual Rollout
1. **Internal Testing**: Enable for development environment
2. **Beta Users**: Enable for select users (5%)
3. **Wider Rollout**: Increase to 50%
4. **Full Migration**: Enable for all users
5. **Cleanup**: Remove legacy code
## Phase 4: Migration Utilities
### 4.1 Control Panel Converter
```typescript
// convertLegacyPanel.ts
export function convertControlSetRows(rows: ControlSetRow[]): ReactElement {
return rows.map(row => {
if (row.length === 1) {
return <SingleControlRow>{convertControl(row[0])}</SingleControlRow>;
}
if (row.length === 2) {
return (
<TwoColumnRow
left={convertControl(row[0])}
right={convertControl(row[1])}
/>
);
}
// ... handle other cases
});
}
```
### 4.2 Common Patterns Library
```typescript
// commonPanelPatterns.tsx
export const QuerySection = ({ values, onChange }) => (
<>
<GroupByControl />
<MetricControl />
<AdhocFiltersControl />
<RowLimitControl />
</>
);
export const AppearanceSection = ({ values, onChange }) => (
<>
<ColorSchemeControl />
<OpacityControl />
<LegendControls />
</>
);
```
## Phase 5: Migrate Other Charts
### Priority Order (Simple to Complex):
1. **Simple Charts** (1-2 weeks each)
- Bar Chart
- Line Chart
- Area Chart
- Scatter Plot
2. **Medium Complexity** (2-3 weeks each)
- Table
- Pivot Table
- Heatmap
- Treemap
3. **Complex Charts** (3-4 weeks each)
- Mixed Time Series
- Box Plot
- Sankey
- Graph/Network
### Migration Checklist per Chart:
- [ ] Create `controlPanelModern.tsx`
- [ ] Update plugin index to support both
- [ ] Write migration tests
- [ ] Test with feature flag
- [ ] Document any chart-specific patterns
- [ ] Update TypeScript types if needed
## Phase 6: System-Wide Updates
### 6.1 Update Control Panel Registry
```typescript
// getChartControlPanelRegistry.ts
export interface ModernControlPanelRegistry {
get(key: string): ControlPanelConfig | ReactControlPanelConfig;
registerModern(key: string, config: ReactControlPanelConfig): void;
}
```
### 6.2 Update Explore Components
- `ControlPanelsContainer` - Full support for modern panels ✅
- `Control` - Ensure all control types work
- `ControlRow` - Already modernized ✅
- `getSectionsToRender` - Update to handle React components
### 6.3 Update Types
```typescript
// types.ts
export type ControlPanelConfig = LegacyControlPanelConfig | ModernControlPanelConfig;
export interface ModernControlPanelConfig {
type: 'modern';
sections: ReactControlPanelSection[];
controlOverrides?: ControlOverrides;
formDataOverrides?: FormDataOverrides;
}
```
## Benefits Tracking
### Metrics to Monitor:
1. **Developer Velocity**: Time to add new controls
2. **Bug Rate**: Control panel-related issues
3. **Performance**: Rendering time for control panels
4. **Type Safety**: TypeScript coverage percentage
5. **Code Maintainability**: Lines of code, complexity metrics
### Expected Improvements:
- 50% reduction in control panel code
- 80% reduction in control panel bugs
- 100% TypeScript coverage
- 30% faster control panel rendering
- Easier onboarding for new developers
## Rollback Plan
If issues arise:
1. **Feature Flag**: Immediately disable `MODERN_CONTROL_PANELS`
2. **Hotfix**: Revert to legacy panel for affected charts
3. **Investigation**: Debug issues in staging environment
4. **Fix Forward**: Address issues and re-enable gradually
## Timeline Estimate
- **Phase 1**: ✅ Completed
- **Phase 2**: 1 week (testing and integration)
- **Phase 3**: 2 weeks (feature flag and rollout)
- **Phase 4**: 1 week (utilities and patterns)
- **Phase 5**: 3-6 months (all charts migration)
- **Phase 6**: 2 weeks (system updates)
- **Cleanup**: 1 week (remove legacy code)
**Total: 4-7 months for complete migration**
## Next Immediate Steps
1. Test the modern Pie control panel in development
2. Fix any issues with value binding and onChange handlers
3. Create feature flag in Python backend
4. Write comprehensive tests
5. Get team buy-in on approach
6. Start incremental migration
## Code Snippets for Testing
```bash
# Test the modern panel
cd superset-frontend
npm run dev
# In browser console
window.featureFlags = { MODERN_CONTROL_PANELS: true };
# Create a new Pie chart and verify controls work
```
## Success Criteria
- [ ] All control panels migrated to modern format
- [ ] No regression in functionality
- [ ] Improved developer experience
- [ ] Better performance metrics
- [ ] Reduced maintenance burden
- [ ] Full TypeScript coverage

View File

@@ -28,7 +28,6 @@ These features are considered **unfinished** and should only be used on developm
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
- ALERT_REPORT_TABS
- DATE_RANGE_TIMESHIFTS_ENABLED
- ENABLE_ADVANCED_DATA_TYPES
- PRESTO_EXPAND_DATA
- SHARE_QUERIES_VIA_KV_STORE

View File

@@ -23,8 +23,6 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
- [34204](https://github.com/apache/superset/pull/33603) OpenStreetView has been promoted as the new default for Deck.gl visualization since it can be enabled by default without requiring an API key. If you have Mapbox set up and want to disable OpenStreeView in your environment, please follow the steps documented here [https://superset.apache.org/docs/configuration/map-tiles].
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
@@ -32,7 +30,6 @@ assists people when migrating to a new version.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
## 5.0.0

View File

@@ -20,9 +20,6 @@
# If you choose to use this type of deployment make sure to
# create you own docker environment file (docker/.env) with your own
# unique random secure passwords and SECRET_KEY.
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
x-superset-image: &superset-image apachesuperset.docker.scarf.sh/apache/superset:${TAG:-latest-dev}
x-superset-volumes:

View File

@@ -1,157 +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.
#
# -----------------------------------------------------------------------
# Lightweight docker-compose for running multiple Superset instances
# This includes only essential services: database, Redis, and Superset app
#
# IMPORTANT: To run multiple instances in parallel:
# - Use different project names: docker-compose -p project1 -f docker-compose-light.yml up
# - Use different NODE_PORT values: NODE_PORT=9002 docker-compose -p project2 -f docker-compose-light.yml up
# - Volumes are isolated by project name (e.g., project1_db_home_light, project2_db_home_light)
# - Database name is intentionally different (superset_light) to prevent accidental cross-connections
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
x-superset-user: &superset-user root
x-superset-volumes: &superset-volumes
# /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- ./superset:/app/superset
- ./superset-frontend:/app/superset-frontend
- superset_home_light:/app/superset_home
- ./tests:/app/tests
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-bookworm
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
services:
db-light:
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
image: postgres:16
restart: unless-stopped
# No host port mapping - only accessible within Docker network
volumes:
- db_home_light:/var/lib/postgresql/data
- ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
# Override database name to avoid conflicts
POSTGRES_DB: superset_light
superset-light:
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
build:
<<: *common-build
command: ["/app/docker/docker-bootstrap.sh", "app"]
restart: unless-stopped
# No host port mapping - accessed via webpack dev server proxy
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user
depends_on:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
environment:
# Override DB connection for light service
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
EXAMPLES_HOST: db-light
EXAMPLES_DB: superset_light
EXAMPLES_USER: superset
EXAMPLES_PASSWORD: superset
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
superset-init-light:
build:
<<: *common-build
command: ["/app/docker/docker-init.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
depends_on:
db-light:
condition: service_started
user: *superset-user
volumes: *superset-volumes
environment:
# Override DB connection for light service
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
EXAMPLES_HOST: db-light
EXAMPLES_DB: superset_light
EXAMPLES_USER: superset
EXAMPLES_PASSWORD: superset
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
healthcheck:
disable: true
superset-node-light:
build:
context: .
target: superset-node
args:
# This prevents building the frontend bundle since we'll mount local folder
# and build it on startup while firing docker-frontend.sh in dev mode, where
# it'll mount and watch local files and rebuild as you update them
DEV_MODE: "true"
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
environment:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
# configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset-light:8088"
ports:
- "127.0.0.1:${NODE_PORT:-9001}:9000" # Parameterized port
command: ["/app/docker/docker-frontend.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
volumes: *superset-volumes
volumes:
superset_home_light:
external: false
db_home_light:
external: false

View File

@@ -20,9 +20,6 @@
# If you choose to use this type of deployment make sure to
# create you own docker environment file (docker/.env) with your own
# unique random secure passwords and SECRET_KEY.
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
x-superset-volumes:
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container

View File

@@ -20,9 +20,6 @@
# If you choose to use this type of deployment make sure to
# create you own docker environment file (docker/.env) with your own
# unique random secure passwords and SECRET_KEY.
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
x-superset-user: &superset-user root
x-superset-volumes: &superset-volumes

View File

@@ -53,12 +53,7 @@ PYTHONPATH=/app/pythonpath:/app/docker/pythonpath_dev
REDIS_HOST=redis
REDIS_PORT=6379
# Development and logging configuration
# FLASK_DEBUG: Enables Flask dev features (auto-reload, better error pages) - keep 'true' for development
FLASK_DEBUG=true
# SUPERSET_LOG_LEVEL: Controls Superset application logging verbosity (debug, info, warning, error, critical)
SUPERSET_LOG_LEVEL=info
SUPERSET_APP_ROOT="/"
SUPERSET_ENV=development
SUPERSET_LOAD_EXAMPLES=yes
@@ -71,3 +66,4 @@ SUPERSET_SECRET_KEY=TEST_NON_DEV_SECRET
ENABLE_PLAYWRIGHT=false
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true
SUPERSET_LOG_LEVEL=info

View File

@@ -23,57 +23,25 @@ MIN_MEM_FREE_GB=3
MIN_MEM_FREE_KB=$(($MIN_MEM_FREE_GB*1000000))
echo_mem_warn() {
# Check if running in Codespaces first
if [[ -n "${CODESPACES}" ]]; then
echo "Memory available: Codespaces managed"
return
fi
MEM_FREE_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_FREE_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
# Check platform and get memory accordingly
if [[ -f /proc/meminfo ]]; then
# Linux
if grep -q MemAvailable /proc/meminfo; then
MEM_AVAIL_KB=$(awk '/MemAvailable/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_AVAIL_GB=$(awk '/MemAvailable/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
else
MEM_AVAIL_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_AVAIL_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
fi
elif [[ "$(uname)" == "Darwin" ]]; then
# macOS - use vm_stat to get free memory
# vm_stat reports in pages, typically 4096 bytes per page
PAGE_SIZE=$(pagesize)
FREE_PAGES=$(vm_stat | awk '/Pages free:/ {print $3}' | tr -d '.')
INACTIVE_PAGES=$(vm_stat | awk '/Pages inactive:/ {print $3}' | tr -d '.')
# Free + inactive pages give us available memory (similar to MemAvailable on Linux)
AVAIL_PAGES=$((FREE_PAGES + INACTIVE_PAGES))
MEM_AVAIL_KB=$((AVAIL_PAGES * PAGE_SIZE / 1024))
MEM_AVAIL_GB=$(echo "scale=2; $MEM_AVAIL_KB / 1024 / 1024" | bc)
else
# Other platforms
echo "Memory available: Unable to determine"
return
fi
if [[ "${MEM_AVAIL_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
if [[ "${MEM_FREE_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
cat <<EOF
===============================================
======== Memory Insufficient Warning =========
===============================================
It looks like you only have ${MEM_AVAIL_GB}GB of
memory ${MEM_TYPE}. Please increase your Docker
It looks like you only have ${MEM_FREE_GB}GB of
memory free. Please increase your Docker
resources to at least ${MIN_MEM_FREE_GB}GB
Note: During builds, available memory may be
temporarily low due to caching and compilation.
===============================================
======== Memory Insufficient Warning =========
===============================================
EOF
else
echo "Memory available: ${MEM_AVAIL_GB} GB"
echo "Memory check Ok [${MEM_FREE_GB}GB free]"
fi
}

View File

@@ -20,5 +20,4 @@
# DON'T ignore the .gitignore
!.gitignore
!superset_config.py
!superset_config_docker_light.py
!superset_config_local.example

View File

@@ -129,7 +129,7 @@ if os.getenv("CYPRESS_CONFIG") == "true":
#
try:
import superset_config_docker
from superset_config_docker import * # noqa: F403
from superset_config_docker import * # noqa
logger.info(
f"Loaded your Docker configuration at [{superset_config_docker.__file__}]"

View File

@@ -1,37 +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.
#
# Configuration for docker-compose-light.yml - disables Redis and uses minimal services
# Import all settings from the main config first
from flask_caching.backends.filesystemcache import FileSystemCache
from superset_config import * # noqa: F403
# Override caching to use simple in-memory cache instead of Redis
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab")
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_KEY_PREFIX": "superset_light_",
}
DATA_CACHE_CONFIG = CACHE_CONFIG
THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
# Disable Celery entirely for lightweight mode
CELERY_CONFIG = None # type: ignore[assignment,misc]

View File

@@ -10,143 +10,44 @@ version: 1
apache-superset>=6.0
:::
Superset now rides on **Ant Design v5's token-based theming**.
Superset now rides on **Ant Design v5s token-based theming**.
Every Antd token works, plus a handful of Superset-specific ones for charts and dashboard chrome.
## Managing Themes via CRUD Interface
## 1 — Create a theme
Superset now includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
### Creating a New Theme
1. Navigate to **Settings > Themes** in the Superset interface
2. Click **+ Theme** to create a new theme
3. Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to design your theme:
- Design your palette, typography, and component overrides
- Open the `CONFIG` modal and copy the JSON configuration
4. Paste the JSON into the theme definition field in Superset
5. Give your theme a descriptive name and save
1. Open the official [Ant Design Theme Editor](https://ant.design/theme-editor)
2. Design your palette, typography, and component overrides.
3. Open the `CONFIG` modal and paste the JSON.
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
### Applying Themes to Dashboards
Once created, themes can be applied to individual dashboards:
- Edit any dashboard and select your custom theme from the theme dropdown
- Each dashboard can have its own theme, allowing for branded or context-specific styling
## Alternative: Instance-wide Configuration
For system-wide theming, you can configure default themes via Python configuration:
### Setting Default Themes
## 2 — Apply it instance-wide
```python
# superset_config.py
# Default theme (light mode)
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
"colorSuccess": "#5ac189",
# ... your theme JSON configuration
}
}
# Dark theme configuration
THEME_DARK = {
"algorithm": "dark",
"token": {
"colorPrimary": "#2893B3",
# ... your dark theme overrides
}
}
# Theme behavior settings
THEME_SETTINGS = {
"enforced": False, # If True, forces default theme always
"allowSwitching": True, # Allow users to switch between themes
"allowOSPreference": True, # Auto-detect system theme preference
THEME = {
# Paste your JSON theme definition here
}
```
### Copying Themes from CRUD Interface
Restart Superset to apply changes
To use a theme created via the CRUD interface as your system default:
1. Navigate to **Settings > Themes** and edit your desired theme
2. Copy the complete JSON configuration from the theme definition field
3. Paste it directly into your `superset_config.py` as shown above
Restart Superset to apply changes.
## Theme Development Workflow
1. **Design**: Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to iterate on your design
2. **Test**: Create themes in Superset's CRUD interface for testing
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
## Custom Fonts
Superset supports custom fonts through runtime configuration, allowing you to use branded or custom typefaces without rebuilding the application.
### Configuring Custom Fonts
Add font URLs to your `superset_config.py`:
## 3 — Tweak live in the app (beta)
Set the feature flag in your `superset_config`
```python
# Load fonts from Google Fonts, Adobe Fonts, or self-hosted sources
CUSTOM_FONT_URLS = [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
]
# Update CSP to allow font sources
TALISMAN_CONFIG = {
"content_security_policy": {
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
}
DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
{{ ... }}
THEME_ALLOW_THEME_EDITOR_BETA = True,
}
```
### Using Custom Fonts in Themes
- Enables a JSON editor panel inside Superset as a new icon in the navbar
- Intended for testing/design and rapid in-context iteration
- End-user theme switching & preferences coming later
Once configured, reference the fonts in your theme configuration:
## 4 — Potential Next Steps
```python
THEME_DEFAULT = {
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace",
# ... other theme tokens
}
}
```
Or in the CRUD interface theme JSON:
```json
{
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace"
}
}
```
### Font Sources
- **Google Fonts**: Free, CDN-hosted fonts with wide variety
- **Adobe Fonts**: Premium fonts (requires subscription and kit ID)
- **Self-hosted**: Place font files in `/static/assets/fonts/` and reference via CSS
This feature works with the stock Docker image - no custom build required!
## Advanced Features
- **System Themes**: Superset includes built-in light and dark themes
- **Per-Dashboard Theming**: Each dashboard can have its own visual identity
- **JSON Editor**: Edit theme configurations directly within Superset's interface
- **Custom Fonts**: Load external fonts via configuration without rebuilding
- CRUD UI for managing multiple themes
- Per-dashboard & per-workspace theme assignment
- User-selectable theme preferences

View File

@@ -120,78 +120,6 @@ docker volume rm superset_db_home
docker-compose up
```
## GitHub Codespaces (Cloud Development)
GitHub Codespaces provides a complete, pre-configured development environment in the cloud. This is ideal for:
- Quick contributions without local setup
- Consistent development environments across team members
- Working from devices that can't run Docker locally
- Safe experimentation in isolated environments
:::info
We're grateful to GitHub for providing this excellent cloud development service that makes
contributing to Apache Superset more accessible to developers worldwide.
:::
### Getting Started with Codespaces
1. **Create a Codespace**: Use this pre-configured link that sets up everything you need:
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=master&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=UsWest)
:::caution
**Important**: You must select at least the **4 CPU / 16GB RAM** machine type (pre-selected in the link above).
Smaller instances will not have sufficient resources to run Superset effectively.
:::
2. **Wait for Setup**: The initial setup takes several minutes. The Codespace will:
- Build the development container
- Install all dependencies
- Start all required services (PostgreSQL, Redis, etc.)
- Initialize the database with example data
3. **Access Superset**: Once ready, check the **PORTS** tab in VS Code for port `9001`.
Click the globe icon to open Superset in your browser.
- Default credentials: `admin` / `admin`
### Key Features
- **Auto-reload**: Both Python and TypeScript files auto-refresh on save
- **Pre-installed Extensions**: VS Code extensions for Python, TypeScript, and database tools
- **Multiple Instances**: Run multiple Codespaces for different branches/features
- **SSH Access**: Connect via terminal using `gh cs ssh` or through the GitHub web UI
- **VS Code Integration**: Works seamlessly with VS Code desktop app
### Managing Codespaces
- **List active Codespaces**: `gh cs list`
- **SSH into a Codespace**: `gh cs ssh`
- **Stop a Codespace**: Via GitHub UI or `gh cs stop`
- **Delete a Codespace**: Via GitHub UI or `gh cs delete`
### Debugging and Logs
Since Codespaces uses `docker-compose-light.yml`, you can monitor all services:
```bash
# Stream logs from all services
docker compose -f docker-compose-light.yml logs -f
# Stream logs from a specific service
docker compose -f docker-compose-light.yml logs -f superset
# View last 100 lines and follow
docker compose -f docker-compose-light.yml logs --tail=100 -f
# List all running services
docker compose -f docker-compose-light.yml ps
```
:::tip
Codespaces automatically stop after 30 minutes of inactivity to save resources.
Your work is preserved and you can restart anytime.
:::
## Installing Development Tools
:::note
@@ -421,6 +349,14 @@ Then make sure you run your WSGI server using the right worker type:
gunicorn "superset.app:create_app()" -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -b 127.0.0.1:8088 --reload
```
You can log anything to the browser console, including objects:
```python
from superset import app
app.logger.error('An exception occurred!')
app.logger.info(form_data)
```
### Frontend
Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in order to properly display the web UI. The `superset-frontend` directory contains all NPM-managed frontend assets. Note that for some legacy pages there are additional frontend assets bundled with Flask-Appbuilder (e.g. jQuery and bootstrap). These are not managed by NPM and may be phased out in the future.

View File

@@ -26,14 +26,11 @@ Superset locally is using Docker Compose on a Linux or Mac OSX
computer. Superset does not have official support for Windows. It's also the easiest
way to launch a fully functioning **development environment** quickly.
Note that there are 4 major ways we support to run `docker compose`:
Note that there are 3 major ways we support to run `docker compose`:
1. **docker-compose.yml:** for interactive development, where we mount your local folder with the
frontend/backend files that you can edit and experience the changes you
make in the app in real time
1. **docker-compose-light.yml:** a lightweight configuration with minimal services (database,
Superset app, and frontend dev server) for development. Uses in-memory caching instead of Redis
and is designed for running multiple instances simultaneously
1. **docker-compose-non-dev.yml** where we just build a more immutable image based on the
local branch and get all the required images running. Changes in the local branch
at the time you fire this up will be reflected, but changes to the code
@@ -47,7 +44,7 @@ Note that there are 4 major ways we support to run `docker compose`:
The `dev` builds include the `psycopg2-binary` required to connect
to the Postgres database launched as part of the `docker compose` builds.
More on these approaches after setting up the requirements for either.
More on these two approaches after setting up the requirements for either.
## Requirements
@@ -106,36 +103,13 @@ and help you start fresh. In the context of `docker compose` setting
from within docker. This will slow down the startup, but will fix various npm-related issues.
:::
### Option #2 - lightweight development with multiple instances
For a lighter development setup that uses fewer resources and supports running multiple instances:
```bash
# Single lightweight instance (default port 9001)
docker compose -f docker-compose-light.yml up
# Multiple instances with different ports
NODE_PORT=9001 docker compose -p superset-1 -f docker-compose-light.yml up
NODE_PORT=9002 docker compose -p superset-2 -f docker-compose-light.yml up
NODE_PORT=9003 docker compose -p superset-3 -f docker-compose-light.yml up
```
This configuration includes:
- PostgreSQL database (internal network only)
- Superset application server
- Frontend development server with webpack hot reloading
- In-memory caching (no Redis)
- Isolated volumes and networks per instance
Access each instance at `http://localhost:{NODE_PORT}` (e.g., `http://localhost:9001`).
### Option #3 - build a set of immutable images from the local branch
### Option #2 - build a set of immutable images from the local branch
```bash
docker compose -f docker-compose-non-dev.yml up
```
### Option #4 - boot up an official release
### Option #3 - boot up an official release
```bash
# Set the version you want to run

View File

@@ -28,9 +28,6 @@ const globals = require('globals');
const { defineConfig, globalIgnores } = require('eslint/config');
module.exports = defineConfig([
{
files: ['**/*.{js,jsx,ts,tsx}'],
},
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
js.configs.recommended,
...ts.configs.recommended,
@@ -39,7 +36,7 @@ module.exports = defineConfig([
files: ['eslint.config.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
}
},
{
languageOptions: {
@@ -71,5 +68,5 @@ module.exports = defineConfig([
version: 'detect',
},
},
},
]);
}
])

View File

@@ -15,7 +15,7 @@
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"eslint": "eslint ."
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
@@ -26,33 +26,33 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"antd": "^5.26.7",
"antd": "^5.26.3",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.4.0",
"less": "^4.3.0",
"less-loader": "^12.3.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.27.1"
"swagger-ui-react": "^5.26.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@eslint/js": "^9.32.0",
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.8",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.37.0",
"webpack": "^5.101.0"
"webpack": "^5.99.9"
},
"browserslist": {
"production": [
@@ -65,6 +65,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
}

View File

@@ -2205,16 +2205,11 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.31.0":
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/js@^9.32.0":
version "9.32.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
@@ -2517,10 +2512,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -3427,10 +3422,10 @@
dependencies:
"@types/estree" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.6"
@@ -3971,11 +3966,6 @@ accepts@~1.3.4, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-import-phases@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -3988,7 +3978,12 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -4112,10 +4107,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.7:
version "5.26.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
@@ -4128,7 +4123,7 @@ antd@^5.26.7:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.3.0"
"@rc-component/trigger" "^2.2.7"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4158,7 +4153,7 @@ antd@^5.26.7:
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-tabs "~15.6.1"
rc-textarea "~1.10.1"
rc-textarea "~1.10.0"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
@@ -4325,12 +4320,12 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-loader@^9.2.1:
@@ -4513,7 +4508,17 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
browserslist@^4.25.0:
version "4.25.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
@@ -4615,7 +4620,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
version "1.0.30001714"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
@@ -5898,6 +5903,11 @@ electron-to-chromium@^1.5.160:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
electron-to-chromium@^1.5.73:
version "1.5.138"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -5942,10 +5952,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.17.2:
version "5.18.2"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
enhanced-resolve@^5.17.1:
version "5.18.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6151,10 +6161,10 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^10.1.8:
version "10.1.8"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
eslint-config-prettier@^10.1.5:
version "10.1.5"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-plugin-prettier@^5.5.1:
version "5.5.1"
@@ -6643,7 +6653,7 @@ form-data-encoder@^2.1.2:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.4:
form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
@@ -8053,10 +8063,10 @@ less-loader@^12.3.0:
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
less@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
less@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
dependencies:
copy-anything "^2.0.1"
parse-node-version "^1.0.1"
@@ -10704,10 +10714,10 @@ rc-tabs@~15.6.1:
rc-resize-observer "^1.0.0"
rc-util "^5.34.1"
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
version "1.10.2"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
rc-textarea@~1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
@@ -12090,10 +12100,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -12515,7 +12525,7 @@ unraw@^3.0.0:
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.3:
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
@@ -12784,27 +12794,26 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.101.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
version "5.99.9"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
acorn "^8.14.0"
browserslist "^4.24.0"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.2"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -12818,7 +12827,7 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
tapable "^2.1.1"
terser-webpack-plugin "^5.3.11"
watchpack "^2.4.1"
webpack-sources "^3.3.3"
webpack-sources "^3.2.3"
webpackbar@^6.0.1:
version "6.0.1"

486
package-lock.json generated
View File

@@ -1,486 +0,0 @@
{
"name": "move-controls",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"glob": "^11.0.3"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"glob": "^11.0.3"
}
}

View File

@@ -95,7 +95,7 @@ dependencies = [
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.3, <0.39",
"sqlglot>=27.3.0, <28",
"sqlglot>=26.1.3, <27",
# newer pandas needs 0.9+
"tabulate>=0.9.0, <1.0",
"typing-extensions>=4, <5",
@@ -111,7 +111,7 @@ athena = ["pyathena[pandas]>=2, <3"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
"sqlalchemy-bigquery>=1.15.0",
"sqlalchemy-bigquery>=1.6.1",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
@@ -311,16 +311,15 @@ select = [
"Q",
"S",
"T",
"TID",
"W",
]
ignore = [
"S101",
"PT006",
"T201",
"N999",
]
extend-select = ["I"]
# Allow fix for all enabled rules (when `--fix`) is provided.
@@ -330,16 +329,6 @@ unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.per-file-ignores]
"scripts/*" = ["TID251"]
"setup.py" = ["TID251"]
"superset/config.py" = ["TID251"]
"superset/cli/update.py" = ["TID251"]
"superset/key_value/types.py" = ["TID251"]
"superset/translations/utils.py" = ["TID251"]
"superset/extensions/__init__.py" = ["TID251"]
"superset/utils/json.py" = ["TID251"]
[tool.ruff.lint.isort]
case-sensitive = false
combine-as-imports = true
@@ -356,9 +345,6 @@ section-order = [
"local-folder"
]
[tool.ruff.lint.flake8-tidy-imports]
banned-api = { json = { msg = "Use superset.utils.json instead" }, simplejson = { msg = "Use superset.utils.json instead" } }
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"

View File

@@ -378,7 +378,7 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
sqlglot==27.3.0
sqlglot==26.28.1
# via apache-superset (pyproject.toml)
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)

View File

@@ -795,14 +795,14 @@ sqlalchemy==1.4.54
# shillelagh
# sqlalchemy-bigquery
# sqlalchemy-utils
sqlalchemy-bigquery==1.15.0
sqlalchemy-bigquery==1.12.0
# via apache-superset
sqlalchemy-utils==0.38.3
# via
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
sqlglot==27.3.0
sqlglot==26.28.1
# via
# -c requirements/base.txt
# apache-superset

View File

@@ -32,10 +32,6 @@ const PACKAGE_ARG_REGEX = /^package=/;
const EXCLUDE_DECLARATION_DIR_REGEX = /^excludeDeclarationDir=/;
const DECLARATION_FILE_REGEX = /\.d\.ts$/;
// Configuration for batching and fallback
const MAX_FILES_FOR_TARGETED_CHECK = 20; // Fallback to full check if more files
const BATCH_SIZE = 10; // Process files in batches of this size
void (async () => {
const args = process.argv.slice(2);
const {
@@ -49,94 +45,27 @@ void (async () => {
}
const packageRootDir = await getPackage(packageArg);
const changedFiles = removePackageSegment(remainingArgs, packageRootDir);
const updatedArgs = removePackageSegment(remainingArgs, packageRootDir);
const argsStr = updatedArgs.join(" ");
// Filter to only TypeScript files
const tsFiles = changedFiles.filter(file =>
/\.(ts|tsx)$/.test(file) && !DECLARATION_FILE_REGEX.test(file)
const excludedDeclarationDirs = getExcludedDeclarationDirs(
excludeDeclarationDirArg
);
console.log(`Type checking ${tsFiles.length} changed TypeScript files...`);
if (tsFiles.length === 0) {
console.log("No TypeScript files to check.");
exit(0);
}
// Decide strategy based on number of files
if (tsFiles.length > MAX_FILES_FOR_TARGETED_CHECK) {
console.log(`Too many files (${tsFiles.length} > ${MAX_FILES_FOR_TARGETED_CHECK}), running full type check...`);
await runFullTypeCheck(packageRootDir, excludeDeclarationDirArg);
} else {
console.log(`Running targeted type check on ${tsFiles.length} files...`);
await runTargetedTypeCheck(packageRootDir, tsFiles, excludeDeclarationDirArg);
}
})();
/**
* Run full type check on the entire project
*/
async function runFullTypeCheck(packageRootDir, excludeDeclarationDirArg) {
const packageRootDirAbsolute = join(SUPERSET_ROOT, packageRootDir);
const tsConfig = getTsConfig(packageRootDirAbsolute);
// Use incremental compilation for better caching
const command = `--noEmit --allowJs --incremental --project ${tsConfig}`;
await executeTypeCheck(packageRootDirAbsolute, command);
}
/**
* Run targeted type check on specific files, with batching
*/
async function runTargetedTypeCheck(packageRootDir, tsFiles, excludeDeclarationDirArg) {
const excludedDeclarationDirs = getExcludedDeclarationDirs(excludeDeclarationDirArg);
let declarationFiles = await getFilesRecursively(
join(SUPERSET_ROOT, packageRootDir),
packageRootDir,
DECLARATION_FILE_REGEX,
excludedDeclarationDirs
);
declarationFiles = removePackageSegment(declarationFiles, packageRootDir);
const declarationFilesStr = declarationFiles.join(" ");
const packageRootDirAbsolute = join(SUPERSET_ROOT, packageRootDir);
const tsConfig = getTsConfig(packageRootDirAbsolute);
const command = `--noEmit --allowJs --composite false --project ${tsConfig} ${argsStr} ${declarationFilesStr}`;
// Process files in batches to avoid command line length limits
const batches = [];
for (let i = 0; i < tsFiles.length; i += BATCH_SIZE) {
batches.push(tsFiles.slice(i, i + BATCH_SIZE));
}
let hasErrors = false;
for (const [batchIndex, batch] of batches.entries()) {
if (batches.length > 1) {
console.log(`\nProcessing batch ${batchIndex + 1}/${batches.length} (${batch.length} files)...`);
}
const argsStr = batch.join(" ");
const declarationFilesStr = declarationFiles.join(" ");
// For targeted checks, keep composite false since we're passing specific files
const command = `--noEmit --allowJs --composite false --project ${tsConfig} ${argsStr} ${declarationFilesStr}`;
try {
await executeTypeCheck(packageRootDirAbsolute, command);
} catch (error) {
hasErrors = true;
// Continue processing other batches to show all errors
}
}
if (hasErrors) {
exit(1);
}
}
/**
* Execute the TypeScript type check command
*/
async function executeTypeCheck(packageRootDirAbsolute, command) {
try {
chdir(packageRootDirAbsolute);
// Please ensure that tscw-config is installed in the package being type-checked.
const tscw = packageRequire("tscw-config");
const child = await tscw`${command}`;
@@ -148,16 +77,14 @@ async function executeTypeCheck(packageRootDirAbsolute, command) {
console.error(child.stderr);
}
if (child.exitCode !== 0) {
throw new Error(`Type check failed with exit code ${child.exitCode}`);
}
exit(child.exitCode);
} catch (e) {
console.error("Failed to execute type checking:", e.message);
console.error("Failed to execute type checking:", e);
console.error("Package:", packageRootDir);
console.error("Command:", `tscw ${command}`);
throw e;
exit(1);
}
}
})();
/**
*
@@ -185,6 +112,7 @@ function shouldExcludeDir(fullPath, excludedDirs) {
*
* @returns {Promise<string[]>}
*/
async function getFilesRecursively(dir, regex, excludedDirs) {
try {
const files = await readdir(dir, { withFileTypes: true });
@@ -258,6 +186,7 @@ function getExcludedDeclarationDirs(excludeDeclarationDirArg) {
* @param {RegExp[]} regexes
* @returns {{ matchedArgs: (string | undefined)[], remainingArgs: string[] }}
*/
function extractArgs(args, regexes) {
/**
* @type {(string | undefined)[]}

View File

@@ -33,4 +33,4 @@ superset load-test-users
echo "Running tests"
pytest --durations-min=2 --cov-report= --cov=superset ./tests/integration_tests "$@"
pytest --durations-min=2 --maxfail=1 --cov-report= --cov=superset ./tests/integration_tests "$@"

View File

@@ -94,12 +94,67 @@ describe('Charts list', () => {
});
});
describe('list mode', () => {
before(() => {
visitChartList();
setGridMode('list');
});
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
cy.getBySel('sort-header').eq(1).contains('Name');
cy.getBySel('sort-header').eq(2).contains('Type');
cy.getBySel('sort-header').eq(3).contains('Dataset');
cy.getBySel('sort-header').eq(4).contains('On dashboards');
cy.getBySel('sort-header').eq(5).contains('Owners');
cy.getBySel('sort-header').eq(6).contains('Last modified');
cy.getBySel('sort-header').eq(7).contains('Actions');
});
it('should bulk select in list mode', () => {
toggleBulkSelect();
cy.get('[aria-label="Select all"]').click();
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
cy.getBySel('bulk-select-copy').contains('0 Selected');
cy.getBySel('bulk-select-action').should('not.exist');
});
});
describe('card mode', () => {
before(() => {
visitChartList();
setGridMode('card');
});
it('should load rows in card mode', () => {
cy.getBySel('listview-table').should('not.exist');
cy.getBySel('styled-card').should('have.length', 25);
});
it('should bulk select in card mode', () => {
toggleBulkSelect();
cy.getBySel('styled-card').click({ multiple: true });
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.getBySel('bulk-select-copy').contains('0 Selected');
cy.getBySel('bulk-select-action').should('not.exist');
});
it('should preserve other filters when sorting', () => {
cy.getBySel('styled-card').should('have.length', 25);
setFilter('Type', 'Big Number');

View File

@@ -31,52 +31,6 @@ import {
interceptFormDataKey,
} from '../explore/utils';
const interceptDrillInfo = () => {
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
statusCode: 200,
body: {
result: {
id: 1,
changed_on_humanized: '2 days ago',
created_on_humanized: 'a week ago',
table_name: 'birth_names',
changed_by: {
first_name: 'Admin',
last_name: 'User',
},
created_by: {
first_name: 'Admin',
last_name: 'User',
},
owners: [
{
first_name: 'Admin',
last_name: 'User',
},
],
columns: [
{
column_name: 'gender',
verbose_name: null,
},
{
column_name: 'state',
verbose_name: null,
},
{
column_name: 'name',
verbose_name: null,
},
{
column_name: 'ds',
verbose_name: null,
},
],
},
},
}).as('drillInfo');
};
const closeModal = () => {
cy.get('body').then($body => {
if ($body.find('[data-test="close-drill-by-modal"]').length) {
@@ -100,7 +54,7 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
interceptV1ChartData();
}
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
.should('be.visible')
.find("[role='menu'] [role='menuitem']")
.contains(/^Drill by$/)
@@ -108,20 +62,14 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
{ timeout: 15000 },
)
.should('be.visible')
.find('[role="menuitem"]')
.contains(new RegExp(`^${targetDrillByColumn}$`))
.click();
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).should('not.exist');
.then($el => {
cy.wrap($el)
.contains(new RegExp(`^${targetDrillByColumn}$`))
.trigger('keydown', { keyCode: 13, which: 13, force: true });
});
if (isLegacy) {
return cy.wait('@legacyData');
@@ -282,19 +230,17 @@ describe('Drill by modal', () => {
closeModal();
});
before(() => {
interceptDrillInfo();
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
});
describe('Modal actions + Table', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
it.only('opens the modal from the context menu', () => {
it('opens the modal from the context menu', () => {
openTableContextMenu('boy');
drillBy('state').then(intercepted => {
verifyExpectedFormData(intercepted, {
@@ -438,7 +384,6 @@ describe('Drill by modal', () => {
describe('Tier 1 charts', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
@@ -584,7 +529,7 @@ describe('Drill by modal', () => {
]);
});
it.skip('Bar Chart', () => {
it('Bar Chart', () => {
testEchart('echarts_timeseries_bar', 'Bar Chart', [
[85, 94],
[490, 68],
@@ -602,7 +547,6 @@ describe('Drill by modal', () => {
describe('Tier 2 charts', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 2');
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});
@@ -668,7 +612,7 @@ describe('Drill by modal', () => {
]);
});
it.skip('Mixed Chart', () => {
it('Mixed Chart', () => {
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
// click 'boy'
cy.wrap($canvas).scrollIntoView();

View File

@@ -154,7 +154,6 @@ describe('Horizontal FilterBar', () => {
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
openMoreFilters();
cy.getBySel('form-item-value').should('have.length', 12);

View File

@@ -22,7 +22,6 @@ import {
dataTestChartName,
} from 'cypress/support/directories';
import { waitForChartLoad } from 'cypress/utils';
import {
addParentFilterWithValue,
applyNativeFilterValueWithIndex,
@@ -161,74 +160,6 @@ describe('Native filters', () => {
);
});
it('Dependent filter selects first item based on parent filter selection', () => {
prepareDashboardFilters([
{ name: 'region', column: 'region', datasetId: 2 },
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
selectFilter(0);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Select first filter value by default')
.should('be.visible')
.click();
},
);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Can select multiple values ')
.should('be.visible')
.click();
},
);
selectFilter(1);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Values are dependent on other filters')
.should('be.visible')
.click();
},
);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Can select multiple values ')
.should('be.visible')
.click();
},
);
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Select first filter value by default')
.should('be.visible')
.click();
},
);
// cannot use saveNativeFilterSettings because there is a bug which
// sometimes does not allow charts to load when enabling the 'Select first filter value by default'
// to be saved when using dependent filters so,
// you reload the window.
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click({ force: true });
cy.get(nativeFilters.modal.container).should('not.exist');
cy.reload();
applyNativeFilterValueWithIndex(0, 'North America');
// Check that dependent filter auto-selects the first item
cy.get(nativeFilters.filterFromDashboardView.filterContent)
.eq(1)
.should('contain.text', 'Bermuda');
});
it('User can create filter depend on 2 other filters', () => {
prepareDashboardFilters([
{ name: 'region', column: 'region', datasetId: 2 },

View File

@@ -68,13 +68,11 @@ function verifyDashboardSearch() {
function verifyDashboardLink() {
interceptDashboardGet();
openDashboardsAddedTo();
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover', {
force: true,
});
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
cy.get('.ant-dropdown-menu-submenu-popup a')
.first()
.invoke('removeAttr', 'target')
.click({ force: true });
.click();
cy.wait('@get');
}

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
"type": "tsc --noEmit",
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
"validate-release": "../RELEASING/validate_this_release.sh"
},
"browserslist": [
@@ -121,15 +121,15 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"ag-grid-community": "33.1.1",
"ag-grid-react": "33.1.1",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.6.0",
"dom-to-image-more": "^3.2.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
"emotion-rgba": "0.0.12",
@@ -145,6 +145,7 @@
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^130.0.0",
"html-webpack-plugin": "^5.6.3",
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
@@ -208,7 +209,7 @@
"devDependencies": {
"@applitools/eyes-storybook": "^3.55.6",
"@babel/cli": "^7.27.2",
"@babel/compat-data": "^7.28.0",
"@babel/compat-data": "^7.26.8",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/node": "^7.22.6",
@@ -243,7 +244,7 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/classnames": "^2.3.4",
"@types/classnames": "^2.2.10",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^29.5.14",
"@types/js-levenshtein": "^1.1.3",
@@ -316,7 +317,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.6.2",
"prettier": "3.5.3",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -331,7 +332,7 @@
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.20.3",
"tsx": "^4.19.2",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",
@@ -349,7 +350,7 @@
"regenerator-runtime": "^0.14.1"
},
"engines": {
"node": "^20.18.1",
"node": "^20.16.0",
"npm": "^10.8.1"
},
"overrides": {

View File

@@ -36,7 +36,7 @@
"devDependencies": {
"cross-env": "^7.0.3",
"fs-extra": "^11.3.0",
"jest": "^30.0.4",
"jest": "^30.0.2",
"yeoman-test": "^10.1.1"
},
"engines": {

View File

@@ -18,7 +18,8 @@
*/
import { ReactNode } from 'react';
import { t, css } from '@superset-ui/core';
import { InfoTooltip, Tooltip, Icons } from '@superset-ui/core/components';
import { InfoCircleOutlined } from '@ant-design/icons';
import { InfoTooltip, Tooltip } from '@superset-ui/core/components';
type ValidationError = string;
@@ -92,7 +93,7 @@ export function ControlHeader({
<div className="ControlHeader" data-test={`${name}-header`}>
<div className="pull-left">
<label className="control-label" htmlFor={name}>
{leftNode && <>{leftNode}</>}
{leftNode && <span>{leftNode}</span>}
<span
role={onClick ? 'button' : undefined}
{...(onClick ? { onClick, tabIndex: 0 } : {})}
@@ -103,9 +104,9 @@ export function ControlHeader({
{warning && (
<span>
<Tooltip id="error-tooltip" placement="top" title={warning}>
<Icons.InfoCircleOutlined
iconSize="m"
<InfoCircleOutlined
css={theme => css`
font-size: ${theme.sizeUnit * 3}px;
color: ${theme.colorError};
`}
/>
@@ -115,9 +116,9 @@ export function ControlHeader({
{danger && (
<span>
<Tooltip id="error-tooltip" placement="top" title={danger}>
<Icons.InfoCircleOutlined
iconSize="m"
<InfoCircleOutlined
css={theme => css`
font-size: ${theme.sizeUnit * 3}px;
color: ${theme.colorError};
`}
/>{' '}
@@ -131,9 +132,9 @@ export function ControlHeader({
placement="top"
title={validationErrors.join(' ')}
>
<Icons.InfoCircleOutlined
iconSize="m"
<InfoCircleOutlined
css={theme => css`
font-size: ${theme.sizeUnit * 3}px;
color: ${theme.colorError};
`}
/>{' '}

View File

@@ -1,72 +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 React, { useState } from 'react';
import { styled } from '@superset-ui/core';
import { Collapse } from '@superset-ui/core/components';
const { Panel } = Collapse;
const StyledCollapse = styled(Collapse)`
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
border: none;
background: transparent;
.ant-collapse-item {
border: 1px solid ${({ theme }: any) => theme.colors.grayscale.light2};
border-radius: ${({ theme }: any) => theme.borderRadius}px;
margin-bottom: ${({ theme }: any) => theme.gridUnit * 2}px;
.ant-collapse-header {
font-weight: ${({ theme }: any) => theme.typography.weights.bold};
background: ${({ theme }: any) => theme.colors.grayscale.light5};
border-radius: ${({ theme }: any) => theme.borderRadius}px
${({ theme }: any) => theme.borderRadius}px 0 0;
}
.ant-collapse-content {
background: white;
padding: ${({ theme }: any) => theme.gridUnit * 3}px;
}
}
`;
export interface ControlPanelSectionProps {
title: string;
children: React.ReactNode;
expanded?: boolean;
}
export const ControlPanelSection: React.FC<ControlPanelSectionProps> = ({
title,
children,
expanded = false,
}) => {
const [activeKey, setActiveKey] = useState(expanded ? ['1'] : []);
return (
<StyledCollapse
activeKey={activeKey}
onChange={keys => setActiveKey(keys as string[])}
>
<Panel header={title} key="1">
{children}
</Panel>
</StyledCollapse>
);
};

View File

@@ -1,42 +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 React from 'react';
import { styled } from '@superset-ui/core';
const StyledRow = styled.div`
display: flex;
gap: ${({ theme }: any) => theme.gridUnit * 3}px;
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
& > * {
flex: 1;
}
.control-wrapper {
min-width: 0; // Allow flex items to shrink
}
`;
export interface ControlRowProps {
children: React.ReactNode;
}
export const ControlRow: React.FC<ControlRowProps> = ({ children }) => (
<StyledRow>{children}</StyledRow>
);

View File

@@ -86,9 +86,7 @@ export default function Select<VT extends string | number>({
minWidth,
}}
>
{options?.map(([val, label]) => (
<Option value={val}>{label}</Option>
))}
{options?.map(([val, label]) => <Option value={val}>{label}</Option>)}
{children}
{value && !optionsHasValue && (
<Option key={value} value={value}>

View File

@@ -1,75 +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 React from 'react';
import { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import AdhocFilterControlOriginal from 'src/explore/components/controls/FilterControl/AdhocFilterControl/index';
import { ControlHeader } from '../ControlHeader';
export interface AdhocFilterControlProps {
name: string;
value?: any[];
onChange: (value: any[]) => void;
label?: string;
description?: string;
required?: boolean;
renderTrigger?: boolean;
}
/**
* Wrapper around the existing AdhocFilterControl that simplifies its API
*/
export const AdhocFilterControl: React.FC<AdhocFilterControlProps> = ({
name,
value = [],
onChange,
label,
description,
required,
renderTrigger,
}) => {
// Get datasource from Redux state
const datasource = useSelector<ExplorePageState>(
state => state.explore.datasource,
) as any;
const columns = datasource?.columns || [];
const savedMetrics = datasource?.metrics || [];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<AdhocFilterControlOriginal
name={name}
value={value}
onChange={onChange}
columns={columns}
savedMetrics={savedMetrics}
datasource={datasource}
/>
</div>
);
};

View File

@@ -1,39 +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 { ReactElement } from 'react';
// @ts-ignore
import CheckboxControlComponent from '../../../../../../../src/explore/components/controls/CheckboxControl';
export interface CheckboxControlProps {
value?: boolean;
onChange: (value: boolean) => void;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
hovered?: boolean;
[key: string]: any;
}
/**
* Checkbox control component
*/
export const CheckboxControl: React.FC<CheckboxControlProps> = (
props,
): ReactElement => <CheckboxControlComponent {...props} />;

View File

@@ -1,68 +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 React from 'react';
import { getCategoricalSchemeRegistry } from '@superset-ui/core';
import { SelectControl } from './SimpleSelectControl';
export interface ColorSchemeControlProps {
name: string;
value?: string;
onChange: (value: string) => void;
label?: string;
description?: string;
required?: boolean;
renderTrigger?: boolean;
clearable?: boolean;
}
/**
* Color scheme selector control
*/
export const ColorSchemeControl: React.FC<ColorSchemeControlProps> = ({
name,
value,
onChange,
label,
description,
required,
renderTrigger,
clearable = false,
}) => {
// Get available color schemes from the categorical scheme registry
const colorSchemes = getCategoricalSchemeRegistry().keys();
const choices: [string, string][] = colorSchemes.map((scheme: string) => [
scheme,
scheme,
]);
return (
<SelectControl
name={name}
value={value}
onChange={onChange as (value: string | string[]) => void}
label={label}
description={description}
required={required}
renderTrigger={renderTrigger}
choices={choices}
clearable={clearable}
multiple={false}
/>
);
};

View File

@@ -1,38 +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 { ReactElement } from 'react';
// @ts-ignore
import ControlComponent from '../../../../../../../src/explore/components/Control';
export interface ControlProps {
type?: string;
name: string;
value?: any;
actions?: any;
formData?: any;
renderTrigger?: boolean;
[key: string]: any;
}
/**
* Generic control wrapper component
*/
export const Control: React.FC<ControlProps> = (props): ReactElement => (
<ControlComponent {...props} />
);

View File

@@ -1,94 +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 { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import { DndColumnSelect as DndColumnSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import { ControlHeader } from '../ControlHeader';
export interface DndColumnSelectProps {
name: string;
value?: any;
onChange: (value: any) => void;
label?: string;
description?: string;
multi?: boolean;
required?: boolean;
renderTrigger?: boolean;
canDelete?: boolean;
ghostButtonText?: string;
isTemporal?: boolean;
}
/**
* Wrapper around the existing DndColumnSelect that simplifies its API
*/
export const DndColumnSelect: React.FC<DndColumnSelectProps> = ({
name,
value,
onChange,
label,
description,
multi = false,
required,
renderTrigger,
canDelete = true,
ghostButtonText,
isTemporal,
}) => {
// Get columns from Redux state
const columns = useSelector<ExplorePageState>(
state => state.explore.datasource?.columns || [],
) as any[];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<DndColumnSelectControl
name={name}
value={value}
onChange={onChange}
options={columns}
multi={multi}
canDelete={canDelete}
ghostButtonText={
ghostButtonText || (multi ? 'Drop columns here' : 'Drop column here')
}
isTemporal={isTemporal}
type="DndColumnSelect"
actions={
{
setControlValue: (controlName: string, value: any) => ({
type: 'SET_CONTROL_VALUE',
controlName,
value,
validationErrors: undefined,
}),
} as any
}
/>
</div>
);
};

View File

@@ -1,83 +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 { ReactElement } from 'react';
// @ts-ignore
import { DndFilterSelect as DndFilterSelectControl } from '../../../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import { Datasource } from '../../types';
export interface DndFilterSelectProps {
value?: any[];
onChange: (value: any[]) => void;
datasource?: Datasource;
columns?: any[];
formData?: any;
savedMetrics?: any[];
selectedMetrics?: any[];
name?: string;
actions?: any;
type?: string;
[key: string]: any;
}
/**
* Wrapper around the existing DndFilterSelect that simplifies its API
*/
export const DndFilterSelect: React.FC<DndFilterSelectProps> = ({
value = [],
onChange,
datasource,
columns = [],
formData = {},
savedMetrics = [],
selectedMetrics = [],
name = 'adhoc_filters',
actions,
type = 'DndFilterSelect',
...restProps
}): ReactElement => {
// Handle the case where onChange needs to be wrapped for actions.setControlValue
const handleChange = (val: any) => {
if (actions?.setControlValue) {
actions.setControlValue(name, val);
} else if (onChange) {
onChange(val);
}
};
// For compatibility with the original component
const componentProps = {
value,
onChange: handleChange,
datasource,
columns,
formData,
name,
savedMetrics,
selectedMetrics,
type,
actions,
...restProps,
};
return (
<div className="filter-select-wrapper">
<DndFilterSelectControl {...componentProps} />
</div>
);
};

View File

@@ -1,95 +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 { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import { DndMetricSelect as DndMetricSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
import { ControlHeader } from '../ControlHeader';
export interface DndMetricSelectProps {
name: string;
value?: any;
onChange: (value: any) => void;
label?: string;
description?: string;
multi?: boolean;
required?: boolean;
renderTrigger?: boolean;
canDelete?: boolean;
ghostButtonText?: string;
}
/**
* Wrapper around the existing DndMetricSelect that simplifies its API
*/
export const DndMetricSelect: React.FC<DndMetricSelectProps> = ({
name,
value,
onChange,
label,
description,
multi = false,
required,
renderTrigger,
canDelete = true,
ghostButtonText,
}) => {
// Get datasource from Redux state
const datasource = useSelector<ExplorePageState>(
state => state.explore.datasource,
) as any;
const columns = datasource?.columns || [];
const savedMetrics = datasource?.metrics || [];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<DndMetricSelectControl
name={name}
value={value}
onChange={onChange}
columns={columns}
savedMetrics={savedMetrics}
multi={multi}
canDelete={canDelete}
ghostButtonText={
ghostButtonText || (multi ? 'Drop metrics here' : 'Drop metric here')
}
type="DndMetricSelect"
actions={
{
setControlValue: (controlName: string, value: any) => ({
type: 'SET_CONTROL_VALUE',
controlName,
value,
validationErrors: undefined,
}),
} as any
}
/>
</div>
);
};

View File

@@ -1,43 +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 { ReactElement } from 'react';
// @ts-ignore
import SelectControlComponent from '../../../../../../../src/explore/components/controls/SelectControl';
export interface SelectControlProps {
value?: any;
onChange: (value: any) => void;
choices?: Array<[string | number, string]>;
clearable?: boolean;
multi?: boolean;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
hovered?: boolean;
placeholder?: string;
[key: string]: any;
}
/**
* Select control component
*/
export const SelectControl: React.FC<SelectControlProps> = (
props,
): ReactElement => <SelectControlComponent {...props} />;

View File

@@ -1,80 +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 React from 'react';
import { Select } from '@superset-ui/core/components';
import { ControlHeader } from '../ControlHeader';
export interface SelectControlProps {
name: string;
value?: string | string[];
onChange: (value: string | string[]) => void;
label?: string;
description?: string;
placeholder?: string;
disabled?: boolean;
choices: Array<[string, string]>;
clearable?: boolean;
multiple?: boolean;
renderTrigger?: boolean;
required?: boolean;
}
export const SelectControl: React.FC<SelectControlProps> = ({
name,
value,
onChange,
label,
description,
placeholder,
disabled,
choices,
clearable = true,
multiple = false,
renderTrigger,
required,
}) => {
const options = choices.map(([val, label]) => ({
value: val,
label,
}));
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<Select
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
options={options}
allowClear={clearable}
mode={multiple ? 'multiple' : undefined}
css={{ width: '100%' }}
/>
</div>
);
};

View File

@@ -1,94 +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 React, { useCallback } from 'react';
import { Input } from '@superset-ui/core/components';
import { ControlHeader } from '../ControlHeader';
export interface TextControlProps {
name: string;
value?: string | number;
onChange: (value: string | number) => void;
label?: string;
description?: string;
placeholder?: string;
disabled?: boolean;
isInt?: boolean;
isFloat?: boolean;
min?: number;
max?: number;
renderTrigger?: boolean;
required?: boolean;
}
export const TextControl: React.FC<TextControlProps> = ({
name,
value,
onChange,
label,
description,
placeholder,
disabled,
isInt,
isFloat,
min,
max,
renderTrigger,
required,
}) => {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
let newValue: string | number = e.target.value;
if (isInt) {
newValue = parseInt(newValue, 10) || 0;
} else if (isFloat) {
newValue = parseFloat(newValue) || 0;
}
onChange(newValue);
},
[onChange, isInt, isFloat],
);
const inputType = isInt || isFloat ? 'number' : 'text';
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<Input
name={name}
type={inputType}
value={value ?? ''}
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
min={min}
max={max}
css={{ width: '100%' }}
/>
</div>
);
};

View File

@@ -1,41 +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 { ReactElement } from 'react';
// @ts-ignore
import SliderControlComponent from '../../../../../../../src/explore/components/controls/SliderControl';
export interface SliderControlProps {
value?: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
[key: string]: any;
}
/**
* Slider control component
*/
export const SliderControl: React.FC<SliderControlProps> = (
props,
): ReactElement => <SliderControlComponent {...props} />;

View File

@@ -1,39 +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 { ReactElement } from 'react';
// @ts-ignore
import TextControlComponent from '../../../../../../../src/explore/components/controls/TextControl';
export interface TextControlProps {
value?: string | number;
onChange: (value: string | number) => void;
placeholder?: string;
isInt?: boolean;
isFloat?: boolean;
disabled?: boolean;
controlId?: string;
[key: string]: any;
}
/**
* Text input control component
*/
export const TextControl: React.FC<TextControlProps> = (
props,
): ReactElement => <TextControlComponent {...props} />;

View File

@@ -1,39 +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.
*/
// Layout components
export { ControlPanelSection } from './ControlPanelSection';
export { ControlRow } from './ControlRow';
export { ControlHeader } from './ControlHeader';
// Control components
export { TextControl as SimpleTextControl } from './controls/SimpleTextControl';
export { SelectControl as SimpleSelectControl } from './controls/SimpleSelectControl';
// Wrapper controls for new simplified API
export { DndColumnSelect } from './controls/DndColumnSelectWrapper';
export { DndMetricSelect } from './controls/DndMetricSelectWrapper';
export { DndFilterSelect } from './controls/DndFilterSelectWrapper';
export { AdhocFilterControl } from './controls/AdhocFilterControlWrapper';
export { ColorSchemeControl as SimpleColorSchemeControl } from './controls/ColorSchemeControlWrapper';
export { TextControl } from './controls/TextControlWrapper';
export { CheckboxControl } from './controls/CheckboxControlWrapper';
export { SelectControl } from './controls/SelectControlWrapper';
export { SliderControl } from './controls/SliderControlWrapper';
export { Control } from './controls/ControlWrapper';

View File

@@ -1,81 +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 { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { QueryFormData } from '@superset-ui/core';
// Define minimal types to avoid circular dependencies
interface ExploreState {
form_data: QueryFormData;
[key: string]: any;
}
interface RootState {
explore: ExploreState;
[key: string]: any;
}
/**
* Hook to access and update form data from Redux store
* Provides a simple interface for control components
*
* NOTE: This hook assumes the Redux store structure used by Superset's explore view.
* The actual setControlValue action should be provided by the app via context or props.
*/
export function useFormData() {
const dispatch = useDispatch();
// Get form data from Redux
const formData = useSelector<RootState, QueryFormData>(state => {
console.log('useFormData - state.explore:', state.explore);
return state.explore?.form_data || {};
});
// Update a single control value
// This is a placeholder - the actual action should be injected
const updateControl = useCallback(
(controlName: string, value: any) => {
console.log('updateControl:', controlName, value);
// In production, this would dispatch the actual setControlValue action
// For now, we'll dispatch a generic action
dispatch({
type: 'SET_CONTROL_VALUE',
controlName,
value,
});
},
[dispatch],
);
// Update multiple controls at once
const updateControls = useCallback(
(updates: Partial<QueryFormData>) => {
Object.entries(updates).forEach(([key, value]) => {
updateControl(key, value);
});
},
[updateControl],
);
return {
formData,
updateControl,
updateControls,
};
}

View File

@@ -21,7 +21,6 @@ import * as sectionsModule from './sections';
export * from './utils';
export * from './constants';
export * from './operators';
export * from './hooks/useFormData';
// can't do `export * as sections from './sections'`, babel-transformer will fail
export const sections = sectionsModule;
@@ -33,26 +32,7 @@ export * from './components/Dropdown';
export * from './components/Menu';
export * from './components/MetricOption';
export * from './components/ControlHeader';
export * from './components';
// Export individual control components for easier access
export {
DndColumnSelect,
DndMetricSelect,
DndFilterSelect,
TextControl,
CheckboxControl,
SelectControl,
SliderControl,
Control,
} from './components';
export * from './shared-controls';
export {
GranularityControl,
RadioButtonControl,
ReactControlPanel,
} from './shared-controls/components';
// Export all from shared-controls/components which includes inline control functions
export * from './shared-controls/components';
export * from './types';
export * from './fixtures';

View File

@@ -18,20 +18,6 @@
*/
import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig, ControlSetRow } from '../types';
import {
AdhocFiltersControl,
GroupByControl,
GroupOthersWhenLimitReachedControl,
LimitControl,
MetricsControl,
OrderDescControl,
RowLimitControl,
ShowEmptyColumnsControl,
TimeGrainSqlaControl,
TimeLimitMetricControl,
TruncateMetricControl,
XAxisControl,
} from '../shared-controls/components/SharedControlComponents';
import {
contributionModeControl,
xAxisForceCategoricalControl,
@@ -40,34 +26,30 @@ import {
} from '../shared-controls';
const controlsWithoutXAxis: ControlSetRow[] = [
[MetricsControl()],
[GroupByControl()],
['metrics'],
['groupby'],
[contributionModeControl],
[AdhocFiltersControl()],
[LimitControl(), GroupOthersWhenLimitReachedControl()],
[TimeLimitMetricControl()],
[OrderDescControl()],
[RowLimitControl()],
[TruncateMetricControl()],
[ShowEmptyColumnsControl()],
['adhoc_filters'],
['limit'],
['timeseries_limit_metric'],
['order_desc'],
['row_limit'],
['truncate_metric'],
['show_empty_columns'],
];
export const echartsTimeSeriesQuery: ControlPanelSectionConfig = {
label: t('Query'),
expanded: true,
controlSetRows: [
[XAxisControl()],
[TimeGrainSqlaControl()],
...controlsWithoutXAxis,
],
controlSetRows: [['x_axis'], ['time_grain_sqla'], ...controlsWithoutXAxis],
};
export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
label: t('Query'),
expanded: true,
controlSetRows: [
[XAxisControl()],
[TimeGrainSqlaControl()],
['x_axis'],
['time_grain_sqla'],
[xAxisForceCategoricalControl],
[xAxisSortControl],
[xAxisSortAscControl],

View File

@@ -18,15 +18,6 @@
*/
import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig } from '../types';
import {
GranularityControl,
GranularitySqlaControl,
TimeGrainSqlaControl,
TimeRangeControl,
DatasourceControl,
VizTypeControl,
ColorSchemeControl,
} from '../shared-controls/components/SharedControlComponents';
// A few standard controls sections that are used internally.
// Not recommended for use in third-party plugins.
@@ -40,10 +31,10 @@ const baseTimeSection = {
export const legacyTimeseriesTime: ControlPanelSectionConfig = {
...baseTimeSection,
controlSetRows: [
[GranularityControl()],
[GranularitySqlaControl()],
[TimeGrainSqlaControl()],
[TimeRangeControl()],
['granularity'],
['granularity_sqla'],
['time_grain_sqla'],
['time_range'],
],
};
@@ -51,8 +42,8 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
label: t('Datasource & Chart Type'),
expanded: true,
controlSetRows: [
[DatasourceControl()],
[VizTypeControl()],
['datasource'],
['viz_type'],
[
{
name: 'slice_id',
@@ -100,7 +91,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
export const colorScheme: ControlPanelSectionConfig = {
label: t('Color Scheme'),
controlSetRows: [[ColorSchemeControl()]],
controlSetRows: [['color_scheme']],
};
export const annotations: ControlPanelSectionConfig = {

View File

@@ -1,252 +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 { Input, Select, Switch, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface AxisControlSectionProps {
axis: 'x' | 'y';
showTitle?: boolean;
showFormat?: boolean;
showRotation?: boolean;
showBounds?: boolean;
showLogarithmic?: boolean;
showMinorTicks?: boolean;
showTruncate?: boolean;
timeFormat?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const D3_FORMAT_OPTIONS = [
['SMART_NUMBER', t('Adaptive formatting')],
['~g', t('Original value')],
['d', t('Signed integer')],
['.1f', t('1 decimal place')],
['.2f', t('2 decimal places')],
['.3f', t('3 decimal places')],
['+,', t('Positive integer')],
['$,.2f', t('Currency (2 decimals)')],
[',.0%', t('Percentage')],
['.1%', t('Percentage (1 decimal)')],
];
const D3_TIME_FORMAT_OPTIONS = [
['smart_date', t('Adaptive formatting')],
['%Y-%m-%d', t('2023-01-01')],
['%Y-%m-%d %H:%M', t('2023-01-01 10:30')],
['%m/%d/%Y', t('01/01/2023')],
['%d/%m/%Y', t('01/01/2023')],
['%Y', t('2023')],
['%B %Y', t('January 2023')],
['%b %Y', t('Jan 2023')],
['%B %-d, %Y', t('January 1, 2023')],
];
const ROTATION_OPTIONS = [
[0, '0°'],
[45, '45°'],
[90, '90°'],
[-45, '-45°'],
[-90, '-90°'],
];
export const AxisControlSection: FC<AxisControlSectionProps> = ({
axis,
showTitle = true,
showFormat = true,
showRotation = false,
showBounds = false,
showLogarithmic = false,
showMinorTicks = false,
showTruncate = false,
timeFormat = false,
values = {},
onChange = () => {},
}) => {
const isXAxis = axis === 'x';
const axisUpper = axis.toUpperCase();
const titleKey = `${axis}_axis_title`;
const formatKey = timeFormat
? `${axis}_axis_time_format`
: `${axis}_axis_format`;
const rotationKey = `${axis}_axis_label_rotation`;
const boundsMinKey = `${axis}_axis_bounds_min`;
const boundsMaxKey = `${axis}_axis_bounds_max`;
const logScaleKey = `log_scale`;
const minorTicksKey = `${axis}_axis_minor_ticks`;
const truncateKey = `truncate_${axis}axis`;
const truncateLabelsKey = `${axis}_axis_truncate_labels`;
return (
<div className="axis-control-section">
{showTitle && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Title`)}</label>
<Input
value={values[titleKey] || ''}
onChange={e => onChange(titleKey, e.target.value)}
placeholder={t(`Enter ${axis} axis title`)}
/>
<small className="text-muted">
{t(
'Overrides the axis title derived from the metric or column name',
)}
</small>
</Col>
</Row>
)}
{showFormat && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Format`)}</label>
<Select
value={
values[formatKey] ||
(timeFormat ? 'smart_date' : 'SMART_NUMBER')
}
onChange={value => onChange(formatKey, value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a format')}
options={(timeFormat
? D3_TIME_FORMAT_OPTIONS
: D3_FORMAT_OPTIONS
).map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{timeFormat
? t('D3 time format for x axis')
: t('D3 format for axis values')}
</small>
</Col>
</Row>
)}
{showRotation && isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Rotation')}</label>
<Select
value={values[rotationKey] || 0}
onChange={value => onChange(rotationKey, value)}
style={{ width: '100%' }}
options={ROTATION_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Rotation angle for axis labels')}
</small>
</Col>
</Row>
)}
{showBounds && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Bounds`)}</label>
<div style={{ display: 'flex', gap: 8 }}>
<InputNumber
value={values[boundsMinKey]}
onChange={value => onChange(boundsMinKey, value)}
placeholder={t('Min')}
style={{ flex: 1 }}
/>
<InputNumber
value={values[boundsMaxKey]}
onChange={value => onChange(boundsMaxKey, value)}
placeholder={t('Max')}
style={{ flex: 1 }}
/>
</div>
<small className="text-muted">
{t('Bounds for axis values. Leave empty for automatic scaling.')}
</small>
</Col>
</Row>
)}
{showLogarithmic && !isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values[logScaleKey] || false}
onChange={checked => onChange(logScaleKey, checked)}
/>
{t('Logarithmic Scale')}
</label>
<small className="text-muted">
{t('Use a logarithmic scale for the Y-axis')}
</small>
</Col>
</Row>
)}
{showMinorTicks && !isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values[minorTicksKey] || false}
onChange={checked => onChange(minorTicksKey, checked)}
/>
{t('Show Minor Ticks')}
</label>
<small className="text-muted">
{t('Show minor grid lines on the axis')}
</small>
</Col>
</Row>
)}
{showTruncate && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={
values[truncateKey] || values[truncateLabelsKey] || false
}
onChange={checked => {
onChange(truncateKey, checked);
onChange(truncateLabelsKey, checked);
}}
/>
{t(`Truncate ${axisUpper} Axis Labels`)}
</label>
<small className="text-muted">
{t('Truncate long axis labels to prevent overlap')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default AxisControlSection;

View File

@@ -1,634 +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 { ReactElement } from 'react';
import type { CustomControlItem, ControlValueValidator } from '../../types';
// Base control props that all controls share
interface BaseControlProps {
name: string;
label?: ReactElement | string;
description?: string;
default?: any;
renderTrigger?: boolean;
validators?: ControlValueValidator[];
warning?: string;
error?: string;
mapStateToProps?: (state: any, control: any) => any;
visibility?: (props: any) => boolean;
value?: any;
onChange?: (value: any) => void;
}
// Use the existing CustomControlItem type instead of creating a duplicate
// This ensures type compatibility with the rest of the codebase
export type ControlComponentConfig = CustomControlItem;
// CheckboxControl Component
interface CheckboxControlProps extends BaseControlProps {
default?: boolean;
}
export const CheckboxControl = (
props: CheckboxControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'CheckboxControl',
label: props.label,
description: props.description,
default: props.default ?? false,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SelectControl Component
interface SelectControlProps extends BaseControlProps {
choices?: Array<[string, string]> | (() => Array<[string, string]>);
clearable?: boolean;
freeForm?: boolean;
multi?: boolean;
placeholder?: string;
optionRenderer?: (option: any) => ReactElement;
valueRenderer?: (value: any) => ReactElement;
valueKey?: string;
labelKey?: string;
}
export const SelectControl = (
props: SelectControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectControl',
label: props.label,
description: props.description,
choices: props.choices ?? [],
clearable: props.clearable ?? true,
freeForm: props.freeForm ?? false,
multi: props.multi ?? false,
default: props.default,
renderTrigger: props.renderTrigger ?? false,
placeholder: props.placeholder,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
validators: props.validators,
warning: props.warning,
error: props.error,
optionRenderer: props.optionRenderer,
valueRenderer: props.valueRenderer,
valueKey: props.valueKey,
labelKey: props.labelKey,
},
});
// TextControl Component
interface TextControlProps extends BaseControlProps {
placeholder?: string;
disabled?: boolean;
isInt?: boolean;
isFloat?: boolean;
}
export const TextControl = (
props: TextControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
default: props.default ?? '',
renderTrigger: props.renderTrigger ?? false,
disabled: props.disabled ?? false,
isInt: props.isInt ?? false,
isFloat: props.isFloat ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// TextAreaControl Component
interface TextAreaControlProps extends BaseControlProps {
placeholder?: string;
rows?: number;
language?: 'json' | 'html' | 'sql' | 'markdown' | 'javascript';
offerEditInModal?: boolean;
disabled?: boolean;
}
export const TextAreaControl = (
props: TextAreaControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextAreaControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
rows: props.rows ?? 3,
language: props.language,
offerEditInModal: props.offerEditInModal ?? true,
default: props.default ?? '',
renderTrigger: props.renderTrigger ?? false,
disabled: props.disabled ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SliderControl Component
interface SliderControlProps extends BaseControlProps {
min?: number;
max?: number;
step?: number;
default?: number;
}
export const SliderControl = (
props: SliderControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SliderControl',
label: props.label,
description: props.description,
min: props.min ?? 0,
max: props.max ?? 100,
step: props.step ?? 1,
default: props.default ?? 0,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// RadioButtonControl Component
interface RadioButtonControlProps extends BaseControlProps {
options?: Array<[string, string | ReactElement]>;
default?: string;
}
export const RadioButtonControl = (
props: RadioButtonControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'RadioButtonControl',
label: props.label,
description: props.description,
options: props.options ?? [],
default: props.default,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// NumberControl Component
interface NumberControlProps extends BaseControlProps {
min?: number;
max?: number;
default?: number;
placeholder?: string;
}
export const NumberControl = (
props: NumberControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
default: props.default,
renderTrigger: props.renderTrigger ?? false,
isFloat: true,
controlHeader: {
label: props.label,
description: props.description,
},
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
min: props.min,
max: props.max,
},
});
// ColorPickerControl Component
interface ColorPickerControlProps extends BaseControlProps {
default?: { r: number; g: number; b: number; a?: number };
}
export const ColorPickerControl = (
props: ColorPickerControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColorPickerControl',
label: props.label,
description: props.description,
default: props.default ?? { r: 0, g: 122, b: 135, a: 1 },
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// DateFilterControl Component
interface DateFilterControlProps extends BaseControlProps {
default?: string;
}
export const DateFilterControl = (
props: DateFilterControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'DateFilterControl',
label: props.label,
description: props.description,
default: props.default,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
renderTrigger: props.renderTrigger,
},
});
// BoundsControl Component
interface BoundsControlProps extends BaseControlProps {
default?: [number | null, number | null];
min?: number;
max?: number;
}
export const BoundsControl = (
props: BoundsControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'BoundsControl',
label: props.label,
description: props.description,
default: props.default ?? [null, null],
min: props.min,
max: props.max,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SwitchControl Component
interface SwitchControlProps extends BaseControlProps {
default?: boolean;
}
export const SwitchControl = (
props: SwitchControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'CheckboxControl',
label: props.label,
description: props.description,
default: props.default ?? false,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// HiddenControl Component (for hidden fields)
interface HiddenControlProps {
name: string;
value?: any;
default?: any;
}
export const HiddenControl = (
props: HiddenControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'HiddenControl',
default: props.default,
value: props.value,
renderTrigger: false,
visible: false,
},
});
// MetricsControl Component
interface MetricsControlProps extends BaseControlProps {
multi?: boolean;
clearable?: boolean;
savedMetrics?: any[];
columns?: any[];
datasourceType?: string;
}
export const MetricsControl = (
props: MetricsControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'MetricsControl',
label: props.label,
description: props.description,
multi: props.multi ?? true,
clearable: props.clearable ?? true,
validators: props.validators ?? [],
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
columns: state.datasource?.columns || [],
savedMetrics: state.datasource?.metrics || [],
datasourceType: state.datasource?.type,
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
savedMetrics: props.savedMetrics,
columns: props.columns,
datasourceType: props.datasourceType,
},
});
// GroupByControl Component
interface GroupByControlProps extends BaseControlProps {
multi?: boolean;
clearable?: boolean;
columns?: any[];
}
export const GroupByControl = (
props: GroupByControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectControl',
label: props.label,
description: props.description,
multi: props.multi ?? true,
clearable: props.clearable ?? true,
validators: props.validators ?? [],
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
choices: state.datasource?.columns || [],
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
columns: props.columns,
},
});
// AdhocFilterControl Component
interface AdhocFilterControlProps extends BaseControlProps {
columns?: any[];
savedMetrics?: any[];
datasourceType?: string;
}
export const AdhocFilterControl = (
props: AdhocFilterControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'AdhocFilterControl',
label: props.label,
description: props.description,
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
columns: state.datasource?.columns || [],
savedMetrics: state.datasource?.metrics || [],
datasourceType: state.datasource?.type,
})),
default: props.default,
renderTrigger: props.renderTrigger,
validators: props.validators,
warning: props.warning,
error: props.error,
visibility: props.visibility,
columns: props.columns,
savedMetrics: props.savedMetrics,
datasourceType: props.datasourceType,
},
});
// SpatialControl Component
interface SpatialControlProps extends BaseControlProps {
choices?: any[];
}
export const SpatialControl = (
props: SpatialControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SpatialControl',
label: props.label,
description: props.description,
validators: props.validators,
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
choices: state.datasource?.columns || [],
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
},
});
// ColorSchemeControl Component
interface ColorSchemeControlProps extends BaseControlProps {
choices?: (() => Array<[string, string]>) | Array<[string, string]>;
schemes?: () => any;
isLinear?: boolean;
}
export const ColorSchemeControl = (
props: ColorSchemeControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColorSchemeControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
choices: props.choices,
schemes: props.schemes,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
isLinear: props.isLinear,
},
});
// SelectAsyncControl Component
interface SelectAsyncControlProps extends BaseControlProps {
dataEndpoint?: string;
multi?: boolean;
mutator?: (data: any) => any;
placeholder?: string;
onAsyncErrorMessage?: string;
cacheOptions?: boolean;
}
export const SelectAsyncControl = (
props: SelectAsyncControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectAsyncControl',
label: props.label,
description: props.description,
default: props.default,
dataEndpoint: props.dataEndpoint,
multi: props.multi ?? false,
mutator: props.mutator,
placeholder: props.placeholder,
onAsyncErrorMessage: props.onAsyncErrorMessage,
cacheOptions: props.cacheOptions ?? true,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
renderTrigger: props.renderTrigger,
},
});
// ContourControl Component
interface ContourControlProps extends BaseControlProps {
renderTrigger?: boolean;
choices?: Array<[string, string]>;
}
export const ContourControl = (
props: ContourControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ContourControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
choices: props.choices,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// ColumnConfigControl Component
interface ColumnConfigControlProps extends BaseControlProps {
renderTrigger?: boolean;
}
export const ColumnConfigControl = (
props: ColumnConfigControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColumnConfigControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// Export all components
export default {
CheckboxControl,
SelectControl,
TextControl,
TextAreaControl,
SliderControl,
RadioButtonControl,
NumberControl,
ColorPickerControl,
DateFilterControl,
BoundsControl,
SwitchControl,
HiddenControl,
MetricsControl,
GroupByControl,
AdhocFilterControl,
SpatialControl,
ColorSchemeControl,
SelectAsyncControl,
ContourControl,
ColumnConfigControl,
};

View File

@@ -1,154 +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 { ReactNode, FC } from 'react';
import { Row, Col, Collapse } from '@superset-ui/core/components';
/**
* Props for control panel sections
*/
export interface ControlSectionProps {
label?: ReactNode;
description?: ReactNode;
expanded?: boolean;
children: ReactNode;
}
/**
* A collapsible section in the control panel
*/
export const ControlSection: FC<ControlSectionProps> = ({
label,
description,
expanded = true,
children,
}) => {
if (!label) {
// No label means no collapsible wrapper
return <>{children}</>;
}
return (
<Collapse defaultActiveKey={expanded ? ['1'] : []} ghost>
<Collapse.Panel
header={
<span>
{label}
{description && (
<span style={{ marginLeft: 8, fontSize: '0.85em', opacity: 0.7 }}>
{description}
</span>
)}
</span>
}
key="1"
>
{children}
</Collapse.Panel>
</Collapse>
);
};
/**
* Props for control row - uses Ant Design grid
*/
export interface ControlRowProps {
children: ReactNode;
gutter?: number | [number, number];
}
/**
* A row of controls using Ant Design's grid system
* Automatically distributes controls evenly across columns
*/
export const ControlPanelRow: FC<ControlRowProps> = ({
children,
gutter = [16, 16],
}) => {
const childArray = Array.isArray(children) ? children : [children];
const validChildren = childArray.filter(
child => child !== null && child !== undefined,
);
const colSpan =
validChildren.length > 0 ? Math.floor(24 / validChildren.length) : 24;
return (
<Row gutter={gutter} style={{ marginBottom: 16 }}>
{validChildren.map((child, index) => (
<Col key={index} span={colSpan}>
{child}
</Col>
))}
</Row>
);
};
/**
* Props for the main control panel layout
*/
export interface ControlPanelLayoutProps {
children: ReactNode;
}
/**
* Main control panel layout container
*/
export const ControlPanelLayout: FC<ControlPanelLayoutProps> = ({
children,
}) => (
<div className="control-panel-layout" style={{ padding: '16px 0' }}>
{children}
</div>
);
/**
* Helper function to create a full-width single control row
*/
export const SingleControlRow: FC<{ children: ReactNode }> = ({ children }) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={24}>{children}</Col>
</Row>
);
/**
* Helper function to create a two-column control row
*/
export const TwoColumnRow: FC<{ left: ReactNode; right: ReactNode }> = ({
left,
right,
}) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={12}>{left}</Col>
<Col span={12}>{right}</Col>
</Row>
);
/**
* Helper function to create a three-column control row
*/
export const ThreeColumnRow: FC<{
left: ReactNode;
center: ReactNode;
right: ReactNode;
}> = ({ left, center, right }) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={8}>{left}</Col>
<Col span={8}>{center}</Col>
<Col span={8}>{right}</Col>
</Row>
);

View File

@@ -1,360 +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 { Switch, Select, Input, Slider } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface DeckGLControlsSectionProps {
layerType?:
| 'scatter'
| 'polygon'
| 'path'
| 'heatmap'
| 'hex'
| 'grid'
| 'screengrid'
| 'contour'
| 'geojson'
| 'arc';
showViewport?: boolean;
showMapStyle?: boolean;
showColorScheme?: boolean;
showLegend?: boolean;
showTooltip?: boolean;
showFilters?: boolean;
showAnimation?: boolean;
show3D?: boolean;
showMultiplier?: boolean;
showPointRadius?: boolean;
showLineWidth?: boolean;
showFillColor?: boolean;
showStrokeColor?: boolean;
showOpacity?: boolean;
showCoverage?: boolean;
showElevation?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const DeckGLControlsSection: FC<DeckGLControlsSectionProps> = ({
layerType = 'scatter',
showViewport = true,
showMapStyle = true,
showColorScheme = true,
showLegend = true,
showTooltip = true,
showFilters = true,
showAnimation = false,
show3D = false,
showMultiplier = false,
showPointRadius = false,
showLineWidth = false,
showFillColor = false,
showStrokeColor = false,
showOpacity = true,
showCoverage = false,
showElevation = false,
values = {},
onChange = () => {},
}) => (
<div className="deckgl-controls-section">
{/* Map Style */}
{showMapStyle && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Map Style')}</label>
<Select
value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'}
onChange={value => onChange('mapbox_style', value)}
style={{ width: '100%' }}
options={[
{
value: 'mapbox://styles/mapbox/streets-v11',
label: t('Streets'),
},
{ value: 'mapbox://styles/mapbox/light-v9', label: t('Light') },
{ value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') },
{
value: 'mapbox://styles/mapbox/satellite-v9',
label: t('Satellite'),
},
{
value: 'mapbox://styles/mapbox/outdoors-v11',
label: t('Outdoors'),
},
]}
/>
<small className="text-muted">
{t('Base map style for the visualization')}
</small>
</Col>
</Row>
)}
{/* Viewport */}
{showViewport && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Zoom')}</label>
<Slider
value={values.zoom || 11}
onChange={value => onChange('zoom', value)}
min={0}
max={22}
step={0.1}
marks={{ 0: '0', 11: '11', 22: '22' }}
/>
<small className="text-muted">{t('Map zoom level')}</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.autozoom || true}
onChange={checked => onChange('autozoom', checked)}
/>
{t('Auto Zoom')}
</label>
<small className="text-muted">
{t('Automatically zoom to fit data bounds')}
</small>
</Col>
</Row>
</>
)}
{/* Point/Shape Size Controls */}
{showPointRadius && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Point Radius')}</label>
<Slider
value={values.point_radius_fixed?.value || 1000}
onChange={value =>
onChange('point_radius_fixed', { type: 'fix', value })
}
min={1}
max={10000}
step={10}
/>
<small className="text-muted">
{t('Fixed radius for points in meters')}
</small>
</Col>
</Row>
)}
{showLineWidth && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Line Width')}</label>
<Slider
value={values.line_width || 1}
onChange={value => onChange('line_width', value)}
min={1}
max={50}
step={1}
/>
<small className="text-muted">{t('Width of lines in pixels')}</small>
</Col>
</Row>
)}
{/* 3D Controls */}
{show3D && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.extruded || false}
onChange={checked => onChange('extruded', checked)}
/>
{t('3D')}
</label>
<small className="text-muted">{t('Show data in 3D')}</small>
</Col>
</Row>
{values.extruded && showElevation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Elevation')}</label>
<Slider
value={values.elevation || 0.1}
onChange={value => onChange('elevation', value)}
min={0}
max={1}
step={0.01}
/>
<small className="text-muted">
{t('Elevation multiplier for 3D rendering')}
</small>
</Col>
</Row>
)}
</>
)}
{/* Opacity */}
{showOpacity && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Opacity')}</label>
<Slider
value={values.opacity || 80}
onChange={value => onChange('opacity', value)}
min={0}
max={100}
step={1}
marks={{ 0: '0%', 50: '50%', 100: '100%' }}
/>
<small className="text-muted">{t('Layer opacity')}</small>
</Col>
</Row>
)}
{/* Coverage (for hex, grid) */}
{showCoverage && (layerType === 'hex' || layerType === 'grid') && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Coverage')}</label>
<Slider
value={values.coverage || 1}
onChange={value => onChange('coverage', value)}
min={0}
max={1}
step={0.01}
/>
<small className="text-muted">{t('Cell coverage radius')}</small>
</Col>
</Row>
)}
{/* Legend */}
{showLegend && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Legend Position')}</label>
<Select
value={values.legend_position || 'top_right'}
onChange={value => onChange('legend_position', value)}
style={{ width: '100%' }}
options={[
{ value: 'top_left', label: t('Top left') },
{ value: 'top_right', label: t('Top right') },
{ value: 'bottom_left', label: t('Bottom left') },
{ value: 'bottom_right', label: t('Bottom right') },
]}
/>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Legend Format')}</label>
<Input
value={values.legend_format || ''}
onChange={e => onChange('legend_format', e.target.value)}
placeholder=".3s"
/>
<small className="text-muted">
{t('D3 number format for legend')}
</small>
</Col>
</Row>
</>
)}
{/* Filters */}
{showFilters && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.filter_nulls || true}
onChange={checked => onChange('filter_nulls', checked)}
/>
{t('Filter Nulls')}
</label>
<small className="text-muted">
{t('Filter out null values from data')}
</small>
</Col>
</Row>
)}
{/* Tooltip */}
{showTooltip && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Tooltip')}</label>
<Input.TextArea
value={values.js_tooltip || ''}
onChange={e => onChange('js_tooltip', e.target.value)}
placeholder={t('JavaScript tooltip generator')}
rows={3}
/>
<small className="text-muted">
{t('JavaScript code for custom tooltip')}
</small>
</Col>
</Row>
)}
{/* Animation */}
{showAnimation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.animation || false}
onChange={checked => onChange('animation', checked)}
/>
{t('Animate')}
</label>
<small className="text-muted">
{t('Animate visualization over time')}
</small>
</Col>
</Row>
)}
{/* Multiplier for some visualizations */}
{showMultiplier && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Multiplier')}</label>
<Slider
value={values.multiplier || 1}
onChange={value => onChange('multiplier', value)}
min={0.01}
max={10}
step={0.01}
/>
<small className="text-muted">{t('Value multiplier')}</small>
</Col>
</Row>
)}
</div>
);
export default DeckGLControlsSection;

View File

@@ -1,336 +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 { Switch, Select, Input, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface FilterControlsSectionProps {
filterType: 'select' | 'range' | 'time' | 'time_column' | 'time_grain';
showMultiple?: boolean;
showSearch?: boolean;
showParentFilter?: boolean;
showDefaultValue?: boolean;
showInverseSelection?: boolean;
showDateFilter?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const FilterControlsSection: FC<FilterControlsSectionProps> = ({
filterType,
showMultiple = true,
showSearch = true,
showParentFilter = true,
showDefaultValue = true,
showInverseSelection = false,
showDateFilter = false,
values = {},
onChange = () => {},
}) => {
const isSelect = filterType === 'select';
const isRange = filterType === 'range';
const isTime = filterType === 'time';
const isTimeColumn = filterType === 'time_column';
const isTimeGrain = filterType === 'time_grain';
return (
<div className="filter-controls-section">
{/* Multiple Selection */}
{showMultiple && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.multiSelect || false}
onChange={checked => onChange('multiSelect', checked)}
/>
{t('Multiple Select')}
</label>
<small className="text-muted">
{t('Allow selecting multiple values')}
</small>
</Col>
</Row>
)}
{/* Search */}
{showSearch && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.enableEmptyFilter || false}
onChange={checked => onChange('enableEmptyFilter', checked)}
/>
{t('Enable Empty Filter')}
</label>
<small className="text-muted">
{t('Allow empty filter values')}
</small>
</Col>
</Row>
)}
{/* Inverse Selection */}
{showInverseSelection && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.inverseSelection || false}
onChange={checked => onChange('inverseSelection', checked)}
/>
{t('Inverse Selection')}
</label>
<small className="text-muted">
{t('Exclude selected values instead of including them')}
</small>
</Col>
</Row>
)}
{/* Parent Filter */}
{showParentFilter && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.parentFilter || false}
onChange={checked => onChange('parentFilter', checked)}
/>
{t('Parent Filter')}
</label>
<small className="text-muted">
{t('Filter is dependent on another filter')}
</small>
</Col>
</Row>
)}
{/* Default Value */}
{showDefaultValue && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Default Value')}</label>
{isSelect ? (
<Input
value={values.defaultValue || ''}
onChange={e => onChange('defaultValue', e.target.value)}
placeholder={t('Enter default value')}
/>
) : isRange ? (
<div style={{ display: 'flex', gap: 8 }}>
<InputNumber
value={values.defaultValueMin}
onChange={value => onChange('defaultValueMin', value)}
placeholder={t('Min')}
style={{ flex: 1 }}
/>
<InputNumber
value={values.defaultValueMax}
onChange={value => onChange('defaultValueMax', value)}
placeholder={t('Max')}
style={{ flex: 1 }}
/>
</div>
) : (
<Input
value={values.defaultValue || ''}
onChange={e => onChange('defaultValue', e.target.value)}
placeholder={t('Enter default value')}
/>
)}
<small className="text-muted">
{t('Default value to use when filter is first loaded')}
</small>
</Col>
</Row>
)}
{/* Sort Options for Select */}
{isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Sort Filter Values')}</label>
<Select
value={values.sortFilter || false}
onChange={value => onChange('sortFilter', value)}
style={{ width: '100%' }}
options={[
{ value: false, label: t('No Sort') },
{ value: true, label: t('Sort Ascending') },
{ value: 'desc', label: t('Sort Descending') },
]}
/>
<small className="text-muted">
{t('Sort filter values alphabetically')}
</small>
</Col>
</Row>
)}
{/* Search for Select Filter */}
{isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.searchAllOptions || false}
onChange={checked => onChange('searchAllOptions', checked)}
/>
{t('Search All Options')}
</label>
<small className="text-muted">
{t('Search all filter options, not just displayed ones')}
</small>
</Col>
</Row>
)}
{/* Range Options */}
{isRange && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Min Value')}</label>
<InputNumber
value={values.rangeMin}
onChange={value => onChange('rangeMin', value)}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t('Minimum value for the range')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Max Value')}</label>
<InputNumber
value={values.rangeMax}
onChange={value => onChange('rangeMax', value)}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t('Maximum value for the range')}
</small>
</Col>
</Row>
</>
)}
{/* Time Options */}
{(isTime || isTimeColumn) && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.defaultToFirstValue || false}
onChange={checked => onChange('defaultToFirstValue', checked)}
/>
{t('Default to First Value')}
</label>
<small className="text-muted">
{t('Default to the first available time value')}
</small>
</Col>
</Row>
)}
{/* Time Grain Options */}
{isTimeGrain && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Default Time Grain')}</label>
<Select
value={values.defaultTimeGrain || 'day'}
onChange={value => onChange('defaultTimeGrain', value)}
style={{ width: '100%' }}
options={[
{ value: 'minute', label: t('Minute') },
{ value: 'hour', label: t('Hour') },
{ value: 'day', label: t('Day') },
{ value: 'week', label: t('Week') },
{ value: 'month', label: t('Month') },
{ value: 'quarter', label: t('Quarter') },
{ value: 'year', label: t('Year') },
]}
/>
<small className="text-muted">
{t('Default time granularity')}
</small>
</Col>
</Row>
)}
{/* UI Configuration */}
<h4 style={{ marginTop: 24, marginBottom: 16 }}>
{t('UI Configuration')}
</h4>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.instant_filtering || true}
onChange={checked => onChange('instant_filtering', checked)}
/>
{t('Instant Filtering')}
</label>
<small className="text-muted">
{t('Apply filters instantly as they change')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_apply || false}
onChange={checked => onChange('show_apply', checked)}
/>
{t('Show Apply Button')}
</label>
<small className="text-muted">
{t('Show an apply button for the filter')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_clear || true}
onChange={checked => onChange('show_clear', checked)}
/>
{t('Show Clear Button')}
</label>
<small className="text-muted">
{t('Show a clear button for the filter')}
</small>
</Col>
</Row>
</div>
);
};
export default FilterControlsSection;

View File

@@ -1,241 +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 { Select } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface FormatControlGroupProps {
showNumber?: boolean;
showCurrency?: boolean;
showDate?: boolean;
showPercentage?: boolean;
numberFormatLabel?: string;
currencyFormatLabel?: string;
dateFormatLabel?: string;
percentageFormatLabel?: string;
customFormatOptions?: Array<[string, string]>;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
export const D3_FORMAT_OPTIONS = [
['SMART_NUMBER', t('Adaptive formatting')],
['~g', t('Original value')],
['d', t('Signed integer')],
['.0f', t('Integer')],
['.1f', t('1 decimal place')],
['.2f', t('2 decimal places')],
['.3f', t('3 decimal places')],
['.4f', t('4 decimal places')],
['.5f', t('5 decimal places')],
['+,', t('Positive integer')],
['+,.0f', t('Positive number')],
['+,.1f', t('Positive (1 decimal)')],
['+,.2f', t('Positive (2 decimals)')],
[',.0f', t('Number (no decimals)')],
[',.1f', t('Number (1 decimal)')],
[',.2f', t('Number (2 decimals)')],
[',.3f', t('Number (3 decimals)')],
['.0%', t('Percentage')],
['.1%', t('Percentage (1 decimal)')],
['.2%', t('Percentage (2 decimals)')],
['.3%', t('Percentage (3 decimals)')],
[',.0%', t('Percentage with thousands')],
['.1s', t('SI notation')],
['.2s', t('SI notation (2 decimals)')],
['.3s', t('SI notation (3 decimals)')],
['$,.0f', t('Currency (no decimals)')],
['$,.1f', t('Currency (1 decimal)')],
['$,.2f', t('Currency (2 decimals)')],
['$,.3f', t('Currency (3 decimals)')],
];
export const D3_TIME_FORMAT_OPTIONS = [
['smart_date', t('Adaptive formatting')],
['%Y-%m-%d', t('YYYY-MM-DD')],
['%Y-%m-%d %H:%M', t('YYYY-MM-DD HH:MM')],
['%Y-%m-%d %H:%M:%S', t('YYYY-MM-DD HH:MM:SS')],
['%Y/%m/%d', t('YYYY/MM/DD')],
['%m/%d/%Y', t('MM/DD/YYYY')],
['%d/%m/%Y', t('DD/MM/YYYY')],
['%d.%m.%Y', t('DD.MM.YYYY')],
['%Y', t('Year (YYYY)')],
['%B %Y', t('Month Year (January 2023)')],
['%b %Y', t('Month Year (Jan 2023)')],
['%B', t('Month (January)')],
['%b', t('Month (Jan)')],
['%B %-d, %Y', t('Month Day, Year')],
['%b %-d, %Y', t('Mon Day, Year')],
['%a', t('Day of week (short)')],
['%A', t('Day of week (full)')],
['%H:%M', t('Time (24-hour)')],
['%I:%M %p', t('Time (12-hour)')],
['%H:%M:%S', t('Time with seconds')],
];
const CURRENCY_OPTIONS = [
{ value: 'USD', label: 'USD ($)' },
{ value: 'EUR', label: 'EUR (€)' },
{ value: 'GBP', label: 'GBP (£)' },
{ value: 'JPY', label: 'JPY (¥)' },
{ value: 'CNY', label: 'CNY (¥)' },
{ value: 'INR', label: 'INR (₹)' },
{ value: 'CAD', label: 'CAD ($)' },
{ value: 'AUD', label: 'AUD ($)' },
{ value: 'CHF', label: 'CHF (Fr)' },
{ value: 'SEK', label: 'SEK (kr)' },
{ value: 'NOK', label: 'NOK (kr)' },
{ value: 'DKK', label: 'DKK (kr)' },
{ value: 'KRW', label: 'KRW (₩)' },
{ value: 'BRL', label: 'BRL (R$)' },
{ value: 'MXN', label: 'MXN ($)' },
{ value: 'RUB', label: 'RUB (₽)' },
];
const FormatControlGroup: FC<FormatControlGroupProps> = ({
showNumber = true,
showCurrency = false,
showDate = false,
showPercentage = false,
numberFormatLabel = t('Number format'),
currencyFormatLabel = t('Currency'),
dateFormatLabel = t('Date format'),
percentageFormatLabel = t('Percentage format'),
customFormatOptions = [],
values = {},
onChange = () => {},
}) => {
const formatOptions =
customFormatOptions.length > 0 ? customFormatOptions : D3_FORMAT_OPTIONS;
return (
<div className="format-control-group">
{showNumber && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{numberFormatLabel}</label>
<Select
value={values.number_format || 'SMART_NUMBER'}
onChange={value => onChange('number_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={formatOptions.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 format string for numbers. See ')}
<a
href="https://github.com/d3/d3-format/blob/main/README.md#format"
target="_blank"
rel="noopener noreferrer"
>
{t('D3 format docs')}
</a>
{t(' for details.')}
</small>
</Col>
</Row>
)}
{showCurrency && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{currencyFormatLabel}</label>
<Select
value={values.currency_format || 'USD'}
onChange={value => onChange('currency_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select currency')}
options={CURRENCY_OPTIONS}
/>
<small className="text-muted">
{t('Currency to use for formatting')}
</small>
</Col>
</Row>
)}
{showDate && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{dateFormatLabel}</label>
<Select
value={values.date_format || 'smart_date'}
onChange={value => onChange('date_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 time format string. See ')}
<a
href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format"
target="_blank"
rel="noopener noreferrer"
>
{t('D3 time format docs')}
</a>
{t(' for details.')}
</small>
</Col>
</Row>
)}
{showPercentage && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{percentageFormatLabel}</label>
<Select
value={values.percentage_format || '.0%'}
onChange={value => onChange('percentage_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={[
['.0%', t('0%')],
['.1%', t('0.1%')],
['.2%', t('0.12%')],
['.3%', t('0.123%')],
[',.0%', t('1,234%')],
[',.1%', t('1,234.5%')],
].map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 format for percentages')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default FormatControlGroup;

View File

@@ -1,131 +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, useMemo } from 'react';
import { t } from '@superset-ui/core';
import { Select } from '@superset-ui/core/components';
import { ControlComponentProps, ColumnMeta } from '../../types';
import { ControlHeader } from '../../components/ControlHeader';
export interface GranularityControlValue {
column_name: string;
type?: string;
is_dttm?: boolean;
}
export interface GranularityControlProps
extends ControlComponentProps<GranularityControlValue | string> {
columns?: ColumnMeta[];
datasource?: {
columns?: ColumnMeta[];
verbose_map?: Record<string, string>;
};
clearable?: boolean;
temporalColumnsOnly?: boolean;
}
const GranularityControl: FC<GranularityControlProps> = ({
value,
onChange,
columns = [],
datasource,
clearable = false,
temporalColumnsOnly = true,
name,
label,
description,
validationErrors,
renderTrigger,
...props
}) => {
const allColumns = useMemo(() => {
const cols = columns.length > 0 ? columns : datasource?.columns || [];
if (temporalColumnsOnly) {
return cols.filter(col => col.is_dttm);
}
return cols;
}, [columns, datasource?.columns, temporalColumnsOnly]);
const options = useMemo(
() =>
allColumns.map(col => ({
value: col.column_name,
label:
datasource?.verbose_map?.[col.column_name] ||
col.verbose_name ||
col.column_name,
})),
[allColumns, datasource?.verbose_map],
);
const currentValue = useMemo(() => {
if (typeof value === 'string') {
return value;
}
return value?.column_name;
}, [value]);
const handleChange = (newValue: string | undefined) => {
if (onChange) {
if (!newValue && clearable) {
onChange(null as any);
} else if (newValue) {
const column = allColumns.find(col => col.column_name === newValue);
if (column) {
onChange({
column_name: column.column_name,
type: column.type,
is_dttm: column.is_dttm,
});
}
}
}
};
return (
<div>
<ControlHeader
name={name}
label={label || t('Time Column')}
description={
description ||
t(
'The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' +
'column in the table. Also note that the ' +
'filter below is applied against this column or ' +
'expression',
)
}
validationErrors={validationErrors}
renderTrigger={renderTrigger}
/>
<Select
value={currentValue}
onChange={handleChange}
options={options}
placeholder={t('Select a temporal column')}
allowClear={clearable}
showSearch
css={{ width: '100%' }}
/>
</div>
);
};
export default GranularityControl;

View File

@@ -1,268 +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 type { CustomControlItem } from '../../types';
/**
* Helper function to create a SelectControl configuration
*/
export const SelectControl = (config: {
name: string;
label: string;
default?: any;
choices?: any[][] | (() => any[][]) | any[];
description?: string;
freeForm?: boolean;
clearable?: boolean;
multiple?: boolean;
validators?: any[];
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SelectControl',
...config,
},
});
/**
* Helper function to create a TextControl configuration
*/
export const TextControl = (config: {
name: string;
label: string;
default?: any;
description?: string;
isInt?: boolean;
isFloat?: boolean;
validators?: any[];
renderTrigger?: boolean;
placeholder?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'TextControl',
...config,
},
});
/**
* Helper function to create a CheckboxControl configuration
*/
export const CheckboxControl = (config: {
name: string;
label: string;
default?: boolean;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'CheckboxControl',
...config,
},
});
/**
* Helper function to create a SliderControl configuration
*/
export const SliderControl = (config: {
name: string;
label: string;
default?: number;
min?: number;
max?: number;
step?: number;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SliderControl',
...config,
},
});
/**
* Helper function to create a RadioButtonControl configuration
*/
export const RadioButtonControl = (config: {
name: string;
label: string;
default?: any;
options?: any[][] | any[];
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'RadioButtonControl',
...config,
},
});
/**
* Helper function to create a BoundsControl configuration
*/
export const BoundsControl = (config: {
name: string;
label: string;
default?: [number | null, number | null];
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'BoundsControl',
...config,
},
});
/**
* Helper function to create a ColorPickerControl configuration
*/
export const ColorPickerControl = (config: {
name: string;
label: string;
default?: { r: number; g: number; b: number; a: number };
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'ColorPickerControl',
...config,
},
});
/**
* Helper function to create a DateFilterControl configuration
*/
export const DateFilterControl = (config: {
name: string;
label: string;
default?: string;
description?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'DateFilterControl',
...config,
},
});
/**
* Helper function to create a SwitchControl configuration
*/
export const SwitchControl = (config: {
name: string;
label: string;
default?: boolean;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SwitchControl',
...config,
},
});
/**
* Helper function to create a HiddenControl configuration
*/
export const HiddenControl = (config: {
name: string;
default?: any;
initialValue?: any;
description?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'HiddenControl',
...config,
},
});
/**
* Helper function to create a SpatialControl configuration
*/
export const SpatialControl = (config: {
name: string;
label: string;
description?: string;
validators?: any[];
mapStateToProps?: (state: any) => any;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SpatialControl',
...config,
},
});
/**
* Helper function to create a ContourControl configuration
*/
export const ContourControl = (config: {
name: string;
label: string;
description?: string;
renderTrigger?: boolean;
mapStateToProps?: (state: any) => any;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'ContourControl',
...config,
},
});
/**
* Helper function to create a TextAreaControl configuration
*/
export const TextAreaControl = (config: {
name: string;
label: string;
default?: string;
description?: string;
renderTrigger?: boolean;
rows?: number;
placeholder?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'TextAreaControl',
...config,
},
});

View File

@@ -1,241 +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 { Select, Switch, InputNumber, Input } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface LabelControlGroupProps {
chartType?: 'pie' | 'sunburst' | 'treemap' | 'funnel' | 'gauge';
showLabelType?: boolean;
showTemplate?: boolean;
showThreshold?: boolean;
showOutside?: boolean;
showLabelLine?: boolean;
showRotation?: boolean;
showUpperLabels?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const LABEL_TYPE_OPTIONS = [
['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')],
];
const LABEL_ROTATION_OPTIONS = [
['0', t('Horizontal')],
['45', t('45°')],
['90', t('Vertical')],
['-45', t('-45°')],
];
const LabelControlGroup: FC<LabelControlGroupProps> = ({
chartType = 'pie',
showLabelType = true,
showTemplate = true,
showThreshold = true,
showOutside = false,
showLabelLine = false,
showRotation = false,
showUpperLabels = false,
values = {},
onChange = () => {},
}) => {
const showLabels = values.show_labels ?? true;
const labelType = values.label_type || 'key';
return (
<div className="label-control-group">
{/* Show Labels Toggle */}
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={showLabels}
onChange={checked => onChange('show_labels', checked)}
/>
{t('Show Labels')}
</label>
<small className="text-muted">
{t('Whether to display the labels')}
</small>
</Col>
</Row>
{showLabels && (
<>
{/* Label Type */}
{showLabelType && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Type')}</label>
<Select
value={labelType}
onChange={value => onChange('label_type', value)}
style={{ width: '100%' }}
options={LABEL_TYPE_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('What should be shown on the label?')}
</small>
</Col>
</Row>
)}
{/* Label Template */}
{showTemplate && labelType === 'template' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Template')}</label>
<Input.TextArea
value={values.label_template || ''}
onChange={e => onChange('label_template', e.target.value)}
placeholder="{name}: {value} ({percent}%)"
rows={3}
/>
<small className="text-muted">
{t(
'Format data labels. Use variables: {name}, {value}, {percent}. \\n represents a new line.',
)}
</small>
</Col>
</Row>
)}
{/* Label Threshold */}
{showThreshold && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Threshold')}</label>
<InputNumber
value={values.show_labels_threshold ?? 5}
onChange={value => onChange('show_labels_threshold', value)}
min={0}
max={100}
step={0.5}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t(
'Minimum threshold in percentage points for showing labels',
)}
</small>
</Col>
</Row>
)}
{/* Labels Outside (Pie specific) */}
{showOutside && chartType === 'pie' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.labels_outside || false}
onChange={checked => onChange('labels_outside', checked)}
/>
{t('Put labels outside')}
</label>
<small className="text-muted">
{t('Put the labels outside of the pie?')}
</small>
</Col>
</Row>
)}
{/* Label Line (Pie specific) */}
{showLabelLine && chartType === 'pie' && values.labels_outside && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.label_line || false}
onChange={checked => onChange('label_line', checked)}
/>
{t('Label Line')}
</label>
<small className="text-muted">
{t('Draw a line from the label to the slice')}
</small>
</Col>
</Row>
)}
{/* Label Rotation */}
{showRotation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Rotation')}</label>
<Select
value={values.label_rotation || '0'}
onChange={value => onChange('label_rotation', value)}
style={{ width: '100%' }}
options={LABEL_ROTATION_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Rotation angle of labels')}
</small>
</Col>
</Row>
)}
{/* Show Upper Labels (Treemap specific) */}
{showUpperLabels && chartType === 'treemap' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.show_upper_labels || false}
onChange={checked => onChange('show_upper_labels', checked)}
/>
{t('Show Upper Labels')}
</label>
<small className="text-muted">
{t('Show labels for parent nodes')}
</small>
</Col>
</Row>
)}
</>
)}
</div>
);
};
export default LabelControlGroup;

View File

@@ -1,133 +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 { Switch, Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface MarkerControlGroupProps {
enabledLabel?: string;
sizeLabel?: string;
maxSize?: number;
minSize?: number;
defaultSize?: number;
values?: {
markerEnabled?: boolean;
markerSize?: number;
};
onChange?: (name: string, value: any) => void;
disabled?: boolean;
}
const MarkerControlGroup: FC<MarkerControlGroupProps> = ({
enabledLabel = t('Show markers'),
sizeLabel = t('Marker size'),
maxSize = 20,
minSize = 0,
defaultSize = 6,
values = {},
onChange = () => {},
disabled = false,
}) => {
const markerEnabled = values.markerEnabled ?? false;
const markerSize = values.markerSize ?? defaultSize;
const handleEnabledChange = (checked: boolean) => {
onChange('markerEnabled', checked);
if (checked && !values.markerSize) {
onChange('markerSize', defaultSize);
}
};
const handleSizeChange = (value: number) => {
onChange('markerSize', value);
};
const handleInputChange = (value: number | null) => {
if (value !== null) {
const clampedValue = Math.max(minSize, Math.min(maxSize, value));
onChange('markerSize', clampedValue);
}
};
return (
<div className="marker-control-group">
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={markerEnabled}
onChange={handleEnabledChange}
disabled={disabled}
/>
{enabledLabel}
</label>
<small className="text-muted">
{t('Draw markers on data points for better visibility')}
</small>
</Col>
</Row>
{markerEnabled && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'block', marginBottom: 8 }}>
{sizeLabel}
</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={minSize}
max={maxSize}
step={1}
value={markerSize}
onChange={handleSizeChange}
disabled={disabled || !markerEnabled}
marks={{
[minSize]: minSize.toString(),
[Math.floor(maxSize / 2)]: Math.floor(
maxSize / 2,
).toString(),
[maxSize]: maxSize.toString(),
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={minSize}
max={maxSize}
step={1}
value={markerSize}
onChange={handleInputChange}
disabled={disabled || !markerEnabled}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Size of the markers in pixels')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default MarkerControlGroup;

View File

@@ -1,114 +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 { Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface OpacityControlProps {
name?: string;
label?: string;
description?: string;
min?: number;
max?: number;
step?: number;
value?: number;
onChange?: (value: number) => void;
disabled?: boolean;
marks?: Record<number, string>;
}
const OpacityControl: FC<OpacityControlProps> = ({
name = 'opacity',
label = t('Opacity'),
description = t('Opacity of the elements'),
min = 0,
max = 1,
step = 0.1,
value = 0.8,
onChange = () => {},
disabled = false,
marks,
}) => {
const defaultMarks = marks || {
0: '0%',
0.25: '25%',
0.5: '50%',
0.75: '75%',
1: '100%',
};
const handleSliderChange = (val: number) => {
onChange(val);
};
const handleInputChange = (val: number | null) => {
if (val !== null) {
const clampedValue = Math.max(min, Math.min(max, val));
onChange(clampedValue);
}
};
const percentageValue = Math.round(value * 100);
return (
<div className="opacity-control" data-name={name}>
<label style={{ display: 'block', marginBottom: 8 }}>{label}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={min}
max={max}
step={step}
value={value}
onChange={handleSliderChange}
marks={defaultMarks}
disabled={disabled}
tooltip={{
formatter: val => `${Math.round((val as number) * 100)}%`,
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={min * 100}
max={max * 100}
step={step * 100}
value={percentageValue}
onChange={val => handleInputChange(val !== null ? val / 100 : null)}
formatter={val => `${val}%`}
parser={val => Number((val as string).replace('%', ''))}
disabled={disabled}
style={{ width: '100%' }}
/>
</Col>
</Row>
{description && (
<small
className="text-muted"
style={{ display: 'block', marginTop: 4 }}
>
{description}
</small>
)}
</div>
);
};
export default OpacityControl;

View File

@@ -1,175 +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 { Select, Switch, Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface PieShapeControlProps {
showDonut?: boolean;
showRoseType?: boolean;
showRadius?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const ROSE_TYPE_OPTIONS = [
['area', t('Area')],
['radius', t('Radius')],
[null, t('None')],
];
const PieShapeControl: FC<PieShapeControlProps> = ({
showDonut = true,
showRoseType = true,
showRadius = true,
values = {},
onChange = () => {},
}) => {
const isDonut = values.donut || false;
const innerRadius = values.innerRadius || 30;
const outerRadius = values.outerRadius || 70;
return (
<div className="pie-shape-control">
{/* Donut Toggle */}
{showDonut && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={isDonut}
onChange={checked => onChange('donut', checked)}
/>
{t('Donut')}
</label>
<small className="text-muted">
{t('Do you want a donut or a pie?')}
</small>
</Col>
</Row>
)}
{/* Inner Radius (for Donut) */}
{showRadius && isDonut && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Inner Radius')}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={0}
max={100}
step={1}
value={innerRadius}
onChange={value => onChange('innerRadius', value)}
marks={{
0: '0%',
50: '50%',
100: '100%',
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={0}
max={100}
step={1}
value={innerRadius}
onChange={value => onChange('innerRadius', value)}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Inner radius of donut hole')}
</small>
</Col>
</Row>
)}
{/* Outer Radius */}
{showRadius && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Outer Radius')}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={0}
max={100}
step={1}
value={outerRadius}
onChange={value => onChange('outerRadius', value)}
marks={{
0: '0%',
50: '50%',
100: '100%',
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={0}
max={100}
step={1}
value={outerRadius}
onChange={value => onChange('outerRadius', value)}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Outer edge of the pie/donut')}
</small>
</Col>
</Row>
)}
{/* Rose Type (Nightingale Chart) */}
{showRoseType && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Rose Type')}</label>
<Select
value={values.roseType || null}
onChange={value => onChange('roseType', value)}
style={{ width: '100%' }}
allowClear
placeholder={t('None')}
options={ROSE_TYPE_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Whether to show as Nightingale chart (polar area chart)')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default PieShapeControl;

View File

@@ -1,104 +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, ReactElement, useMemo } from 'react';
import { JsonValue } from '@superset-ui/core';
export interface ReactControlPanelSection {
key: string;
label: string;
description?: string;
expanded?: boolean;
render: (props: ControlPanelRenderProps) => ReactElement;
}
export interface ControlPanelRenderProps {
values: Record<string, JsonValue>;
onChange: (name: string, value: JsonValue) => void;
datasource?: any;
formData?: any;
validationErrors?: Record<string, string[]>;
}
export interface ReactControlPanelConfig {
sections: ReactControlPanelSection[];
onFormDataChange?: (formData: any) => any;
}
/**
* A wrapper component that allows rendering control panels with actual React components
* instead of configuration objects. This provides a stepping stone toward JSON-driven forms.
*/
export const ReactControlPanel: FC<{
config: ReactControlPanelConfig;
values: Record<string, JsonValue>;
onChange: (name: string, value: JsonValue) => void;
datasource?: any;
formData?: any;
validationErrors?: Record<string, string[]>;
}> = ({ config, values, onChange, datasource, formData, validationErrors }) => {
const renderProps: ControlPanelRenderProps = useMemo(
() => ({
values,
onChange,
datasource,
formData,
validationErrors,
}),
[values, onChange, datasource, formData, validationErrors],
);
return (
<div className="react-control-panel">
{config.sections.map(section => (
<div key={section.key} className="control-panel-section">
<h4>{section.label}</h4>
{section.description && (
<p className="section-description">{section.description}</p>
)}
{section.render(renderProps)}
</div>
))}
</div>
);
};
/**
* Helper function to create a control panel configuration that works with
* both the old and new systems
*/
export function createReactControlPanel(config: ReactControlPanelConfig): any {
return {
controlPanelSections: config.sections.map(section => ({
label: section.label,
description: section.description,
expanded: section.expanded ?? true,
controlSetRows: [
[
// Return a React element that will be rendered directly
<ReactControlPanel
key={section.key}
config={{ sections: [section] }}
values={{}}
onChange={() => {}}
/>,
],
],
})),
};
}

View File

@@ -1,306 +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 type { CustomControlItem } from '../../types';
import sharedControls from '../sharedControls';
/**
* React component wrappers for shared controls.
* These replace string references like ['metrics'] with actual component calls.
*/
// Metrics controls
export const MetricsControl = (): CustomControlItem => ({
name: 'metrics',
config: sharedControls.metrics,
});
export const MetricControl = (): CustomControlItem => ({
name: 'metric',
config: sharedControls.metric,
});
export const SecondaryMetricControl = (): CustomControlItem => ({
name: 'secondary_metric',
config: sharedControls.secondary_metric,
});
export const Metric2Control = (): CustomControlItem => ({
name: 'metric_2',
config: sharedControls.metric_2,
});
export const TimeLimitMetricControl = (): CustomControlItem => ({
name: 'timeseries_limit_metric',
config: sharedControls.timeseries_limit_metric,
});
export const OrderByControl = (): CustomControlItem => ({
name: 'orderby',
config: sharedControls.orderby,
});
export const SeriesLimitMetricControl = (): CustomControlItem => ({
name: 'series_limit_metric',
config: sharedControls.series_limit_metric,
});
export const SortByMetricControl = (): CustomControlItem => ({
name: 'sort_by_metric',
config: sharedControls.sort_by_metric,
});
// Dimension controls
export const GroupByControl = (): CustomControlItem => ({
name: 'groupby',
config: sharedControls.groupby,
});
export const ColumnsControl = (): CustomControlItem => ({
name: 'columns',
config: sharedControls.columns,
});
// Note: These controls are not in sharedControls, using columns as fallback
export const AllColumnsControl = (): CustomControlItem => ({
name: 'all_columns',
config: sharedControls.columns || {},
});
export const AllColumnsXControl = (): CustomControlItem => ({
name: 'all_columns_x',
config: sharedControls.columns || {},
});
export const AllColumnsYControl = (): CustomControlItem => ({
name: 'all_columns_y',
config: sharedControls.columns || {},
});
export const SeriesControl = (): CustomControlItem => ({
name: 'series',
config: sharedControls.series,
});
export const EntityControl = (): CustomControlItem => ({
name: 'entity',
config: sharedControls.entity,
});
export const XControl = (): CustomControlItem => ({
name: 'x',
config: sharedControls.x,
});
export const YControl = (): CustomControlItem => ({
name: 'y',
config: sharedControls.y,
});
// Note: sort_by is not in sharedControls, using a default config
export const SortByControl = (): CustomControlItem => ({
name: 'sort_by',
config: {
type: 'SelectControl',
label: 'Sort By',
description: 'Sort by column',
},
});
export const SizeControl = (): CustomControlItem => ({
name: 'size',
config: sharedControls.size,
});
export const XAxisControl = (): CustomControlItem => ({
name: 'x_axis',
config: sharedControls.x_axis,
});
// Filter controls
export const AdhocFiltersControl = (): CustomControlItem => ({
name: 'adhoc_filters',
config: sharedControls.adhoc_filters,
});
export const TimeRangeControl = (): CustomControlItem => ({
name: 'time_range',
config: sharedControls.time_range,
});
export const TimeGrainSqlaControl = (): CustomControlItem => ({
name: 'time_grain_sqla',
config: sharedControls.time_grain_sqla,
});
export const GranularityControl = (): CustomControlItem => ({
name: 'granularity',
config: sharedControls.granularity,
});
export const GranularitySqlaControl = (): CustomControlItem => ({
name: 'granularity_sqla',
config: sharedControls.granularity_sqla,
});
// Limit controls
export const RowLimitControl = (): CustomControlItem => ({
name: 'row_limit',
config: sharedControls.row_limit,
});
export const LimitControl = (): CustomControlItem => ({
name: 'limit',
config: sharedControls.limit,
});
export const GroupOthersWhenLimitReachedControl = (): CustomControlItem => ({
name: 'group_others_when_limit_reached',
config: sharedControls.group_others_when_limit_reached,
});
export const SeriesLimitControl = (): CustomControlItem => ({
name: 'series_limit',
config: sharedControls.series_limit,
});
// Sort controls
export const OrderDescControl = (): CustomControlItem => ({
name: 'order_desc',
config: sharedControls.order_desc,
});
export const OrderByColsControl = (): CustomControlItem => ({
name: 'order_by_cols',
config: sharedControls.order_by_cols,
});
// Color controls
export const ColorSchemeControl = (): CustomControlItem => ({
name: 'color_scheme',
config: sharedControls.color_scheme,
});
export const LinearColorSchemeControl = (): CustomControlItem => ({
name: 'linear_color_scheme',
config: sharedControls.linear_color_scheme,
});
export const ColorPickerControl = (): CustomControlItem => ({
name: 'color_picker',
config: sharedControls.color_picker,
});
export const TimeShiftColorControl = (): CustomControlItem => ({
name: 'time_shift_color',
config: sharedControls.time_shift_color,
});
export const CurrencyFormatControl = (): CustomControlItem => ({
name: 'currency_format',
config: sharedControls.currency_format,
});
export const TruncateMetricControl = (): CustomControlItem => ({
name: 'truncate_metric',
config: sharedControls.truncate_metric,
});
export const ShowEmptyColumnsControl = (): CustomControlItem => ({
name: 'show_empty_columns',
config: sharedControls.show_empty_columns,
});
// Other controls
export const ZoomableControl = (): CustomControlItem => ({
name: 'zoomable',
config: sharedControls.zoomable,
});
export const DatasourceControl = (): CustomControlItem => ({
name: 'datasource',
config: sharedControls.datasource,
});
export const VizTypeControl = (): CustomControlItem => ({
name: 'viz_type',
config: sharedControls.viz_type,
});
// Tooltip controls
export const TooltipColumnsControl = (): CustomControlItem => ({
name: 'tooltip_columns',
config: sharedControls.tooltip_columns,
});
export const TooltipMetricsControl = (): CustomControlItem => ({
name: 'tooltip_metrics',
config: sharedControls.tooltip_metrics,
});
// Format controls
export const YAxisFormatControl = (): CustomControlItem => ({
name: 'y_axis_format',
config: sharedControls.y_axis_format,
});
export const XAxisTimeFormatControl = (): CustomControlItem => ({
name: 'x_axis_time_format',
config: sharedControls.x_axis_time_format,
});
// Hidden controls
export const TemporalColumnsLookupControl = (): CustomControlItem => ({
name: 'temporal_columns_lookup',
config: sharedControls.temporal_columns_lookup,
});
// Inline controls - for creating custom controls with overrides
export const InlineTextControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'TextControl',
...overrides,
},
});
export const InlineSelectControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'SelectControl',
...overrides,
},
});
export const InlineCheckboxControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'CheckboxControl',
...overrides,
},
});

View File

@@ -1,272 +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 { Switch, Select, Input } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
import FormatControlGroup from './FormatControlGroup';
export interface TableControlsSectionProps {
variant?: 'table' | 'pivot' | 'ag-grid';
showPagination?: boolean;
showCellBars?: boolean;
showTotals?: boolean;
showConditionalFormatting?: boolean;
showTimestampFormat?: boolean;
showAllowHtml?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const TableControlsSection: FC<TableControlsSectionProps> = ({
variant = 'table',
showPagination = false,
showCellBars = false,
showTotals = false,
showConditionalFormatting = true,
showTimestampFormat = false,
showAllowHtml = true,
values = {},
onChange = () => {},
}) => {
const isPivot = variant === 'pivot';
return (
<div className="table-controls-section">
{/* Pagination Controls */}
{showPagination && !isPivot && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.server_pagination || false}
onChange={checked => onChange('server_pagination', checked)}
/>
{t('Server Pagination')}
</label>
<small className="text-muted">
{t('Enable server-side pagination for large datasets')}
</small>
</Col>
</Row>
{values.server_pagination && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Page Length')}</label>
<Select
value={values.server_page_length || 10}
onChange={value => onChange('server_page_length', value)}
style={{ width: '100%' }}
options={[
{ value: 10, label: '10' },
{ value: 25, label: '25' },
{ value: 50, label: '50' },
{ value: 100, label: '100' },
{ value: 200, label: '200' },
]}
/>
<small className="text-muted">
{t('Number of rows per page')}
</small>
</Col>
</Row>
)}
</>
)}
{/* Cell Bars */}
{showCellBars && !isPivot && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_cell_bars || false}
onChange={checked => onChange('show_cell_bars', checked)}
/>
{t('Show Cell Bars')}
</label>
<small className="text-muted">
{t('Display mini bar charts in numeric columns')}
</small>
</Col>
</Row>
)}
{/* Totals */}
{showTotals && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_totals || values.rowTotals || false}
onChange={checked => {
if (isPivot) {
onChange('rowTotals', checked);
onChange('colTotals', checked);
} else {
onChange('show_totals', checked);
}
}}
/>
{t('Show Totals')}
</label>
<small className="text-muted">
{isPivot
? t('Show row and column totals')
: t('Show total row at bottom')}
</small>
</Col>
</Row>
)}
{/* Subtotals for Pivot */}
{isPivot && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.rowSubTotals || false}
onChange={checked => onChange('rowSubTotals', checked)}
/>
{t('Show Row Subtotals')}
</label>
<small className="text-muted">
{t('Show subtotals for row groups')}
</small>
</Col>
</Row>
{values.rowSubTotals && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Subtotal Position')}</label>
<Select
value={values.rowSubtotalPosition || 'bottom'}
onChange={value => onChange('rowSubtotalPosition', value)}
style={{ width: '100%' }}
options={[
{ value: 'top', label: t('Top') },
{ value: 'bottom', label: t('Bottom') },
]}
/>
</Col>
</Row>
)}
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.colSubTotals || false}
onChange={checked => onChange('colSubTotals', checked)}
/>
{t('Show Column Subtotals')}
</label>
<small className="text-muted">
{t('Show subtotals for column groups')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.transposePivot || false}
onChange={checked => onChange('transposePivot', checked)}
/>
{t('Transpose Pivot')}
</label>
<small className="text-muted">{t('Swap rows and columns')}</small>
</Col>
</Row>
</>
)}
{/* Conditional Formatting */}
{showConditionalFormatting && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Conditional Formatting')}</label>
<Input.TextArea
value={values.conditional_formatting || ''}
onChange={e => onChange('conditional_formatting', e.target.value)}
placeholder={t('Enter conditional formatting rules as JSON')}
rows={4}
/>
<small className="text-muted">
{t('Apply conditional color formatting to cells')}
</small>
</Col>
</Row>
)}
{/* Timestamp Format */}
{showTimestampFormat && !isPivot && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Timestamp Format')}</label>
<Input
value={values.table_timestamp_format || ''}
onChange={e => onChange('table_timestamp_format', e.target.value)}
placeholder="%Y-%m-%d %H:%M:%S"
/>
<small className="text-muted">
{t('D3 time format for timestamp columns')}
</small>
</Col>
</Row>
)}
{/* Allow HTML */}
{showAllowHtml && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.allow_render_html || false}
onChange={checked => onChange('allow_render_html', checked)}
/>
{t('Allow HTML')}
</label>
<small className="text-muted">
{t(
'Render HTML content in cells (security warning: only enable for trusted data)',
)}
</small>
</Col>
</Row>
)}
{/* Format Controls */}
<div style={{ marginTop: 24 }}>
<h4>{t('Value Formats')}</h4>
<FormatControlGroup
showNumber
showCurrency
showPercentage={isPivot}
showDate={!isPivot}
values={values}
onChange={onChange}
/>
</div>
</div>
);
};
export default TableControlsSection;

View File

@@ -1,237 +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 { Select, Radio } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
import AxisControlSection from './AxisControlSection';
import FormatControlGroup from './FormatControlGroup';
import OpacityControl from './OpacityControl';
import MarkerControlGroup from './MarkerControlGroup';
export interface TimeseriesControlPanelProps {
variant: 'area' | 'bar' | 'line' | 'scatter' | 'smooth' | 'step';
showSeriesType?: boolean;
showStack?: boolean;
showArea?: boolean;
showMarkers?: boolean;
showOpacity?: boolean;
showOrientation?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const SERIES_TYPE_OPTIONS: Record<string, Array<[string, string]>> = {
line: [
['line', t('Line')],
['scatter', t('Scatter')],
['smooth', t('Smooth')],
],
area: [
['line', t('Line')],
['smooth', t('Smooth Line')],
['start', t('Step - start')],
['middle', t('Step - middle')],
['end', t('Step - end')],
],
step: [
['start', t('Step - start')],
['middle', t('Step - middle')],
['end', t('Step - end')],
],
bar: [],
scatter: [],
smooth: [],
};
const STACK_OPTIONS = [
['stack', t('Stack')],
['stream', t('Stream')],
['expand', t('Expand')],
];
const TimeseriesControlPanel: FC<TimeseriesControlPanelProps> = ({
variant,
showSeriesType = true,
showStack = false,
showArea = false,
showMarkers = true,
showOpacity = false,
showOrientation = false,
values = {},
onChange = () => {},
}) => {
const hasAreaOptions = variant === 'area' || showArea;
const hasBarOptions = variant === 'bar';
const hasLineOptions = variant === 'line' || variant === 'smooth';
return (
<div className="timeseries-control-panel">
{/* Series Type Selection */}
{showSeriesType && SERIES_TYPE_OPTIONS[variant] && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Series Style')}</label>
<Select
value={
values.seriesType ||
(SERIES_TYPE_OPTIONS[variant][0]
? SERIES_TYPE_OPTIONS[variant][0][0]
: 'line')
}
onChange={value => onChange('seriesType', value)}
style={{ width: '100%' }}
options={SERIES_TYPE_OPTIONS[variant].map(
([value, label]: [string, string]) => ({
value,
label,
}),
)}
/>
<small className="text-muted">
{t('Series chart type (line, smooth, step, etc)')}
</small>
</Col>
</Row>
)}
{/* Stack Options */}
{showStack && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Stacking')}</label>
<Select
value={values.stack || null}
onChange={value => onChange('stack', value)}
style={{ width: '100%' }}
allowClear
placeholder={t('No stacking')}
options={STACK_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Stack series on top of each other')}
</small>
</Col>
</Row>
)}
{/* Bar Orientation */}
{showOrientation && hasBarOptions && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Bar Orientation')}</label>
<Radio.Group
value={values.orientation || 'vertical'}
onChange={e => onChange('orientation', e.target.value)}
>
<Radio value="vertical">{t('Vertical')}</Radio>
<Radio value="horizontal">{t('Horizontal')}</Radio>
</Radio.Group>
<small
className="text-muted"
style={{ display: 'block', marginTop: 8 }}
>
{t('Orientation of bar chart')}
</small>
</Col>
</Row>
)}
{/* Area Chart Options */}
{hasAreaOptions && (
<div style={{ marginBottom: 24 }}>
<h4>{t('Area Chart')}</h4>
<OpacityControl
name="opacity"
label={t('Area opacity')}
description={t(
'Opacity of area under the line. Set to 0 to disable area.',
)}
value={values.opacity || (variant === 'area' ? 0.2 : 0)}
onChange={value => onChange('opacity', value)}
/>
</div>
)}
{/* Line/Marker Options */}
{showMarkers && (hasLineOptions || variant === 'area') && (
<div style={{ marginBottom: 24 }}>
<h4>{t('Markers')}</h4>
<MarkerControlGroup
values={{
markerEnabled: values.markerEnabled || false,
markerSize: values.markerSize || 6,
}}
onChange={onChange}
/>
</div>
)}
{/* X Axis Controls */}
<div style={{ marginBottom: 24 }}>
<h4>{t('X Axis')}</h4>
<AxisControlSection
axis="x"
showTitle
showFormat
showRotation
showBounds={hasBarOptions}
showTruncate
timeFormat
values={values}
onChange={onChange}
/>
</div>
{/* Y Axis Controls */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Y Axis')}</h4>
<AxisControlSection
axis="y"
showTitle
showFormat
showBounds
showLogarithmic
showMinorTicks
showTruncate
values={values}
onChange={onChange}
/>
</div>
{/* Value Formats */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Value Formats')}</h4>
<FormatControlGroup
showNumber
showCurrency={hasBarOptions}
showDate
showPercentage={showStack}
values={values}
onChange={onChange}
/>
</div>
</div>
);
};
export default TimeseriesControlPanel;

View File

@@ -16,42 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import RadioButtonControl from './RadioButtonControl';
export * from './RadioButtonControl';
export { default as RadioButtonControl } from './RadioButtonControl';
export { default as GranularityControl } from './GranularityControl';
export * from './ReactControlPanel';
export { default as AxisControlSection } from './AxisControlSection';
export { default as FormatControlGroup } from './FormatControlGroup';
export { default as OpacityControl } from './OpacityControl';
export { default as MarkerControlGroup } from './MarkerControlGroup';
export { default as TimeseriesControlPanel } from './TimeseriesControlPanel';
export { default as LabelControlGroup } from './LabelControlGroup';
export { default as PieShapeControl } from './PieShapeControl';
export { default as TableControlsSection } from './TableControlsSection';
export { default as FilterControlsSection } from './FilterControlsSection';
export { default as DeckGLControlsSection } from './DeckGLControlsSection';
// Export ControlComponents with specific names
// ColorPickerControl from ControlComponents takes props, renamed to avoid conflict
export {
CheckboxControl,
NumberControl,
SelectControl,
SliderControl,
SwitchControl,
TextAreaControl,
TextControl,
ColorPickerControl as ColorPickerControlWithProps,
type ControlComponentConfig,
} from './ControlComponents';
// Export all SharedControlComponents which replace string references
// ColorPickerControl from here does NOT take props - it's for the shared 'color_picker' control
export * from './SharedControlComponents';
// Export React control panel
export { ReactControlPanel } from './ReactControlPanel';
// Export control panel layout components
export * from './ControlPanelLayout';
// Inline control functions are exported from SharedControlComponents
/**
* Shared chart controls. Can be referred via string shortcuts in chart control
* configs.
*/
export default {
RadioButtonControl,
};

View File

@@ -41,53 +41,6 @@ import {
import { checkColumnType } from '../utils/checkColumnType';
import { isSortable } from '../utils/isSortable';
// Aggregation choices with computation methods for plugins and controls
export const aggregationChoices = {
raw: {
label: 'Overall value',
compute: (data: number[]) => {
if (!data.length) return null;
return data[0];
},
},
LAST_VALUE: {
label: 'Last Value',
compute: (data: number[]) => {
if (!data.length) return null;
return data[0];
},
},
sum: {
label: 'Total (Sum)',
compute: (data: number[]) =>
data.length ? data.reduce((a, b) => a + b, 0) : null,
},
mean: {
label: 'Average (Mean)',
compute: (data: number[]) =>
data.length ? data.reduce((a, b) => a + b, 0) / data.length : null,
},
min: {
label: 'Minimum',
compute: (data: number[]) => (data.length ? Math.min(...data) : null),
},
max: {
label: 'Maximum',
compute: (data: number[]) => (data.length ? Math.max(...data) : null),
},
median: {
label: 'Median',
compute: (data: number[]) => {
if (!data.length) return null;
const sorted = [...data].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
},
},
} as const;
export const contributionModeControl = {
name: 'contributionMode',
config: {
@@ -116,12 +69,17 @@ export const aggregationControl = {
default: 'LAST_VALUE',
clearable: false,
renderTrigger: false,
choices: Object.entries(aggregationChoices).map(([value, { label }]) => [
value,
t(label),
]),
choices: [
['raw', t('None')],
['LAST_VALUE', t('Last Value')],
['sum', t('Total (Sum)')],
['mean', t('Average (Mean)')],
['min', t('Minimum')],
['max', t('Maximum')],
['median', t('Median')],
],
description: t(
'Method to compute the displayed value. "Overall value" calculates a single metric across the entire filtered time period, ideal for non-additive metrics like ratios, averages, or distinct counts. Other methods operate over the time series data points.',
'Aggregation method used to compute the Big Number from the Trendline.For non-additive metrics like ratios, averages, distinct counts, etc use NONE.',
),
provideFormDataToProps: true,
mapStateToProps: ({ form_data }: ControlPanelState) => ({

View File

@@ -17,8 +17,8 @@
* under the License.
*/
export { default as sharedControls } from './sharedControls';
// sharedControlComponents is deprecated - import components directly instead
// export { default as sharedControlComponents } from './components';
// React control components
export { default as sharedControlComponents } from './components';
export { aggregationControl } from './customControls';
export * from './components';
export * from './customControls';

View File

@@ -177,7 +177,6 @@ const granularity: SharedControlConfig<'SelectControl'> = {
'can type and use simple natural language as in `10 seconds`, ' +
'`1 day` or `56 weeks`',
),
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
};
const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
@@ -205,7 +204,6 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
choices: (datasource as Dataset)?.time_grain_sqla || [],
}),
visibility: displayTimeRelatedControls,
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
};
const time_range: SharedControlConfig<'DateFilterControl'> = {
@@ -285,19 +283,6 @@ const series_limit: SharedControlConfig<'SelectControl'> = {
),
};
const group_others_when_limit_reached: SharedControlConfig<'CheckboxControl'> =
{
type: 'CheckboxControl',
label: t('Group remaining as "Others"'),
default: false,
description: t(
'Groups remaining series into an "Others" category when series limit is reached. ' +
'This prevents incomplete time series data from being displayed.',
),
visibility: ({ form_data }: { form_data: any }) =>
Boolean(form_data?.limit || form_data?.series_limit),
};
const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> =
{
type: 'SelectControl',
@@ -461,7 +446,6 @@ export default {
time_shift_color,
series_columns: dndColumnsControl,
series_limit,
group_others_when_limit_reached,
series_limit_metric: dndSortByControl,
legacy_order_by: dndSortByControl,
truncate_metric,

View File

@@ -32,7 +32,7 @@ import type {
QueryFormMetric,
QueryResponse,
} from '@superset-ui/core';
import { sharedControls } from './shared-controls';
import { sharedControls, sharedControlComponents } from './shared-controls';
export type { Metric } from '@superset-ui/core';
export type { ControlComponentProps } from './shared-controls/components/types';
@@ -45,6 +45,8 @@ interface Action {
interface AnyAction extends Action, AnyDict {}
export type SharedControls = typeof sharedControls;
export type SharedControlAlias = keyof typeof sharedControls;
export type SharedControlComponents = typeof sharedControlComponents;
/** ----------------------------------------------
* Input data/props while rendering
@@ -183,7 +185,7 @@ export type InternalControlType =
| 'Select'
| 'Slider'
| 'Input'
| 'RadioButtonControl';
| keyof SharedControlComponents; // expanded in `expandControlConfig`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ControlType = InternalControlType | ComponentType<any>;
@@ -357,7 +359,7 @@ export type SharedSectionAlias =
| 'NVD3TimeSeries';
export interface OverrideSharedControlItem<
A extends keyof SharedControls = keyof SharedControls,
A extends SharedControlAlias = SharedControlAlias,
> {
name: A;
override: Partial<SharedControls[A]>;
@@ -380,16 +382,16 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
// interfere with other ControlSetItem types
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
// All controls must be React components or control configuration objects
export type ControlSetItem = OverrideSharedControlItem | ExpandedControlItem;
export type ControlSetItem =
| SharedControlAlias
| OverrideSharedControlItem
| ExpandedControlItem;
export type ControlSetRow = ControlSetItem[];
// Ref:
// - superset-frontend/src/explore/components/ControlPanelsContainer.jsx
// - superset-frontend/src/explore/components/ControlPanelSection.jsx
// DEPRECATED: Legacy control panel types - use JsonFormsControlPanelConfig instead
// These are kept temporarily for backward compatibility during migration
export interface ControlPanelSectionConfig {
label?: ReactNode;
description?: ReactNode;
@@ -426,7 +428,6 @@ export const isStandardizedFormData = (
Array.isArray(formData.standardizedFormData.controls.metrics) &&
Array.isArray(formData.standardizedFormData.controls.columns);
// DEPRECATED: Use JsonFormsControlPanelConfig from './types/jsonForms' instead
export interface ControlPanelConfig {
controlPanelSections: (ControlPanelSectionConfig | null)[];
controlOverrides?: ControlOverrides;
@@ -436,7 +437,7 @@ export interface ControlPanelConfig {
}
export type ControlOverrides = {
[P in keyof SharedControls]?: Partial<SharedControls[P]>;
[P in SharedControlAlias]?: Partial<SharedControls[P]>;
};
export type SectionOverrides = {

View File

@@ -1,20 +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.
*/
// Type exports placeholder

View File

@@ -17,15 +17,28 @@
* under the License.
*/
import { isValidElement, ReactElement } from 'react';
import { sharedControls } from '../shared-controls';
import { sharedControls, sharedControlComponents } from '../shared-controls';
import {
ControlType,
ControlSetItem,
ExpandedControlItem,
ControlOverrides,
} from '../types';
export function expandControlType(controlType: ControlType) {
if (
typeof controlType === 'string' &&
controlType in sharedControlComponents
) {
return sharedControlComponents[
controlType as keyof typeof sharedControlComponents
];
}
return controlType;
}
/**
* Expand a control config item to full config in the format of
* Expand a shorthand control config item to full config in the format of
* {
* name: ...,
* config: {
@@ -33,31 +46,26 @@ import {
* ...
* }
* }
*
* Note: String references to shared controls are no longer supported.
* All controls must be React components or control configuration objects.
*/
export function expandControlConfig(
control: ControlSetItem,
controlOverrides: ControlOverrides = {},
): ExpandedControlItem {
// one of the named shared controls
if (typeof control === 'string' && control in sharedControls) {
const name = control;
return {
name,
config: {
...sharedControls[name],
...controlOverrides[name],
},
};
}
// JSX/React element or NULL
if (!control || isValidElement(control)) {
if (!control || typeof control === 'string' || isValidElement(control)) {
return control as ReactElement;
}
// Check if it's a modern panel component (function with isModernPanel flag)
if (typeof control === 'function' && (control as any).isModernPanel) {
console.log('expandControlConfig - Found modern panel, returning as-is');
return control as any;
}
// String controls are no longer supported - they must be migrated to React components
if (typeof control === 'string') {
throw new Error(
`String control reference "${control}" is not supported. ` +
`Use the corresponding React component from @superset-ui/chart-controls instead. ` +
`For example, replace ['metrics'] with [MetricsControl()].`,
);
}
// already fully expanded control config, e.g.
// {
// name: 'metric',
@@ -66,7 +74,13 @@ export function expandControlConfig(
// }
// }
if ('name' in control && 'config' in control) {
return control;
return {
...control,
config: {
...control.config,
type: expandControlType(control.config.type as ControlType),
},
};
}
// apply overrides with shared controls
if ('override' in control && control.name in sharedControls) {

View File

@@ -1,330 +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 {
MetricsControl,
MetricControl,
GroupByControl,
ColumnsControl,
AdhocFiltersControl,
LimitControl,
RowLimitControl,
OrderByControl,
OrderDescControl,
SeriesControl,
EntityControl,
XControl,
YControl,
SizeControl,
ColorSchemeControl,
LinearColorSchemeControl,
ColorPickerControl,
TimeRangeControl,
GranularitySqlaControl,
TimeGrainSqlaControl,
DatasourceControl,
VizTypeControl,
XAxisControl,
YAxisFormatControl,
XAxisTimeFormatControl,
ZoomableControl,
SortByMetricControl,
CurrencyFormatControl,
TooltipColumnsControl,
TooltipMetricsControl,
sharedControls,
} from '../../src';
describe('SharedControlComponents', () => {
describe('React Component Controls', () => {
it('should return proper control items for metrics controls', () => {
const metricsControl = MetricsControl();
expect(metricsControl).toEqual({
name: 'metrics',
config: sharedControls.metrics,
});
const metricControl = MetricControl();
expect(metricControl).toEqual({
name: 'metric',
config: sharedControls.metric,
});
});
it('should return proper control items for dimension controls', () => {
const groupByControl = GroupByControl();
expect(groupByControl).toEqual({
name: 'groupby',
config: sharedControls.groupby,
});
const columnsControl = ColumnsControl();
expect(columnsControl).toEqual({
name: 'columns',
config: sharedControls.columns,
});
const seriesControl = SeriesControl();
expect(seriesControl).toEqual({
name: 'series',
config: sharedControls.series,
});
const entityControl = EntityControl();
expect(entityControl).toEqual({
name: 'entity',
config: sharedControls.entity,
});
});
it('should return proper control items for filter controls', () => {
const adhocFiltersControl = AdhocFiltersControl();
expect(adhocFiltersControl).toEqual({
name: 'adhoc_filters',
config: sharedControls.adhoc_filters,
});
});
it('should return proper control items for limit controls', () => {
const limitControl = LimitControl();
expect(limitControl).toEqual({
name: 'limit',
config: sharedControls.limit,
});
const rowLimitControl = RowLimitControl();
expect(rowLimitControl).toEqual({
name: 'row_limit',
config: sharedControls.row_limit,
});
});
it('should return proper control items for sort controls', () => {
const orderByControl = OrderByControl();
expect(orderByControl).toEqual({
name: 'orderby',
config: sharedControls.orderby,
});
const orderDescControl = OrderDescControl();
expect(orderDescControl).toEqual({
name: 'order_desc',
config: sharedControls.order_desc,
});
const sortByMetricControl = SortByMetricControl();
expect(sortByMetricControl).toEqual({
name: 'sort_by_metric',
config: sharedControls.sort_by_metric,
});
});
it('should return proper control items for axis controls', () => {
const xControl = XControl();
expect(xControl).toEqual({
name: 'x',
config: sharedControls.x,
});
const yControl = YControl();
expect(yControl).toEqual({
name: 'y',
config: sharedControls.y,
});
const xAxisControl = XAxisControl();
expect(xAxisControl).toEqual({
name: 'x_axis',
config: sharedControls.x_axis,
});
// Note: YAxisControl doesn't exist, YControl is reused for y axis
const yControl2 = YControl();
expect(yControl2).toEqual({
name: 'y',
config: sharedControls.y,
});
});
it('should return proper control items for formatting controls', () => {
const yAxisFormatControl = YAxisFormatControl();
expect(yAxisFormatControl).toEqual({
name: 'y_axis_format',
config: sharedControls.y_axis_format,
});
const xAxisTimeFormatControl = XAxisTimeFormatControl();
expect(xAxisTimeFormatControl).toEqual({
name: 'x_axis_time_format',
config: sharedControls.x_axis_time_format,
});
const currencyFormatControl = CurrencyFormatControl();
expect(currencyFormatControl).toEqual({
name: 'currency_format',
config: sharedControls.currency_format,
});
});
it('should return proper control items for color controls', () => {
const colorSchemeControl = ColorSchemeControl();
expect(colorSchemeControl).toEqual({
name: 'color_scheme',
config: sharedControls.color_scheme,
});
const linearColorSchemeControl = LinearColorSchemeControl();
expect(linearColorSchemeControl).toEqual({
name: 'linear_color_scheme',
config: sharedControls.linear_color_scheme,
});
const colorPickerControl = ColorPickerControl();
expect(colorPickerControl).toEqual({
name: 'color_picker',
config: sharedControls.color_picker,
});
});
it('should return proper control items for time controls', () => {
const timeRangeControl = TimeRangeControl();
expect(timeRangeControl).toEqual({
name: 'time_range',
config: sharedControls.time_range,
});
const granularitySqlaControl = GranularitySqlaControl();
expect(granularitySqlaControl).toEqual({
name: 'granularity_sqla',
config: sharedControls.granularity_sqla,
});
const timeGrainSqlaControl = TimeGrainSqlaControl();
expect(timeGrainSqlaControl).toEqual({
name: 'time_grain_sqla',
config: sharedControls.time_grain_sqla,
});
});
it('should return proper control items for datasource controls', () => {
const datasourceControl = DatasourceControl();
expect(datasourceControl).toEqual({
name: 'datasource',
config: sharedControls.datasource,
});
const vizTypeControl = VizTypeControl();
expect(vizTypeControl).toEqual({
name: 'viz_type',
config: sharedControls.viz_type,
});
});
it('should return proper control items for tooltip controls', () => {
const tooltipColumnsControl = TooltipColumnsControl();
expect(tooltipColumnsControl).toEqual({
name: 'tooltip_columns',
config: sharedControls.tooltip_columns,
});
const tooltipMetricsControl = TooltipMetricsControl();
expect(tooltipMetricsControl).toEqual({
name: 'tooltip_metrics',
config: sharedControls.tooltip_metrics,
});
});
it('should return proper control items for other controls', () => {
const sizeControl = SizeControl();
expect(sizeControl).toEqual({
name: 'size',
config: sharedControls.size,
});
const zoomableControl = ZoomableControl();
expect(zoomableControl).toEqual({
name: 'zoomable',
config: sharedControls.zoomable,
});
});
});
describe('Control compatibility', () => {
it('should be usable in control panel configurations', () => {
// Simulate a control panel configuration
const controlPanel = {
controlPanelSections: [
{
label: 'Query',
expanded: true,
controlSetRows: [
[MetricsControl()],
[GroupByControl()],
[AdhocFiltersControl()],
[LimitControl(), OrderDescControl()],
[RowLimitControl()],
],
},
{
label: 'Chart Options',
expanded: true,
controlSetRows: [[ColorSchemeControl()], [YAxisFormatControl()]],
},
],
};
// Verify structure
expect(controlPanel.controlPanelSections).toHaveLength(2);
// Verify first section
const querySection = controlPanel.controlPanelSections[0];
expect(querySection.controlSetRows[0][0]).toHaveProperty(
'name',
'metrics',
);
expect(querySection.controlSetRows[1][0]).toHaveProperty(
'name',
'groupby',
);
expect(querySection.controlSetRows[2][0]).toHaveProperty(
'name',
'adhoc_filters',
);
expect(querySection.controlSetRows[3][0]).toHaveProperty('name', 'limit');
expect(querySection.controlSetRows[3][1]).toHaveProperty(
'name',
'order_desc',
);
expect(querySection.controlSetRows[4][0]).toHaveProperty(
'name',
'row_limit',
);
// Verify second section
const optionsSection = controlPanel.controlPanelSections[1];
expect(optionsSection.controlSetRows[0][0]).toHaveProperty(
'name',
'color_scheme',
);
expect(optionsSection.controlSetRows[1][0]).toHaveProperty(
'name',
'y_axis_format',
);
});
});
});

View File

@@ -20,19 +20,15 @@ import {
expandControlConfig,
sharedControls,
CustomControlItem,
sharedControlComponents,
} from '../../src';
describe('expandControlConfig()', () => {
it('throws error when string control is passed', () => {
expect(() => expandControlConfig('metrics' as any)).toThrow(
'String control reference "metrics" is not supported',
);
expect(() => expandControlConfig('groupby' as any)).toThrow(
'String control reference "groupby" is not supported',
);
expect(() => expandControlConfig('columns' as any)).toThrow(
'String control reference "columns" is not supported',
);
it('expands shared control alias', () => {
expect(expandControlConfig('metrics')).toEqual({
name: 'metrics',
config: sharedControls.metrics,
});
});
it('expands control with overrides', () => {
@@ -73,7 +69,7 @@ describe('expandControlConfig()', () => {
};
expect(
(expandControlConfig(input) as CustomControlItem).config.type,
).toEqual('RadioButtonControl');
).toEqual(sharedControlComponents.RadioButtonControl);
});
it('leave NULL and ReactElement untouched', () => {
@@ -82,6 +78,11 @@ describe('expandControlConfig()', () => {
expect(expandControlConfig(input)).toBe(input);
});
it('leave unknown text untouched', () => {
const input = 'superset-ui';
expect(expandControlConfig(input as never)).toBe(input);
});
it('return null for invalid configs', () => {
expect(
expandControlConfig({ type: 'SelectControl', label: 'Hello' } as never),

View File

@@ -37,7 +37,7 @@
"d3-format": "^1.3.2",
"dayjs": "^1.11.13",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-scale": "^3.0.0",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dompurify": "^3.2.4",
@@ -59,7 +59,7 @@
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"reselect": "^5.1.1",
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
"@visx/responsive": "^3.12.0",

View File

@@ -17,8 +17,11 @@
* under the License.
*/
/** Type checking is disabled for this file due to reselect only supporting
* TS declarations for selectors with up to 12 arguments. */
// @ts-nocheck
import { RefObject } from 'react';
import { createSelector, lruMemoize } from 'reselect';
import { createSelector } from 'reselect';
import {
AppSection,
Behavior,
@@ -34,7 +37,7 @@ import {
SetDataMaskHook,
} from '../types/Base';
import { QueryData, DataRecordFilters } from '..';
import { supersetTheme, SupersetTheme } from '../../theme';
import { SupersetTheme } from '../../theme';
// TODO: more specific typing for these fields of ChartProps
type AnnotationData = PlainObject;
@@ -106,8 +109,6 @@ export interface ChartPropsConfig {
theme: SupersetTheme;
/* legend index */
legendIndex?: number;
inContextMenu?: boolean;
emitCrossFilters?: boolean;
}
const DEFAULT_WIDTH = 800;
@@ -160,11 +161,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
theme: SupersetTheme;
constructor(
config: ChartPropsConfig & { formData?: FormData } = {
theme: supersetTheme,
},
) {
constructor(config: ChartPropsConfig & { formData?: FormData } = {}) {
const {
annotationData = {},
datasource = {},
@@ -279,16 +276,5 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
emitCrossFilters,
theme,
}),
// Below config is to retain usage of 1-sized `lruMemoize` object in Reselect v4
// Reselect v5 introduces `weakMapMemoize` which is more performant but potentially memory-leaky
// due to infinite cache size.
// Source: https://github.com/reduxjs/reselect/releases/tag/v5.0.1
{
memoize: lruMemoize,
argsMemoize: lruMemoize,
memoizeOptions: {
maxSize: 10,
},
},
);
};

View File

@@ -35,11 +35,6 @@ import { useTheme, css } from '@superset-ui/core';
import { Global } from '@emotion/react';
export { getTooltipHTML } from './Tooltip';
export { useJsonValidation } from './useJsonValidation';
export type {
JsonValidationAnnotation,
UseJsonValidationOptions,
} from './useJsonValidation';
export interface AceCompleterKeywordData {
name: string;
@@ -270,7 +265,7 @@ export function AsyncAceEditor(
/* Adjust tooltip styles */
.ace_tooltip {
margin-left: ${token.margin}px;
padding: ${token.sizeUnit * 2}px;
padding: 0px;
background-color: ${token.colorBgElevated} !important;
color: ${token.colorText} !important;
border: 1px solid ${token.colorBorderSecondary};

View File

@@ -1,75 +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 { renderHook } from '@testing-library/react-hooks';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {
it('returns empty array for valid JSON', () => {
const { result } = renderHook(() => useJsonValidation('{"key": "value"}'));
expect(result.current).toEqual([]);
});
it('returns empty array when disabled', () => {
const { result } = renderHook(() =>
useJsonValidation('invalid json', { enabled: false }),
);
expect(result.current).toEqual([]);
});
it('returns empty array for empty input', () => {
const { result } = renderHook(() => useJsonValidation(''));
expect(result.current).toEqual([]);
});
it('extracts line and column from error message with parentheses', () => {
// Since we can't control the exact error message from JSON.parse,
// let's test with a mock that demonstrates the pattern matching
const mockError = {
message:
"Expected ',' or '}' after property value in JSON at position 19 (line 3 column 2)",
};
// Test the regex pattern directly
const match = mockError.message.match(/\(line (\d+) column (\d+)\)/);
expect(match).toBeTruthy();
expect(match![1]).toBe('3');
expect(match![2]).toBe('2');
});
it('returns error on first line when no line/column info in message', () => {
const invalidJson = '{invalid}';
const { result } = renderHook(() => useJsonValidation(invalidJson));
expect(result.current).toHaveLength(1);
expect(result.current[0]).toMatchObject({
type: 'error',
row: 0,
column: 0,
text: expect.stringContaining('Invalid JSON'),
});
});
it('uses custom error prefix', () => {
const { result } = renderHook(() =>
useJsonValidation('{invalid}', { errorPrefix: 'Custom error' }),
);
expect(result.current[0].text).toContain('Custom error');
});
});

Some files were not shown because too many files have changed in this diff Show More