mirror of
https://github.com/apache/superset.git
synced 2026-06-29 03:15:34 +00:00
Compare commits
69 Commits
mcp_servic
...
move-contr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e85ee6d9 | ||
|
|
361a7f0f94 | ||
|
|
184f800ec1 | ||
|
|
a5bc492a95 | ||
|
|
544236ff20 | ||
|
|
2670c3e951 | ||
|
|
0f60a8d57b | ||
|
|
51c40dc971 | ||
|
|
5c90fca556 | ||
|
|
859e627c30 | ||
|
|
980c06e7d7 | ||
|
|
204b32e4a0 | ||
|
|
3603775df1 | ||
|
|
cf3b93b7bc | ||
|
|
df772a9afa | ||
|
|
d01c038471 | ||
|
|
7f4a3a3d0f | ||
|
|
c198b990a3 | ||
|
|
286b4d81e9 | ||
|
|
446beb4d2e | ||
|
|
0ea89c1c57 | ||
|
|
26f0556bef | ||
|
|
1137185842 | ||
|
|
97913203e1 | ||
|
|
06b98c1095 | ||
|
|
fe0ea69280 | ||
|
|
761daec53d | ||
|
|
407fb67f1e | ||
|
|
49689eec6c | ||
|
|
791ea9860d | ||
|
|
2f8939d229 | ||
|
|
ccf6290120 | ||
|
|
96a1aa60e8 | ||
|
|
2ea0368c2d | ||
|
|
9e407e4e80 | ||
|
|
360e58c181 | ||
|
|
22d5eb7835 | ||
|
|
7c4a77a909 | ||
|
|
4e209e51d0 | ||
|
|
7191ae55c8 | ||
|
|
17725ebc83 | ||
|
|
1a7a381bd5 | ||
|
|
daf207e5c2 | ||
|
|
72294c569f | ||
|
|
792dd08d38 | ||
|
|
1e40e7d02b | ||
|
|
7e98c75f01 | ||
|
|
b18de05ea4 | ||
|
|
9300652277 | ||
|
|
7c2ec4ca5f | ||
|
|
6a83b6fd87 | ||
|
|
659cd33749 | ||
|
|
cb27d5fe8d | ||
|
|
6c9cda758a | ||
|
|
967134f540 | ||
|
|
25bb353f9d | ||
|
|
9cf2472291 | ||
|
|
cf5b976659 | ||
|
|
70394e79ef | ||
|
|
ea64f3122e | ||
|
|
50197fc33e | ||
|
|
c480fa7fcf | ||
|
|
6fc734da51 | ||
|
|
762a11b0bb | ||
|
|
f168dd69a8 | ||
|
|
becd0b8883 | ||
|
|
fd4570625a | ||
|
|
54a5b58e40 | ||
|
|
a611278e04 |
40
.claude_rc
Normal file
40
.claude_rc
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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
|
||||
20
.devcontainer/Dockerfile
Normal file
20
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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}"
|
||||
@@ -3,3 +3,14 @@
|
||||
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.
|
||||
|
||||
62
.devcontainer/bashrc-additions
Normal file
62
.devcontainer/bashrc-additions
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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 ""
|
||||
20
.devcontainer/build-and-push-image.sh
Executable file
20
.devcontainer/build-and-push-image.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/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\""
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
// Extend the base configuration
|
||||
"extends": "../devcontainer-base.json",
|
||||
|
||||
"name": "Apache Superset Development (Default)",
|
||||
|
||||
// Forward ports for development
|
||||
"forwardPorts": [9001],
|
||||
"portsAttributes": {
|
||||
"9001": {
|
||||
"label": "Superset (via Webpack Dev Server)",
|
||||
"onAutoForward": "notify",
|
||||
"visibility": "public"
|
||||
}
|
||||
},
|
||||
|
||||
// Auto-start Superset on Codespace resume
|
||||
"postStartCommand": ".devcontainer/start-superset.sh"
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "Apache Superset Development",
|
||||
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
|
||||
// Using the same base as Dockerfile, but non-slim for dev tools
|
||||
"image": "python:3.11.13-bookworm",
|
||||
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
// Run commands after container is created
|
||||
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
|
||||
|
||||
// VS Code customizations
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"charliermarsh.ruff",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "Apache Superset Development",
|
||||
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
|
||||
// Using the same base as Dockerfile, but non-slim for dev tools
|
||||
"image": "python:3.11.13-bookworm",
|
||||
// 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": {
|
||||
@@ -32,10 +39,17 @@
|
||||
},
|
||||
|
||||
// Run commands after container is created
|
||||
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
|
||||
"postCreateCommand": "bash .devcontainer/setup-dev.sh || echo '⚠️ Setup had issues - run .devcontainer/setup-dev.sh manually'",
|
||||
|
||||
// Auto-start Superset on Codespace resume
|
||||
"postStartCommand": ".devcontainer/start-superset.sh",
|
||||
// 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": {
|
||||
@@ -3,30 +3,76 @@
|
||||
|
||||
echo "🔧 Setting up Superset development environment..."
|
||||
|
||||
# The universal image has most tools, just need Superset-specific libs
|
||||
echo "📦 Installing Superset-specific dependencies..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libsasl2-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
tmux \
|
||||
gh
|
||||
# System dependencies and uv are now pre-installed in the Docker image
|
||||
# This speeds up Codespace creation significantly!
|
||||
|
||||
# Install uv for fast Python package management
|
||||
echo "📦 Installing uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
# Create virtual environment using uv
|
||||
echo "🐍 Creating Python virtual environment..."
|
||||
if ! uv venv; then
|
||||
echo "❌ Failed to create virtual environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add cargo/bin to PATH for uv
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
|
||||
# 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..."
|
||||
npm install -g @anthropic-ai/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 "🚀 Run '.devcontainer/start-superset.sh' to start Superset"
|
||||
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 ""
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/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"
|
||||
|
||||
# Check if MCP is enabled
|
||||
if [ "$ENABLE_MCP" = "true" ]; then
|
||||
echo "🤖 MCP Service will be available at port 5008"
|
||||
fi
|
||||
|
||||
# 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
|
||||
@@ -18,32 +18,71 @@ else
|
||||
echo "📁 Using current directory: $(pwd)"
|
||||
fi
|
||||
|
||||
# Check if docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "⏳ Waiting for Docker to start..."
|
||||
sleep 5
|
||||
# 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 --profile mcp down
|
||||
docker-compose -f docker-compose-light.yml down
|
||||
|
||||
# Start services
|
||||
echo "🏗️ Building and starting services..."
|
||||
echo "🏗️ Starting Superset in background (daemon mode)..."
|
||||
echo ""
|
||||
echo "📝 Once started, login with:"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin"
|
||||
echo ""
|
||||
echo "📋 Running in foreground with live logs (Ctrl+C to stop)..."
|
||||
|
||||
# Run docker-compose and capture exit code
|
||||
if [ "$ENABLE_MCP" = "true" ]; then
|
||||
echo "🤖 Starting with MCP Service enabled..."
|
||||
docker-compose -f docker-compose-light.yml --profile mcp up
|
||||
else
|
||||
docker-compose -f docker-compose-light.yml up
|
||||
fi
|
||||
# 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
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
// Extend the base configuration
|
||||
"extends": "../devcontainer-base.json",
|
||||
|
||||
"name": "Apache Superset Development with MCP",
|
||||
|
||||
// Forward ports for development
|
||||
"forwardPorts": [9001, 5008],
|
||||
"portsAttributes": {
|
||||
"9001": {
|
||||
"label": "Superset (via Webpack Dev Server)",
|
||||
"onAutoForward": "notify",
|
||||
"visibility": "public"
|
||||
},
|
||||
"5008": {
|
||||
"label": "MCP Service (Model Context Protocol)",
|
||||
"onAutoForward": "notify",
|
||||
"visibility": "private"
|
||||
}
|
||||
},
|
||||
|
||||
// Auto-start Superset with MCP on Codespace resume
|
||||
"postStartCommand": "ENABLE_MCP=true .devcontainer/start-superset.sh",
|
||||
|
||||
// Environment variables
|
||||
"containerEnv": {
|
||||
"ENABLE_MCP": "true"
|
||||
}
|
||||
}
|
||||
2
.github/workflows/welcome-new-users.yml
vendored
2
.github/workflows/welcome-new-users.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Welcome Message
|
||||
uses: actions/first-interaction@v1
|
||||
uses: actions/first-interaction@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ __pycache__
|
||||
.cache
|
||||
.bento*
|
||||
.cache-loader
|
||||
.claude_rc
|
||||
.coverage
|
||||
cover
|
||||
.DS_Store
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
# Chart Metadata API Reference
|
||||
|
||||
The Superset MCP service provides rich metadata alongside chart generation to enable better UI integration and user experiences.
|
||||
|
||||
## Background & Design Philosophy
|
||||
|
||||
Modern chart systems need to provide more than just visual output. Inspired by contemporary web standards and LLM integration patterns, this metadata system addresses several key needs:
|
||||
|
||||
**Accessibility-First Design**: Following WCAG guidelines and `aria-*` attribute patterns, charts include semantic descriptions and accessibility metadata to ensure inclusive experiences.
|
||||
|
||||
**Rich Context for AI Systems**: Similar to how platforms like social media generate rich previews (OpenGraph, Twitter Cards), charts provide semantic understanding beyond just visual representation - enabling AI agents to reason about and describe visualizations meaningfully.
|
||||
|
||||
**Performance-Aware Integration**: Modern web APIs emphasize performance transparency (Core Web Vitals, etc.). Charts include execution metrics and optimization suggestions to help UIs make informed decisions about rendering and user feedback.
|
||||
|
||||
**Capability-Driven UX**: Rather than requiring UIs to hardcode chart type behaviors, the system exposes what each chart can actually do - enabling dynamic, contextual interfaces that adapt to chart capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
When generating charts via `generate_chart`, the response includes structured metadata that helps UIs:
|
||||
- Present appropriate controls and interactions
|
||||
- Generate accessible descriptions
|
||||
- Optimize rendering performance
|
||||
- Guide user workflows
|
||||
|
||||
## Metadata Types
|
||||
|
||||
### ChartCapabilities
|
||||
|
||||
Describes what interactions and features the chart supports.
|
||||
|
||||
```python
|
||||
{
|
||||
"supports_interaction": bool, # User can interact (zoom, pan, hover)
|
||||
"supports_real_time": bool, # Chart can update with live data
|
||||
"supports_drill_down": bool, # Can navigate to more detailed views
|
||||
"supports_export": bool, # Can be exported to other formats
|
||||
"optimal_formats": [ # Recommended preview formats
|
||||
"url", # Static image URL
|
||||
"interactive", # HTML with JavaScript controls
|
||||
"ascii", # Text-based representation
|
||||
"vega_lite" # Vega-Lite specification
|
||||
],
|
||||
"data_types": [ # Types of data visualized
|
||||
"time_series", # Time-based data
|
||||
"categorical", # Discrete categories
|
||||
"metric" # Numeric measurements
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**UI Integration:**
|
||||
- Show/hide interaction controls based on `supports_interaction`
|
||||
- Enable real-time updates if `supports_real_time`
|
||||
- Display drill-down options for `supports_drill_down`
|
||||
- Choose optimal preview format from `optimal_formats`
|
||||
|
||||
### ChartSemantics
|
||||
|
||||
Provides semantic understanding of what the chart represents and reveals.
|
||||
|
||||
```python
|
||||
{
|
||||
"primary_insight": "Shows trends and changes over time",
|
||||
"data_story": "This line chart analyzes sales, revenue over Q1-Q4",
|
||||
"recommended_actions": [
|
||||
"Review data patterns and trends",
|
||||
"Consider filtering for more detail",
|
||||
"Export chart for reporting"
|
||||
],
|
||||
"anomalies": [], # Notable outliers (future enhancement)
|
||||
"statistical_summary": {} # Key statistics (future enhancement)
|
||||
}
|
||||
```
|
||||
|
||||
**UI Integration:**
|
||||
- Display `primary_insight` as chart description
|
||||
- Use `data_story` for accessibility and tooltips
|
||||
- Show `recommended_actions` as suggested next steps
|
||||
- Highlight `anomalies` in the visualization
|
||||
|
||||
### AccessibilityMetadata
|
||||
|
||||
Information for creating inclusive, accessible chart experiences.
|
||||
|
||||
```python
|
||||
{
|
||||
"color_blind_safe": bool, # Uses colorblind-friendly palette
|
||||
"alt_text": "Chart showing Sales Data over time",
|
||||
"high_contrast_available": bool # High contrast version available
|
||||
}
|
||||
```
|
||||
|
||||
**UI Integration:**
|
||||
- Use `alt_text` for screen readers
|
||||
- Show accessibility indicators if `color_blind_safe`
|
||||
- Offer high contrast mode if available
|
||||
|
||||
### PerformanceMetadata
|
||||
|
||||
Performance information for optimization and user feedback.
|
||||
|
||||
```python
|
||||
{
|
||||
"query_duration_ms": 1250, # Time to generate chart data
|
||||
"cache_status": "hit|miss|error", # Whether data came from cache
|
||||
"optimization_suggestions": [ # Performance improvement tips
|
||||
"Consider adding date filters to reduce data volume",
|
||||
"Chart complexity may impact load time"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**UI Integration:**
|
||||
- Show loading indicators based on `query_duration_ms`
|
||||
- Display cache status for debugging
|
||||
- Present `optimization_suggestions` to users
|
||||
- Warn about slow queries
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"chart": {
|
||||
"id": 123,
|
||||
"slice_name": "Sales Trends Q1-Q4",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"url": "/explore/?slice_id=123"
|
||||
},
|
||||
"capabilities": {
|
||||
"supports_interaction": true,
|
||||
"supports_real_time": false,
|
||||
"supports_drill_down": false,
|
||||
"supports_export": true,
|
||||
"optimal_formats": ["url", "interactive", "ascii"],
|
||||
"data_types": ["time_series", "metric"]
|
||||
},
|
||||
"semantics": {
|
||||
"primary_insight": "Shows trends and changes over time",
|
||||
"data_story": "This line chart analyzes sales over Q1-Q4",
|
||||
"recommended_actions": [
|
||||
"Review seasonal patterns",
|
||||
"Export for quarterly report"
|
||||
]
|
||||
},
|
||||
"accessibility": {
|
||||
"color_blind_safe": true,
|
||||
"alt_text": "Line chart showing sales trends from Q1 to Q4",
|
||||
"high_contrast_available": false
|
||||
},
|
||||
"performance": {
|
||||
"query_duration_ms": 450,
|
||||
"cache_status": "miss",
|
||||
"optimization_suggestions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### React Component Integration
|
||||
|
||||
```jsx
|
||||
function ChartComponent({ chartData }) {
|
||||
const { capabilities, semantics, accessibility, performance } = chartData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Accessibility */}
|
||||
<img
|
||||
src={chartData.chart.url}
|
||||
alt={accessibility.alt_text}
|
||||
aria-describedby="chart-description"
|
||||
/>
|
||||
|
||||
{/* Semantic description */}
|
||||
<p id="chart-description">{semantics.primary_insight}</p>
|
||||
|
||||
{/* Conditional controls based on capabilities */}
|
||||
{capabilities.supports_interaction && (
|
||||
<InteractiveControls />
|
||||
)}
|
||||
|
||||
{capabilities.supports_export && (
|
||||
<ExportButton />
|
||||
)}
|
||||
|
||||
{/* Performance feedback */}
|
||||
{performance.query_duration_ms > 2000 && (
|
||||
<SlowQueryWarning suggestions={performance.optimization_suggestions} />
|
||||
)}
|
||||
|
||||
{/* Recommended actions */}
|
||||
<ActionSuggestions actions={semantics.recommended_actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Chart Type Mapping
|
||||
|
||||
Different chart types provide different capabilities:
|
||||
|
||||
| Chart Type | Interaction | Real-time | Drill-down | Optimal Formats |
|
||||
|------------|------------|-----------|------------|-----------------|
|
||||
| `echarts_timeseries_line` | ✅ | ✅ | ❌ | url, interactive, ascii |
|
||||
| `echarts_timeseries_bar` | ✅ | ✅ | ❌ | url, interactive, ascii |
|
||||
| `table` | ❌ | ❌ | ✅ | url, table, ascii |
|
||||
| `pie` | ✅ | ❌ | ❌ | url, interactive |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Statistical Summary**: Automatic calculation of mean, median, trends
|
||||
- **Anomaly Detection**: Identification of outliers and unusual patterns
|
||||
- **Smart Recommendations**: ML-powered suggestions for chart improvements
|
||||
- **Accessibility Scoring**: Automated accessibility compliance checking
|
||||
802
CONTROL_PANEL_MIGRATION_AGENT.md
Normal file
802
CONTROL_PANEL_MIGRATION_AGENT.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# 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.
|
||||
153
CONTROL_PANEL_MODERNIZATION.md
Normal file
153
CONTROL_PANEL_MODERNIZATION.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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
|
||||
75
LLMS.md
75
LLMS.md
@@ -180,7 +180,6 @@ pre-commit run eslint # Frontend linting
|
||||
|
||||
## Platform-Specific Instructions
|
||||
|
||||
- **[LLMS.md](LLMS.md)** - General LLM development guide (READ THIS FIRST)
|
||||
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
|
||||
@@ -190,3 +189,77 @@ 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.
|
||||
|
||||
257
PIE_CHART_MIGRATION_PLAN.md
Normal file
257
PIE_CHART_MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 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
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
|
||||
@@ -32,6 +32,7 @@ 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
|
||||
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
# - 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
|
||||
#
|
||||
# MCP Service (Model Context Protocol):
|
||||
# - Optional service for LLM agent integration, available under 'mcp' profile
|
||||
# - To include MCP: docker-compose -f docker-compose-light.yml --profile mcp up
|
||||
# - MCP runs on port 5008 by default (customize with MCP_PORT=5009)
|
||||
# - Enable SQL debugging with MCP_SQL_DEBUG=true
|
||||
#
|
||||
# For verbose logging during development:
|
||||
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -156,37 +150,6 @@ services:
|
||||
required: false
|
||||
volumes: *superset-volumes
|
||||
|
||||
superset-mcp-light:
|
||||
profiles:
|
||||
- mcp
|
||||
build:
|
||||
<<: *common-build
|
||||
command: ["/app/docker/docker-bootstrap.sh", "mcp"]
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${MCP_PORT:-5008}:5008" # Parameterized port
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
user: *superset-user
|
||||
depends_on:
|
||||
superset-init-light:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
env_file:
|
||||
- path: docker/.env # default
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
environment:
|
||||
# Override DB connection for light service
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
POSTGRES_DB: superset_light
|
||||
# Use light-specific config that disables Redis
|
||||
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
|
||||
# Enable SQL debugging for MCP if needed
|
||||
SQLALCHEMY_DEBUG: ${MCP_SQL_DEBUG:-false}
|
||||
|
||||
volumes:
|
||||
superset_home_light:
|
||||
external: false
|
||||
|
||||
@@ -78,10 +78,6 @@ case "${1}" in
|
||||
echo "Starting web app..."
|
||||
/usr/bin/run-server.sh
|
||||
;;
|
||||
mcp)
|
||||
echo "Starting MCP service..."
|
||||
superset mcp run --host 0.0.0.0 --port ${MCP_PORT:-5008} --debug
|
||||
;;
|
||||
*)
|
||||
echo "Unknown Operation!!!"
|
||||
;;
|
||||
|
||||
@@ -23,25 +23,57 @@ MIN_MEM_FREE_GB=3
|
||||
MIN_MEM_FREE_KB=$(($MIN_MEM_FREE_GB*1000000))
|
||||
|
||||
echo_mem_warn() {
|
||||
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 if running in Codespaces first
|
||||
if [[ -n "${CODESPACES}" ]]; then
|
||||
echo "Memory available: Codespaces managed"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${MEM_FREE_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
|
||||
# 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
|
||||
cat <<EOF
|
||||
===============================================
|
||||
======== Memory Insufficient Warning =========
|
||||
===============================================
|
||||
|
||||
It looks like you only have ${MEM_FREE_GB}GB of
|
||||
memory free. Please increase your Docker
|
||||
It looks like you only have ${MEM_AVAIL_GB}GB of
|
||||
memory ${MEM_TYPE}. 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 check Ok [${MEM_FREE_GB}GB free]"
|
||||
echo "Memory available: ${MEM_AVAIL_GB} GB"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,66 @@ Restart Superset to apply changes.
|
||||
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`:
|
||||
|
||||
```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"],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Custom Fonts in Themes
|
||||
|
||||
Once configured, reference the fonts in your theme configuration:
|
||||
|
||||
```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
|
||||
|
||||
@@ -137,7 +137,7 @@ contributing to Apache Superset more accessible to developers worldwide.
|
||||
|
||||
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=codespaces&geo=UsWest&devcontainer_path=.devcontainer%2Fdevcontainer.json)
|
||||
[**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).
|
||||
@@ -421,14 +421,6 @@ 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.
|
||||
|
||||
@@ -1,741 +0,0 @@
|
||||
---
|
||||
title: API Reference
|
||||
sidebar_position: 3
|
||||
version: 1
|
||||
---
|
||||
|
||||
# MCP Tools API Reference
|
||||
|
||||
Complete reference for all 16 MCP tools with request/response examples.
|
||||
|
||||
> 🚀 **First time here?** Start with [Dashboard Tools](#dashboard-tools) or [Chart Tools](#chart-tools) to see the most commonly used features.
|
||||
>
|
||||
> 🔐 **Need authentication?** See the [Authentication Guide](./authentication) for JWT setup.
|
||||
>
|
||||
> 🔧 **Want to add tools?** Check the [Development Guide](./development#adding-new-tools) for step-by-step instructions.
|
||||
|
||||
## Dashboard Tools
|
||||
|
||||
### list_dashboards
|
||||
|
||||
List dashboards with search, filtering, and pagination support.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"search": "sales", // Optional: Search term
|
||||
"filters": [ // Optional: Advanced filters
|
||||
{
|
||||
"col": "published",
|
||||
"opr": "eq",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"page": 1, // Optional: Page number (default: 1)
|
||||
"page_size": 20, // Optional: Items per page (default: 20)
|
||||
"select_columns": [ // Optional: Specific columns
|
||||
"id", "dashboard_title", "uuid"
|
||||
],
|
||||
"use_cache": true // Optional: Use cached data (default: true)
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"dashboards": [
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"dashboard_title": "Sales Performance",
|
||||
"url": "/superset/dashboard/1/",
|
||||
"published": true,
|
||||
"owners": ["admin"],
|
||||
"created_on": "2024-01-15T10:30:00Z",
|
||||
"changed_on": "2024-01-20T14:15:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 45,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"cache_status": {
|
||||
"cache_hit": true,
|
||||
"cache_age_seconds": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### get_dashboard_info
|
||||
|
||||
Get detailed information about a specific dashboard.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // ID, UUID, or slug
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"dashboard_id": 1,
|
||||
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"dashboard_title": "Sales Performance Dashboard",
|
||||
"slug": "sales-performance",
|
||||
"url": "/superset/dashboard/1/",
|
||||
"published": true,
|
||||
"owners": ["admin", "analyst"],
|
||||
"roles": ["Sales Team"],
|
||||
"charts": [
|
||||
{
|
||||
"id": 10,
|
||||
"slice_name": "Monthly Revenue",
|
||||
"viz_type": "line"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"slice_name": "Regional Sales",
|
||||
"viz_type": "bar"
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"column": "region",
|
||||
"type": "select"
|
||||
}
|
||||
],
|
||||
"created_on": "2024-01-15T10:30:00Z",
|
||||
"changed_on": "2024-01-20T14:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### generate_dashboard
|
||||
|
||||
Create a new dashboard with multiple charts.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"chart_ids": [10, 11, 12, 13],
|
||||
"dashboard_title": "Q4 Performance Dashboard",
|
||||
"description": "Quarterly performance metrics and KPIs",
|
||||
"published": true,
|
||||
"layout_type": "grid" // Optional: "grid" or "tabs"
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"dashboard_id": 25,
|
||||
"uuid": "new-dash-uuid-here",
|
||||
"dashboard_title": "Q4 Performance Dashboard",
|
||||
"url": "/superset/dashboard/25/",
|
||||
"charts_added": 4,
|
||||
"layout": {
|
||||
"type": "grid",
|
||||
"columns": 2,
|
||||
"rows": 2
|
||||
},
|
||||
"created_on": "2024-01-25T16:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Chart Tools
|
||||
|
||||
### list_charts
|
||||
|
||||
List charts with advanced filtering and search capabilities.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"search": "revenue",
|
||||
"filters": [
|
||||
{
|
||||
"col": "viz_type",
|
||||
"opr": "in",
|
||||
"value": ["line", "bar", "area"]
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 25,
|
||||
"select_columns": ["id", "slice_name", "viz_type", "uuid"],
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"charts": [
|
||||
{
|
||||
"id": 10,
|
||||
"uuid": "chart-uuid-1",
|
||||
"slice_name": "Monthly Revenue Trend",
|
||||
"viz_type": "line",
|
||||
"datasource_name": "sales_data",
|
||||
"owners": ["admin"],
|
||||
"created_on": "2024-01-10T09:15:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 125,
|
||||
"page": 1,
|
||||
"page_size": 25
|
||||
}
|
||||
```
|
||||
|
||||
### get_chart_info
|
||||
|
||||
Get comprehensive chart information including configuration.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"identifier": 10, // ID or UUID
|
||||
"include_form_data": true, // Include chart configuration
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"chart_id": 10,
|
||||
"uuid": "chart-uuid-1",
|
||||
"slice_name": "Monthly Revenue Trend",
|
||||
"viz_type": "line",
|
||||
"datasource_id": 5,
|
||||
"datasource_name": "sales_data",
|
||||
"datasource_type": "table",
|
||||
"form_data": {
|
||||
"viz_type": "line",
|
||||
"x_axis": "month",
|
||||
"metrics": ["sum__revenue"],
|
||||
"time_range": "Last 12 months"
|
||||
},
|
||||
"query_context": {
|
||||
"datasource": {"id": 5, "type": "table"},
|
||||
"queries": [{"columns": [], "metrics": ["sum__revenue"]}]
|
||||
},
|
||||
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A10%7D",
|
||||
"owners": ["admin"],
|
||||
"created_on": "2024-01-10T09:15:00Z",
|
||||
"changed_on": "2024-01-15T11:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### generate_chart
|
||||
|
||||
Create a new chart with specified configuration.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"dataset_id": "5",
|
||||
"config": {
|
||||
"chart_type": "xy",
|
||||
"x": {"name": "month", "label": "Month"},
|
||||
"y": [
|
||||
{
|
||||
"name": "revenue",
|
||||
"aggregate": "SUM",
|
||||
"label": "Total Revenue"
|
||||
},
|
||||
{
|
||||
"name": "orders",
|
||||
"aggregate": "COUNT",
|
||||
"label": "Order Count"
|
||||
}
|
||||
],
|
||||
"kind": "line",
|
||||
"x_axis": {
|
||||
"title": "Month",
|
||||
"format": "smart_date"
|
||||
},
|
||||
"y_axis": {
|
||||
"title": "Revenue ($)",
|
||||
"format": "$,.0f"
|
||||
},
|
||||
"legend": {
|
||||
"show": true,
|
||||
"position": "top"
|
||||
}
|
||||
},
|
||||
"slice_name": "Revenue and Orders Trend",
|
||||
"description": "Monthly revenue and order count comparison",
|
||||
"save_chart": true,
|
||||
"generate_preview": true,
|
||||
"preview_formats": ["url", "ascii"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"chart_id": 45,
|
||||
"uuid": "new-chart-uuid",
|
||||
"slice_name": "Revenue and Orders Trend",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"datasource_id": 5,
|
||||
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A45%7D",
|
||||
"query_executed": true,
|
||||
"query_result": {
|
||||
"status": "success",
|
||||
"row_count": 12,
|
||||
"execution_time": 0.145
|
||||
},
|
||||
"preview": {
|
||||
"url": {
|
||||
"preview_url": "http://localhost:5008/screenshot/chart/45.png",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
},
|
||||
"ascii": {
|
||||
"ascii_content": "Revenue Trend\n==============\nJan |████████████████ $125K\nFeb |██████████████████ $140K\n...",
|
||||
"width": 80,
|
||||
"height": 20
|
||||
}
|
||||
},
|
||||
"created_on": "2024-01-25T14:20:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### get_chart_data
|
||||
|
||||
Export chart data in multiple formats.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"identifier": 10,
|
||||
"format": "json", // "json", "csv", "excel"
|
||||
"limit": 1000, // Optional: Row limit
|
||||
"offset": 0, // Optional: Row offset
|
||||
"filters": [ // Optional: Additional filters
|
||||
{
|
||||
"column": "region",
|
||||
"op": "=",
|
||||
"value": "US"
|
||||
}
|
||||
],
|
||||
"use_cache": true,
|
||||
"force_refresh": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"month": "2024-01",
|
||||
"revenue": 125000,
|
||||
"orders": 450
|
||||
},
|
||||
{
|
||||
"month": "2024-02",
|
||||
"revenue": 140000,
|
||||
"orders": 520
|
||||
}
|
||||
],
|
||||
"total_rows": 12,
|
||||
"columns": [
|
||||
{"name": "month", "type": "DATE"},
|
||||
{"name": "revenue", "type": "BIGINT"},
|
||||
{"name": "orders", "type": "BIGINT"}
|
||||
],
|
||||
"query": {
|
||||
"sql": "SELECT month, SUM(revenue) as revenue, COUNT(*) as orders FROM sales_data GROUP BY month ORDER BY month",
|
||||
"execution_time": 0.089
|
||||
},
|
||||
"cache_status": {
|
||||
"cache_hit": false,
|
||||
"cache_type": "query",
|
||||
"refreshed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### get_chart_preview
|
||||
|
||||
Generate chart previews in multiple formats.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"identifier": 10,
|
||||
"format": "url", // "url", "base64", "ascii", "table"
|
||||
"width": 800, // For image formats
|
||||
"height": 600, // For image formats
|
||||
"ascii_width": 80, // For ASCII format
|
||||
"ascii_height": 20, // For ASCII format
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Examples:**
|
||||
|
||||
**URL Format:**
|
||||
```json
|
||||
{
|
||||
"format": "url",
|
||||
"preview_url": "http://localhost:5008/screenshot/chart/10.png",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"supports_interaction": false,
|
||||
"expires_at": "2024-01-26T14:20:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**ASCII Format:**
|
||||
```json
|
||||
{
|
||||
"format": "ascii",
|
||||
"ascii_content": "Monthly Revenue Trend\n=====================\n\nJan |████████████████████ $125K\nFeb |██████████████████████ $140K\nMar |███████████████████ $135K\nApr |█████████████████████████ $155K\n\nRange: $125K to $155K\n▁▃▂▅▇▆▄▃▂▄▅▆▇▅▃▂",
|
||||
"width": 80,
|
||||
"height": 20,
|
||||
"supports_color": false
|
||||
}
|
||||
```
|
||||
|
||||
**Table Format:**
|
||||
```json
|
||||
{
|
||||
"format": "table",
|
||||
"table_data": "Monthly Revenue Data\n====================\n\nMonth | Revenue | Orders\n---------|----------|--------\nJan 2024 | $125,000 | 450\nFeb 2024 | $140,000 | 520\nMar 2024 | $135,000 | 495\n\nTotal: 12 rows × 3 columns",
|
||||
"row_count": 12,
|
||||
"supports_sorting": true
|
||||
}
|
||||
```
|
||||
|
||||
## Dataset Tools
|
||||
|
||||
### list_datasets
|
||||
|
||||
List available datasets with columns and metrics.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"search": "sales",
|
||||
"filters": [
|
||||
{
|
||||
"col": "is_active",
|
||||
"opr": "eq",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"include_columns": true, // Include column metadata
|
||||
"include_metrics": true, // Include metric metadata
|
||||
"page": 1,
|
||||
"page_size": 15
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"datasets": [
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "dataset-uuid-1",
|
||||
"table_name": "sales_data",
|
||||
"database_name": "main_warehouse",
|
||||
"schema": "public",
|
||||
"owners": ["admin"],
|
||||
"columns": [
|
||||
{
|
||||
"column_name": "region",
|
||||
"type": "VARCHAR",
|
||||
"is_active": true,
|
||||
"is_dttm": false
|
||||
},
|
||||
{
|
||||
"column_name": "revenue",
|
||||
"type": "DECIMAL",
|
||||
"is_active": true,
|
||||
"is_dttm": false
|
||||
}
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"metric_name": "sum__revenue",
|
||||
"expression": "SUM(revenue)",
|
||||
"metric_type": "sum"
|
||||
}
|
||||
],
|
||||
"created_on": "2024-01-05T08:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 23,
|
||||
"page": 1,
|
||||
"page_size": 15
|
||||
}
|
||||
```
|
||||
|
||||
### get_dataset_info
|
||||
|
||||
Get detailed dataset information with full column/metric metadata.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"identifier": "dataset-uuid-1", // ID or UUID
|
||||
"include_columns": true,
|
||||
"include_metrics": true,
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"dataset_id": 1,
|
||||
"uuid": "dataset-uuid-1",
|
||||
"table_name": "sales_data",
|
||||
"database_name": "main_warehouse",
|
||||
"database_id": 1,
|
||||
"schema": "public",
|
||||
"sql": null,
|
||||
"is_active": true,
|
||||
"owners": ["admin", "data_team"],
|
||||
"columns": [
|
||||
{
|
||||
"id": 101,
|
||||
"column_name": "region",
|
||||
"type": "VARCHAR",
|
||||
"is_active": true,
|
||||
"is_dttm": false,
|
||||
"groupby": true,
|
||||
"filterable": true,
|
||||
"description": "Geographic region"
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"column_name": "order_date",
|
||||
"type": "DATE",
|
||||
"is_active": true,
|
||||
"is_dttm": true,
|
||||
"groupby": true,
|
||||
"filterable": true
|
||||
}
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"id": 201,
|
||||
"metric_name": "sum__revenue",
|
||||
"expression": "SUM(revenue)",
|
||||
"metric_type": "sum",
|
||||
"is_active": true,
|
||||
"description": "Total revenue"
|
||||
}
|
||||
],
|
||||
"created_on": "2024-01-05T08:00:00Z",
|
||||
"changed_on": "2024-01-18T12:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## System Tools
|
||||
|
||||
### get_superset_instance_info
|
||||
|
||||
Get Superset instance information and statistics.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"include_statistics": true, // Include usage statistics
|
||||
"include_tools": true, // Include available MCP tools
|
||||
"use_cache": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"version": "4.1.0",
|
||||
"build": "apache-superset-4.1.0",
|
||||
"mcp_service_version": "1.0.0",
|
||||
"authentication": {
|
||||
"enabled": true,
|
||||
"type": "jwt_bearer",
|
||||
"required_scopes": ["dashboard:read", "chart:read"]
|
||||
},
|
||||
"statistics": {
|
||||
"dashboards": {
|
||||
"total": 45,
|
||||
"published": 32
|
||||
},
|
||||
"charts": {
|
||||
"total": 125,
|
||||
"by_viz_type": {
|
||||
"line": 35,
|
||||
"bar": 28,
|
||||
"table": 42,
|
||||
"pie": 20
|
||||
}
|
||||
},
|
||||
"datasets": {
|
||||
"total": 23,
|
||||
"active": 18
|
||||
},
|
||||
"users": {
|
||||
"total": 15,
|
||||
"active": 12
|
||||
}
|
||||
},
|
||||
"mcp_tools": [
|
||||
{
|
||||
"name": "list_dashboards",
|
||||
"description": "List dashboards with search and filtering",
|
||||
"category": "dashboard"
|
||||
},
|
||||
{
|
||||
"name": "generate_chart",
|
||||
"description": "Create new charts programmatically",
|
||||
"category": "chart"
|
||||
}
|
||||
],
|
||||
"database_connections": [
|
||||
{
|
||||
"id": 1,
|
||||
"database_name": "main_warehouse",
|
||||
"backend": "postgresql",
|
||||
"status": "healthy"
|
||||
}
|
||||
],
|
||||
"cache_status": {
|
||||
"enabled": true,
|
||||
"backend": "redis",
|
||||
"hit_rate": 0.85
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### generate_explore_link
|
||||
|
||||
Generate Superset explore URLs with pre-configured chart settings.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"dataset_id": "1",
|
||||
"chart_config": {
|
||||
"viz_type": "line",
|
||||
"x_axis": "month",
|
||||
"metrics": ["sum__revenue"],
|
||||
"time_range": "Last 6 months"
|
||||
},
|
||||
"title": "Revenue Analysis",
|
||||
"cache_form_data": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"explore_url": "/superset/explore/?form_data_key=abc123def456",
|
||||
"full_url": "http://localhost:8088/superset/explore/?form_data_key=abc123def456",
|
||||
"form_data_key": "abc123def456",
|
||||
"expires_at": "2024-01-26T16:45:00Z",
|
||||
"chart_config": {
|
||||
"viz_type": "line",
|
||||
"datasource": "1__table",
|
||||
"x_axis": "month",
|
||||
"metrics": ["sum__revenue"],
|
||||
"time_range": "Last 6 months"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Lab Tools
|
||||
|
||||
### open_sql_lab_with_context
|
||||
|
||||
Open SQL Lab with pre-configured database, schema, and SQL.
|
||||
|
||||
**Request Schema:**
|
||||
```json
|
||||
{
|
||||
"database_connection_id": 1,
|
||||
"schema": "public",
|
||||
"dataset_in_context": "sales_data",
|
||||
"sql": "SELECT region, SUM(revenue) as total_revenue\nFROM sales_data \nWHERE order_date >= '2024-01-01'\nGROUP BY region\nORDER BY total_revenue DESC",
|
||||
"title": "Regional Sales Analysis"
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example:**
|
||||
```json
|
||||
{
|
||||
"sql_lab_url": "/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
|
||||
"full_url": "http://localhost:8088/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
|
||||
"database_connection": {
|
||||
"id": 1,
|
||||
"database_name": "main_warehouse",
|
||||
"backend": "postgresql"
|
||||
},
|
||||
"schema": "public",
|
||||
"sql_template": "SELECT region, SUM(revenue) as total_revenue...",
|
||||
"context": {
|
||||
"dataset": "sales_data",
|
||||
"title": "Regional Sales Analysis"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
All tools can return error responses with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Chart not found with identifier: 999",
|
||||
"error_type": "NotFound",
|
||||
"suggestions": [
|
||||
"Verify the chart ID exists",
|
||||
"Check if you have permission to access this chart",
|
||||
"Try using the chart UUID instead of ID"
|
||||
],
|
||||
"details": {
|
||||
"identifier": 999,
|
||||
"identifier_type": "id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Status
|
||||
|
||||
Many responses include cache status information:
|
||||
|
||||
```json
|
||||
{
|
||||
"cache_status": {
|
||||
"cache_hit": true, // Data served from cache
|
||||
"cache_type": "query", // Type: query, metadata, form_data
|
||||
"cache_age_seconds": 300, // Age of cached data
|
||||
"refreshed": false // Whether cache was refreshed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This API reference provides complete documentation for integrating with the Superset MCP service, including all request schemas, response formats, and error handling patterns.
|
||||
|
||||
## What's Next?
|
||||
|
||||
### 🔐 **Ready for Production?**
|
||||
Set up authentication and security with the [Authentication Guide](./authentication).
|
||||
|
||||
### 🔧 **Want to Add More Tools?**
|
||||
Learn how to extend the MCP service in the [Development Guide](./development).
|
||||
|
||||
### 🏗️ **Need Architecture Details?**
|
||||
Understand the system design in the [Architecture Overview](./architecture).
|
||||
|
||||
### 🏢 **Enterprise Features?**
|
||||
Explore advanced capabilities in the [Preset Integration Guide](./preset-integration).
|
||||
|
||||
> 📖 **Back to Documentation Index**: [MCP Service](./intro)
|
||||
@@ -1,191 +0,0 @@
|
||||
---
|
||||
title: Architecture Overview
|
||||
sidebar_position: 5
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP for LLM agents and automation tools.
|
||||
|
||||
**Status:** Phase 1 Complete. Core functionality stable, authentication production-ready. See [SIP-171](https://github.com/apache/superset/issues/33870) for roadmap.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Tool Structure
|
||||
- **16 MCP tools** organized by domain: `dashboard/`, `chart/`, `dataset/`, `system/`
|
||||
- All tools decorated with `@mcp.tool` and `@mcp_auth_hook`
|
||||
- **Import inside functions**: All Superset DAOs/commands imported in function body to ensure proper app context
|
||||
- Pydantic v2 schemas with LLM/OpenAPI-compatible field descriptions
|
||||
|
||||
### Request Schema Pattern
|
||||
Eliminates LLM parameter validation issues using structured request objects:
|
||||
```python
|
||||
# New approach - single request object
|
||||
get_dataset_info(request={"identifier": 123}) # ID
|
||||
get_dataset_info(request={"identifier": "uuid-string"}) # UUID
|
||||
|
||||
# Old approach - replaced
|
||||
get_dataset_info(dataset_id=123)
|
||||
```
|
||||
|
||||
### Multi-Identifier Support
|
||||
- **Charts/Datasets**: ID (numeric) or UUID (string)
|
||||
- **Dashboards**: ID (numeric), UUID (string), or slug (string)
|
||||
- Validation prevents conflicting parameters (search + filters)
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Dashboard Tools (5)
|
||||
- `list_dashboards` - List with search/filters/pagination
|
||||
- `get_dashboard_info` - Get by ID/UUID/slug
|
||||
- `get_dashboard_available_filters` - Discover filterable columns
|
||||
- `generate_dashboard` - Create dashboards with multiple charts
|
||||
- `add_chart_to_existing_dashboard` - Add charts to existing dashboards
|
||||
|
||||
### Chart Tools (8)
|
||||
- `list_charts` - List with search/filters/pagination
|
||||
- `get_chart_info` - Get by ID/UUID
|
||||
- `get_chart_available_filters` - Discover filterable columns
|
||||
- `generate_chart` - Create charts (table, line, bar, area, scatter)
|
||||
- `update_chart` - Update saved charts
|
||||
- `update_chart_preview` - Update cached previews
|
||||
- `get_chart_data` - Export data (JSON/CSV/Excel)
|
||||
- `get_chart_preview` - Screenshots, ASCII art, table previews
|
||||
|
||||
### Dataset Tools (3)
|
||||
- `list_datasets` - List with columns/metrics
|
||||
- `get_dataset_info` - Get by ID/UUID with metadata
|
||||
- `get_dataset_available_filters` - Discover filterable columns
|
||||
|
||||
### System Tools (2)
|
||||
- `get_superset_instance_info` - Instance statistics and version
|
||||
- `generate_explore_link` - Generate chart exploration URLs
|
||||
|
||||
### SQL Lab Tools (1)
|
||||
- `open_sql_lab_with_context` - Pre-configured SQL Lab sessions
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### JWT Bearer Authentication
|
||||
Production-ready authentication with configurable factory pattern:
|
||||
```python
|
||||
# In superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://auth.company.com/"
|
||||
MCP_JWT_AUDIENCE = "superset-mcp-api"
|
||||
```
|
||||
|
||||
### Scope-Based Authorization
|
||||
| Tool Category | Required Scope |
|
||||
|---------------|----------------|
|
||||
| Dashboard ops | `dashboard:read` |
|
||||
| Chart ops | `chart:read` / `chart:write` |
|
||||
| Dataset ops | `dataset:read` |
|
||||
| System ops | `instance:read` |
|
||||
|
||||
### Audit Logging
|
||||
All operations logged with MCP context:
|
||||
- User impersonation tracking
|
||||
- Tool execution details
|
||||
- Sanitized payloads (sensitive data redacted)
|
||||
|
||||
## Cache Control
|
||||
|
||||
Leverages Superset's existing cache layers with comprehensive control:
|
||||
|
||||
### Cache Types
|
||||
1. **Query Result Cache** - Database query results
|
||||
2. **Metadata Cache** - Table schemas, columns, metrics
|
||||
3. **Form Data Cache** - Chart configurations
|
||||
4. **Dashboard Cache** - Rendered components
|
||||
|
||||
### Cache Parameters
|
||||
Tools support cache control through request schemas:
|
||||
- `use_cache`: Enable/disable caching (default: true)
|
||||
- `force_refresh`: Force cache refresh (default: false)
|
||||
- `cache_timeout`: Override timeout in seconds
|
||||
- `refresh_metadata`: Force metadata refresh
|
||||
|
||||
### Cache Status Reporting
|
||||
```json
|
||||
{
|
||||
"cache_status": {
|
||||
"cache_hit": true,
|
||||
"cache_type": "query",
|
||||
"cache_age_seconds": 300,
|
||||
"refreshed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Abstractions
|
||||
|
||||
### Generic Base Classes
|
||||
- **ModelListTool**: Handles list/search/filter operations with pagination
|
||||
- **ModelGetInfoTool**: Single object retrieval by multiple identifier types
|
||||
- **ModelGetAvailableFiltersTool**: Returns filterable columns/operators
|
||||
|
||||
### Implementation Pattern
|
||||
```python
|
||||
@mcp.tool
|
||||
@mcp_auth_hook
|
||||
def my_tool(request: MyRequest) -> MyResponse:
|
||||
# Import Superset modules inside function
|
||||
from superset.daos.dashboard import DashboardDAO
|
||||
from superset.commands.chart.create import CreateChartCommand
|
||||
|
||||
# Tool implementation
|
||||
return response
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### URL Configuration
|
||||
Centralized URL management for consistent link generation:
|
||||
```python
|
||||
# In superset_config.py
|
||||
SUPERSET_WEBSERVER_ADDRESS = "http://localhost:8088" # Development
|
||||
SUPERSET_WEBSERVER_ADDRESS = "https://superset.company.com" # Production
|
||||
```
|
||||
|
||||
### Schema Design Principles
|
||||
- **Minimal columns** in list responses
|
||||
- **Optional fields** in info schemas for missing data handling
|
||||
- **Null exclusion** for cleaner JSON responses
|
||||
- **Type safety** with clear Pydantic validation
|
||||
|
||||
## Adding New Tools
|
||||
|
||||
1. **Choose domain folder**: `dashboard/`, `chart/`, `dataset/`, or `system/`
|
||||
2. **Define schemas**: Use Pydantic with field descriptions
|
||||
3. **Implement tool**:
|
||||
- Decorate with `@mcp.tool` and `@mcp_auth_hook`
|
||||
- Import Superset modules inside function body
|
||||
- Use generic abstractions where applicable
|
||||
4. **Register**: Add to appropriate `__init__.py`
|
||||
5. **Test**: Add unit tests in `tests/unit_tests/mcp_service/`
|
||||
|
||||
## Current Status
|
||||
|
||||
### ✅ Phase 1 Complete
|
||||
- FastMCP server with CLI
|
||||
- JWT authentication with RBAC
|
||||
- All 16 core tools implemented
|
||||
- Request schema pattern
|
||||
- Cache control system
|
||||
- Audit logging
|
||||
- 194+ unit tests
|
||||
|
||||
### 🎯 Future Enhancements
|
||||
- Demo notebooks and video examples
|
||||
- OAuth integration for user impersonation
|
||||
- Enhanced chart rendering formats
|
||||
- Advanced security features
|
||||
|
||||
**Production Ready**: Core functionality stable with comprehensive testing and authentication.
|
||||
|
||||
---
|
||||
|
||||
For setup and usage, see the [MCP Service overview](./intro).
|
||||
@@ -1,434 +0,0 @@
|
||||
---
|
||||
title: Authentication & Security
|
||||
sidebar_position: 4
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Authentication & Security
|
||||
|
||||
The MCP service provides enterprise-grade JWT Bearer authentication with flexible configuration options and comprehensive security controls.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development Mode (Default)
|
||||
|
||||
:::tip
|
||||
Authentication is **disabled by default** for local development - no configuration needed.
|
||||
:::
|
||||
|
||||
```bash
|
||||
# No configuration needed - service runs without authentication
|
||||
superset mcp run --port 5008 --debug
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
|
||||
:::warning
|
||||
Always enable authentication for production deployments to secure your Superset instance.
|
||||
:::
|
||||
|
||||
Enable JWT authentication in your Superset configuration:
|
||||
|
||||
```python
|
||||
# In superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://auth.company.com/"
|
||||
MCP_JWT_AUDIENCE = "superset-mcp-api"
|
||||
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read", "dataset:read"]
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Option 1: Simple Configuration
|
||||
|
||||
Add to your `superset_config.py`:
|
||||
|
||||
```python
|
||||
# Enable authentication
|
||||
MCP_AUTH_ENABLED = True
|
||||
|
||||
# JWT settings
|
||||
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://auth.company.com/"
|
||||
MCP_JWT_AUDIENCE = "superset-mcp-api"
|
||||
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read"]
|
||||
|
||||
# Optional: User resolution
|
||||
MCP_JWT_USER_CLAIM = "sub" # JWT claim for username (default: "sub")
|
||||
MCP_JWT_EMAIL_CLAIM = "email" # JWT claim for email (default: "email")
|
||||
MCP_FALLBACK_USER = "admin" # Fallback user if JWT user not found
|
||||
```
|
||||
|
||||
### Option 2: Custom Factory
|
||||
|
||||
For advanced authentication requirements:
|
||||
|
||||
```python
|
||||
def create_custom_mcp_auth(app):
|
||||
"""Custom auth factory for enterprise environments."""
|
||||
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
|
||||
|
||||
return BearerAuthProvider(
|
||||
jwks_uri=app.config["MCP_JWKS_URI"],
|
||||
issuer=app.config["MCP_JWT_ISSUER"],
|
||||
audience=app.config["MCP_JWT_AUDIENCE"],
|
||||
required_scopes=app.config.get("MCP_REQUIRED_SCOPES", []),
|
||||
user_resolver=custom_user_resolver,
|
||||
cache_ttl=300 # Cache JWKS for 5 minutes
|
||||
)
|
||||
|
||||
MCP_AUTH_FACTORY = create_custom_mcp_auth
|
||||
```
|
||||
|
||||
### Option 3: Environment Variables
|
||||
|
||||
For containerized deployments:
|
||||
|
||||
```bash
|
||||
# Environment variables
|
||||
export MCP_AUTH_ENABLED=true
|
||||
export MCP_JWKS_URI=https://auth.company.com/.well-known/jwks.json
|
||||
export MCP_JWT_ISSUER=https://auth.company.com/
|
||||
export MCP_JWT_AUDIENCE=superset-mcp-api
|
||||
export MCP_REQUIRED_SCOPES=dashboard:read,chart:read,dataset:read
|
||||
```
|
||||
|
||||
## Identity Provider Integration
|
||||
|
||||
### Auth0
|
||||
|
||||
```python
|
||||
# Auth0 configuration
|
||||
MCP_JWKS_URI = "https://your-tenant.auth0.com/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://your-tenant.auth0.com/"
|
||||
MCP_JWT_AUDIENCE = "superset-mcp-api"
|
||||
```
|
||||
|
||||
### Okta
|
||||
|
||||
```python
|
||||
# Okta configuration
|
||||
MCP_JWKS_URI = "https://your-org.okta.com/oauth2/default/v1/keys"
|
||||
MCP_JWT_ISSUER = "https://your-org.okta.com/oauth2/default"
|
||||
MCP_JWT_AUDIENCE = "api://superset-mcp"
|
||||
```
|
||||
|
||||
### AWS Cognito
|
||||
|
||||
```python
|
||||
# Cognito configuration
|
||||
MCP_JWKS_URI = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}"
|
||||
MCP_JWT_AUDIENCE = "your-app-client-id"
|
||||
```
|
||||
|
||||
### Azure AD
|
||||
|
||||
```python
|
||||
# Azure AD configuration
|
||||
MCP_JWKS_URI = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
|
||||
MCP_JWT_ISSUER = "https://login.microsoftonline.com/{tenant}/v2.0"
|
||||
MCP_JWT_AUDIENCE = "api://superset-mcp"
|
||||
```
|
||||
|
||||
## Scope-Based Authorization
|
||||
|
||||
### Standard Scopes
|
||||
|
||||
The MCP service defines these standard scopes:
|
||||
|
||||
| Scope | Description | Required For |
|
||||
|-------|-------------|--------------|
|
||||
| `dashboard:read` | Read dashboard information | `list_dashboards`, `get_dashboard_info` |
|
||||
| `dashboard:write` | Create/modify dashboards | `generate_dashboard`, `add_chart_to_existing_dashboard` |
|
||||
| `chart:read` | Read chart information | `list_charts`, `get_chart_info`, `get_chart_data` |
|
||||
| `chart:write` | Create/modify charts | `generate_chart`, `update_chart` |
|
||||
| `dataset:read` | Read dataset information | `list_datasets`, `get_dataset_info` |
|
||||
| `instance:read` | Read instance information | `get_superset_instance_info` |
|
||||
|
||||
### Custom Scopes
|
||||
|
||||
Define custom scopes for specific use cases:
|
||||
|
||||
```python
|
||||
# Custom scope definitions
|
||||
CUSTOM_MCP_SCOPES = {
|
||||
"analytics:export": "Export analytical data",
|
||||
"reports:generate": "Generate automated reports",
|
||||
"admin:config": "Access administrative configuration"
|
||||
}
|
||||
|
||||
# Map tools to custom scopes
|
||||
def get_custom_required_scopes(tool_name: str) -> List[str]:
|
||||
scope_map = {
|
||||
"get_chart_data": ["chart:read", "analytics:export"],
|
||||
"generate_dashboard": ["dashboard:write", "reports:generate"],
|
||||
"get_superset_instance_info": ["instance:read", "admin:config"]
|
||||
}
|
||||
return scope_map.get(tool_name, [])
|
||||
|
||||
MCP_SCOPE_RESOLVER = get_custom_required_scopes
|
||||
```
|
||||
|
||||
## JWT Token Format
|
||||
|
||||
### Required Claims
|
||||
|
||||
Your JWT tokens must include these standard claims:
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://auth.company.com/", // Issuer
|
||||
"aud": "superset-mcp-api", // Audience
|
||||
"sub": "user@company.com", // Subject (username)
|
||||
"exp": 1704118800, // Expiration timestamp
|
||||
"iat": 1704115200, // Issued at timestamp
|
||||
"scope": "dashboard:read chart:read" // Space-separated scopes
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Claims
|
||||
|
||||
Additional claims for enhanced functionality:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@company.com", // User email
|
||||
"name": "John Doe", // Full name
|
||||
"groups": ["analysts", "sales_team"], // User groups
|
||||
"tenant_id": "company_123", // Multi-tenant ID
|
||||
"role": "analyst" // User role
|
||||
}
|
||||
```
|
||||
|
||||
## Client Integration
|
||||
|
||||
### API Client Usage
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Get JWT token from your identity provider
|
||||
token = get_jwt_token()
|
||||
|
||||
# Call MCP service with Bearer authentication
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
"http://localhost:5008/call_tool",
|
||||
headers=headers,
|
||||
json={
|
||||
"tool": "list_dashboards",
|
||||
"arguments": {"search": "sales"}
|
||||
}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
```
|
||||
|
||||
### Claude Desktop with Authentication
|
||||
|
||||
For Claude Desktop, the proxy script handles authentication:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# run_proxy_with_auth.sh
|
||||
|
||||
# Get token from environment or file
|
||||
if [ -f ~/.superset_mcp_token ]; then
|
||||
TOKEN=$(cat ~/.superset_mcp_token)
|
||||
else
|
||||
TOKEN=${SUPERSET_MCP_TOKEN}
|
||||
fi
|
||||
|
||||
# Export token for proxy
|
||||
export MCP_AUTH_TOKEN="$TOKEN"
|
||||
|
||||
cd /path/to/superset
|
||||
source venv/bin/activate
|
||||
exec fastmcp proxy http://localhost:5008 --auth-header "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## User Resolution
|
||||
|
||||
### Default User Resolution
|
||||
|
||||
The service maps JWT claims to Superset users:
|
||||
|
||||
```python
|
||||
def default_user_resolver(claims: Dict[str, Any]) -> User:
|
||||
"""Default user resolution from JWT claims."""
|
||||
|
||||
# Extract username from configurable claim
|
||||
username = claims.get(app.config.get("MCP_JWT_USER_CLAIM", "sub"))
|
||||
|
||||
# Find Superset user
|
||||
user = security_manager.find_user(username=username)
|
||||
|
||||
if not user:
|
||||
# Try email lookup
|
||||
email = claims.get(app.config.get("MCP_JWT_EMAIL_CLAIM", "email"))
|
||||
if email:
|
||||
user = security_manager.find_user(email=email)
|
||||
|
||||
if not user and app.config.get("MCP_FALLBACK_USER"):
|
||||
# Use fallback user for development
|
||||
user = security_manager.find_user(username=app.config["MCP_FALLBACK_USER"])
|
||||
|
||||
return user
|
||||
```
|
||||
|
||||
### Custom User Resolution
|
||||
|
||||
Implement custom user resolution logic:
|
||||
|
||||
```python
|
||||
def custom_user_resolver(claims: Dict[str, Any]) -> User:
|
||||
"""Custom user resolution for enterprise environments."""
|
||||
|
||||
# Extract custom claims
|
||||
employee_id = claims.get("employee_id")
|
||||
tenant_id = claims.get("tenant_id")
|
||||
|
||||
# Multi-tenant user lookup
|
||||
user = find_user_by_employee_id(employee_id, tenant_id)
|
||||
|
||||
if user:
|
||||
# Set additional context
|
||||
user.mcp_tenant_id = tenant_id
|
||||
user.mcp_groups = claims.get("groups", [])
|
||||
|
||||
return user
|
||||
|
||||
# Use custom resolver
|
||||
MCP_USER_RESOLVER = custom_user_resolver
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### Token Validation
|
||||
|
||||
Comprehensive JWT validation:
|
||||
|
||||
- **Signature verification**: RS256 with JWKS key rotation support
|
||||
- **Expiration checking**: Automatic token expiry validation
|
||||
- **Audience validation**: Prevents token reuse across services
|
||||
- **Issuer validation**: Ensures tokens from trusted sources only
|
||||
- **Scope validation**: Enforces tool-level permissions
|
||||
|
||||
### Request Security
|
||||
|
||||
- **HTTPS enforcement**: Production deployments should use HTTPS
|
||||
- **Rate limiting**: Configurable per-user rate limits
|
||||
- **Request logging**: All authenticated requests logged with user context
|
||||
- **Input validation**: Comprehensive request schema validation
|
||||
|
||||
### Audit Logging
|
||||
|
||||
Every tool call is logged with security context:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-25T14:30:00Z",
|
||||
"user_id": "user@company.com",
|
||||
"tool_name": "get_chart_data",
|
||||
"source": "mcp",
|
||||
"jwt_subject": "user@company.com",
|
||||
"jwt_scopes": ["chart:read", "analytics:export"],
|
||||
"tenant_id": "company_123",
|
||||
"request_id": "req_12345",
|
||||
"execution_time": 0.145,
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Authentication
|
||||
|
||||
### Generate Test Tokens
|
||||
|
||||
For development and testing:
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth.providers.bearer import RSAKeyPair
|
||||
|
||||
# Generate test keypair
|
||||
keypair = RSAKeyPair.generate()
|
||||
print("Public key:", keypair.public_key)
|
||||
|
||||
# Create test token
|
||||
token = keypair.create_token(
|
||||
subject="test@example.com",
|
||||
issuer="https://test.example.com",
|
||||
audience="superset-mcp-api",
|
||||
scopes=["dashboard:read", "chart:read", "dataset:read"],
|
||||
expires_in=3600 # 1 hour
|
||||
)
|
||||
print("Test token:", token)
|
||||
```
|
||||
|
||||
### Test Configuration
|
||||
|
||||
```python
|
||||
# Test configuration with generated keypair
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
-----END PUBLIC KEY-----"""
|
||||
MCP_JWT_ISSUER = "https://test.example.com"
|
||||
MCP_JWT_AUDIENCE = "superset-mcp-api"
|
||||
MCP_FALLBACK_USER = "admin"
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```bash
|
||||
# Test with curl
|
||||
curl -X POST http://localhost:5008/call_tool \
|
||||
-H "Authorization: Bearer $TEST_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool": "get_superset_instance_info",
|
||||
"arguments": {"include_statistics": true}
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Token Validation Errors:**
|
||||
```
|
||||
Error: Invalid JWT signature
|
||||
Solution: Verify JWKS_URI is accessible and contains correct keys
|
||||
```
|
||||
|
||||
**User Not Found:**
|
||||
```
|
||||
Error: User not found for JWT subject
|
||||
Solution: Check MCP_JWT_USER_CLAIM configuration and user exists in Superset
|
||||
```
|
||||
|
||||
**Insufficient Scopes:**
|
||||
```
|
||||
Error: Missing required scope 'chart:read'
|
||||
Solution: Update JWT token to include required scopes
|
||||
```
|
||||
|
||||
### Debug Configuration
|
||||
|
||||
Enable debug logging for authentication issues:
|
||||
|
||||
```python
|
||||
# Enhanced logging for auth debugging
|
||||
import logging
|
||||
logging.getLogger('superset.mcp_service.auth').setLevel(logging.DEBUG)
|
||||
|
||||
# Log all JWT validation steps
|
||||
MCP_AUTH_DEBUG = True
|
||||
```
|
||||
|
||||
This authentication guide provides comprehensive coverage for securing the MCP service in production environments while maintaining development flexibility.
|
||||
@@ -1,705 +0,0 @@
|
||||
---
|
||||
title: Development Guide
|
||||
sidebar_position: 2
|
||||
version: 1
|
||||
---
|
||||
|
||||
# MCP Service Development Guide
|
||||
|
||||
This guide covers the internal architecture, development workflows, and patterns for extending the Superset MCP service.
|
||||
|
||||
> 🚀 **New to MCP?** Start with the [Overview](./overview) to understand what the service does before diving into development.
|
||||
>
|
||||
> 📚 **Need API examples?** Check the [API Reference](./api-reference) to see how existing tools work.
|
||||
>
|
||||
> 🔐 **Planning production use?** Review [Authentication](./authentication) for security considerations.
|
||||
|
||||
## Internal Architecture
|
||||
|
||||
### Component Overview
|
||||
|
||||
The MCP service follows a layered architecture with clear separation of concerns:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Transport Layer"
|
||||
HTTP[HTTP Server :5008]
|
||||
FastMCP[FastMCP Protocol Handler]
|
||||
end
|
||||
|
||||
subgraph "Auth & Middleware Layer"
|
||||
AuthHook[Auth Hook Decorator]
|
||||
JWT[JWT Validator]
|
||||
RBAC[RBAC Engine]
|
||||
Audit[Audit Logger]
|
||||
end
|
||||
|
||||
subgraph "Tool Layer"
|
||||
Tools[16 MCP Tools<br/>Tool Decorated]
|
||||
Schemas[Pydantic Schemas]
|
||||
Validation[Request Validation]
|
||||
end
|
||||
|
||||
subgraph "Business Logic Layer"
|
||||
Generic[Generic Tool Abstractions]
|
||||
ModelList[ModelListTool]
|
||||
ModelGet[ModelGetInfoTool]
|
||||
ModelFilter[ModelGetAvailableFiltersTool]
|
||||
end
|
||||
|
||||
subgraph "Data Access Layer"
|
||||
DAOs[Superset DAOs]
|
||||
Commands[Superset Commands]
|
||||
Cache[Cache Manager]
|
||||
end
|
||||
|
||||
subgraph "Storage Layer"
|
||||
MetaDB[(Metadata DB)]
|
||||
DataWH[(Data Warehouse)]
|
||||
Redis[(Redis Cache)]
|
||||
end
|
||||
|
||||
HTTP --> FastMCP
|
||||
FastMCP --> AuthHook
|
||||
AuthHook --> JWT
|
||||
JWT --> RBAC
|
||||
RBAC --> Audit
|
||||
Audit --> Tools
|
||||
|
||||
Tools --> Schemas
|
||||
Schemas --> Validation
|
||||
Validation --> Generic
|
||||
|
||||
Generic --> ModelList
|
||||
Generic --> ModelGet
|
||||
Generic --> ModelFilter
|
||||
|
||||
ModelList --> DAOs
|
||||
ModelGet --> DAOs
|
||||
ModelFilter --> DAOs
|
||||
|
||||
Tools --> Commands
|
||||
Commands --> Cache
|
||||
|
||||
DAOs --> MetaDB
|
||||
Commands --> MetaDB
|
||||
Commands --> DataWH
|
||||
Cache --> Redis
|
||||
```
|
||||
|
||||
### Request Flow
|
||||
|
||||
Every MCP tool call follows this execution pattern:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as LLM Client
|
||||
participant MCP as FastMCP Server
|
||||
participant Auth as Auth Hook
|
||||
participant Tool as MCP Tool
|
||||
participant Generic as Generic Abstraction
|
||||
participant DAO as Superset DAO
|
||||
participant DB as Database
|
||||
|
||||
Client->>+MCP: tool_call(request)
|
||||
MCP->>+Auth: validate_and_authorize()
|
||||
Auth->>Auth: Validate JWT token
|
||||
Auth->>Auth: Check required scopes
|
||||
Auth->>Auth: Set Flask g.user context
|
||||
Auth->>Auth: Log audit event
|
||||
Auth->>+Tool: execute_tool(validated_request)
|
||||
|
||||
Tool->>Tool: Parse Pydantic request schema
|
||||
Tool->>+Generic: Use generic abstraction
|
||||
Generic->>+DAO: Query Superset data
|
||||
DAO->>+DB: Execute SQL
|
||||
DB-->>-DAO: Return results
|
||||
DAO-->>-Generic: Return objects
|
||||
Generic->>Generic: Apply pagination/filtering
|
||||
Generic-->>-Tool: Return formatted data
|
||||
|
||||
Tool->>Tool: Build Pydantic response schema
|
||||
Tool-->>-Auth: Return response
|
||||
Auth->>Auth: Log success audit event
|
||||
Auth-->>-MCP: Return validated response
|
||||
MCP-->>-Client: JSON response
|
||||
```
|
||||
|
||||
### Tool Registration System
|
||||
|
||||
Tools are automatically discovered and registered through the decorator pattern:
|
||||
|
||||
```python
|
||||
# superset/mcp_service/mcp_app.py
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Global MCP instance
|
||||
mcp = FastMCP("Superset MCP Service")
|
||||
|
||||
# Tools register themselves via decorators
|
||||
@mcp.tool
|
||||
@mcp_auth_hook(['chart:read'])
|
||||
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
|
||||
# Tool implementation
|
||||
pass
|
||||
|
||||
# All tool modules imported to trigger registration
|
||||
from superset.mcp_service.chart.tool import *
|
||||
from superset.mcp_service.dashboard.tool import *
|
||||
from superset.mcp_service.dataset.tool import *
|
||||
from superset.mcp_service.system.tool import *
|
||||
```
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### Tool Implementation Pattern
|
||||
|
||||
All tools follow this standardized pattern:
|
||||
|
||||
```python
|
||||
# Example: superset/mcp_service/chart/tool/get_chart_info.py
|
||||
from superset.mcp_service.auth import mcp_auth_hook
|
||||
from superset.mcp_service.mcp_app import mcp
|
||||
from superset.mcp_service.schemas.chart_schemas import (
|
||||
GetChartInfoRequest,
|
||||
GetChartInfoResponse,
|
||||
ChartError
|
||||
)
|
||||
|
||||
@mcp.tool
|
||||
@mcp_auth_hook(['chart:read'])
|
||||
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
|
||||
"""
|
||||
Get detailed information about a specific chart.
|
||||
|
||||
Supports lookup by ID or UUID with comprehensive metadata.
|
||||
"""
|
||||
try:
|
||||
# CRITICAL: Import Superset modules inside function
|
||||
from superset.daos.chart import ChartDAO
|
||||
from superset.models.slice import Slice
|
||||
|
||||
# Use generic abstraction for common operations
|
||||
from superset.mcp_service.generic_tools import ModelGetInfoTool
|
||||
|
||||
tool = ModelGetInfoTool(
|
||||
dao=ChartDAO,
|
||||
model=Slice,
|
||||
response_schema=GetChartInfoResponse,
|
||||
identifier_field_map={
|
||||
'id': 'id',
|
||||
'uuid': 'uuid'
|
||||
}
|
||||
)
|
||||
|
||||
return tool.execute(request)
|
||||
|
||||
except Exception as e:
|
||||
return ChartError(
|
||||
error=f"Failed to get chart info: {str(e)}",
|
||||
error_type="ChartInfoError"
|
||||
)
|
||||
```
|
||||
|
||||
### Schema Design Patterns
|
||||
|
||||
Pydantic schemas follow these conventions:
|
||||
|
||||
```python
|
||||
# Request Schema Pattern
|
||||
class GetChartInfoRequest(BaseModel):
|
||||
"""Request to get detailed chart information."""
|
||||
|
||||
identifier: Union[int, str] = Field(
|
||||
...,
|
||||
description="Chart ID (numeric) or UUID (string)"
|
||||
)
|
||||
|
||||
include_form_data: bool = Field(
|
||||
default=True,
|
||||
description="Whether to include chart configuration"
|
||||
)
|
||||
|
||||
use_cache: bool = Field(
|
||||
default=True,
|
||||
description="Whether to use cached data"
|
||||
)
|
||||
|
||||
# Response Schema Pattern
|
||||
class GetChartInfoResponse(BaseModel):
|
||||
"""Detailed chart information response."""
|
||||
|
||||
chart_id: int = Field(description="Chart numeric ID")
|
||||
uuid: Optional[str] = Field(description="Chart UUID")
|
||||
slice_name: str = Field(description="Chart display name")
|
||||
viz_type: str = Field(description="Visualization type")
|
||||
datasource_id: Optional[int] = Field(description="Dataset ID")
|
||||
form_data: Optional[Dict[str, Any]] = Field(description="Chart configuration")
|
||||
explore_url: Optional[str] = Field(description="Explore URL for editing")
|
||||
|
||||
# Cache status for transparency
|
||||
cache_status: Optional[CacheStatus] = Field(description="Cache hit information")
|
||||
|
||||
# Error Schema Pattern
|
||||
class ChartError(BaseModel):
|
||||
"""Chart operation error response."""
|
||||
|
||||
error: str = Field(description="Error message")
|
||||
error_type: str = Field(description="Error type identifier")
|
||||
suggestions: Optional[List[str]] = Field(description="Suggested fixes")
|
||||
```
|
||||
|
||||
### Generic Tool Abstractions
|
||||
|
||||
Common operations are abstracted into reusable classes:
|
||||
|
||||
```python
|
||||
# superset/mcp_service/generic_tools.py
|
||||
from typing import Type, Dict, Any, List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ModelListTool:
|
||||
"""Generic tool for list operations with pagination and filtering."""
|
||||
|
||||
def __init__(self,
|
||||
dao: Type,
|
||||
model: Type,
|
||||
response_schema: Type[BaseModel],
|
||||
default_columns: List[str] = None,
|
||||
searchable_columns: List[str] = None):
|
||||
self.dao = dao
|
||||
self.model = model
|
||||
self.response_schema = response_schema
|
||||
self.default_columns = default_columns or []
|
||||
self.searchable_columns = searchable_columns or []
|
||||
|
||||
def execute(self, request: BaseModel) -> BaseModel:
|
||||
"""Execute list operation with pagination and filtering."""
|
||||
|
||||
# Build query with filters
|
||||
query = self.dao.find_all()
|
||||
|
||||
# Apply search if provided
|
||||
if hasattr(request, 'search') and request.search:
|
||||
query = self._apply_search(query, request.search)
|
||||
|
||||
# Apply filters if provided
|
||||
if hasattr(request, 'filters') and request.filters:
|
||||
query = self._apply_filters(query, request.filters)
|
||||
|
||||
# Apply pagination
|
||||
total = query.count()
|
||||
|
||||
if hasattr(request, 'page') and hasattr(request, 'page_size'):
|
||||
offset = (request.page - 1) * request.page_size
|
||||
query = query.offset(offset).limit(request.page_size)
|
||||
|
||||
# Execute query and serialize
|
||||
results = query.all()
|
||||
serialized = [self._serialize_model(obj) for obj in results]
|
||||
|
||||
return self.response_schema(
|
||||
results=serialized,
|
||||
total_count=total,
|
||||
page=getattr(request, 'page', 1),
|
||||
page_size=getattr(request, 'page_size', len(serialized))
|
||||
)
|
||||
|
||||
class ModelGetInfoTool:
|
||||
"""Generic tool for getting single object by multiple identifier types."""
|
||||
|
||||
def __init__(self,
|
||||
dao: Type,
|
||||
model: Type,
|
||||
response_schema: Type[BaseModel],
|
||||
identifier_field_map: Dict[str, str]):
|
||||
self.dao = dao
|
||||
self.model = model
|
||||
self.response_schema = response_schema
|
||||
self.identifier_field_map = identifier_field_map
|
||||
|
||||
def execute(self, request: BaseModel) -> BaseModel:
|
||||
"""Execute get operation with multi-identifier support."""
|
||||
|
||||
identifier = request.identifier
|
||||
|
||||
# Determine identifier type and field
|
||||
if isinstance(identifier, int):
|
||||
field = self.identifier_field_map.get('id', 'id')
|
||||
obj = self.dao.find_by_id(identifier)
|
||||
elif isinstance(identifier, str):
|
||||
if len(identifier) == 36 and '-' in identifier: # UUID format
|
||||
field = self.identifier_field_map.get('uuid', 'uuid')
|
||||
obj = self.dao.find_by_uuid(identifier)
|
||||
else: # Assume slug
|
||||
field = self.identifier_field_map.get('slug', 'slug')
|
||||
obj = getattr(self.dao, 'find_by_slug', lambda x: None)(identifier)
|
||||
|
||||
if not obj:
|
||||
raise ValueError(f"Object not found with identifier: {identifier}")
|
||||
|
||||
# Serialize and return
|
||||
serialized = self._serialize_model(obj)
|
||||
return self.response_schema(**serialized)
|
||||
```
|
||||
|
||||
## Adding New Tools
|
||||
|
||||
### Step-by-Step Process
|
||||
|
||||
1. **Define the Domain**
|
||||
|
||||
Choose the appropriate domain folder:
|
||||
- `dashboard/` - Dashboard operations
|
||||
- `chart/` - Chart operations
|
||||
- `dataset/` - Dataset operations
|
||||
- `system/` - System-level operations
|
||||
|
||||
2. **Create Schemas**
|
||||
|
||||
```bash
|
||||
# Create schema file
|
||||
touch superset/mcp_service/schemas/my_domain_schemas.py
|
||||
```
|
||||
|
||||
```python
|
||||
# Define request/response schemas
|
||||
class MyToolRequest(BaseModel):
|
||||
param1: str = Field(description="Parameter description")
|
||||
param2: Optional[int] = Field(default=None, description="Optional parameter")
|
||||
|
||||
class MyToolResponse(BaseModel):
|
||||
result: str = Field(description="Result description")
|
||||
metadata: Dict[str, Any] = Field(description="Additional metadata")
|
||||
```
|
||||
|
||||
3. **Implement the Tool**
|
||||
|
||||
```bash
|
||||
# Create tool file
|
||||
touch superset/mcp_service/my_domain/tool/my_tool.py
|
||||
```
|
||||
|
||||
```python
|
||||
@mcp.tool
|
||||
@mcp_auth_hook(['required:scope'])
|
||||
def my_tool(request: MyToolRequest) -> MyToolResponse:
|
||||
"""Tool description for LLM."""
|
||||
|
||||
# Import Superset modules inside function
|
||||
from superset.daos.my_dao import MyDAO
|
||||
|
||||
# Implement business logic
|
||||
result = MyDAO.do_something(request.param1)
|
||||
|
||||
return MyToolResponse(
|
||||
result=result,
|
||||
metadata={"processed_at": datetime.utcnow()}
|
||||
)
|
||||
```
|
||||
|
||||
4. **Register the Tool**
|
||||
|
||||
```python
|
||||
# Add to superset/mcp_service/my_domain/tool/__init__.py
|
||||
from .my_tool import my_tool
|
||||
|
||||
__all__ = ['my_tool']
|
||||
```
|
||||
|
||||
```python
|
||||
# Import in superset/mcp_service/mcp_app.py
|
||||
from superset.mcp_service.my_domain.tool import *
|
||||
```
|
||||
|
||||
5. **Add Tests**
|
||||
|
||||
```bash
|
||||
# Create test file
|
||||
touch tests/unit_tests/mcp_service/test_my_tool.py
|
||||
```
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from superset.mcp_service.my_domain.tool.my_tool import my_tool
|
||||
from superset.mcp_service.schemas.my_domain_schemas import MyToolRequest
|
||||
|
||||
class TestMyTool:
|
||||
def test_my_tool_success(self):
|
||||
request = MyToolRequest(param1="test")
|
||||
response = my_tool(request)
|
||||
assert response.result == "expected_result"
|
||||
```
|
||||
|
||||
### Tool Best Practices
|
||||
|
||||
1. **Import Inside Functions**
|
||||
```python
|
||||
# ❌ DON'T: Import at module level
|
||||
from superset.daos.chart import ChartDAO
|
||||
|
||||
@mcp.tool
|
||||
def my_tool():
|
||||
# Tool implementation
|
||||
pass
|
||||
|
||||
# ✅ DO: Import inside function
|
||||
@mcp.tool
|
||||
def my_tool():
|
||||
from superset.daos.chart import ChartDAO
|
||||
# Tool implementation
|
||||
pass
|
||||
```
|
||||
|
||||
2. **Use Generic Abstractions**
|
||||
```python
|
||||
# ✅ Leverage existing patterns
|
||||
@mcp.tool
|
||||
def list_my_objects(request):
|
||||
from superset.mcp_service.generic_tools import ModelListTool
|
||||
|
||||
tool = ModelListTool(
|
||||
dao=MyDAO,
|
||||
model=MyModel,
|
||||
response_schema=ListMyObjectsResponse
|
||||
)
|
||||
return tool.execute(request)
|
||||
```
|
||||
|
||||
3. **Comprehensive Error Handling**
|
||||
```python
|
||||
@mcp.tool
|
||||
def my_tool(request):
|
||||
try:
|
||||
# Tool implementation
|
||||
return success_response
|
||||
except PermissionError as e:
|
||||
return MyToolError(
|
||||
error="Permission denied",
|
||||
error_type="PermissionError",
|
||||
suggestions=["Check user permissions"]
|
||||
)
|
||||
except Exception as e:
|
||||
return MyToolError(
|
||||
error=f"Unexpected error: {str(e)}",
|
||||
error_type="InternalError"
|
||||
)
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Test Structure
|
||||
|
||||
```python
|
||||
# tests/unit_tests/mcp_service/test_chart_tools.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from superset.mcp_service.chart.tool.get_chart_info import get_chart_info
|
||||
from superset.mcp_service.schemas.chart_schemas import GetChartInfoRequest
|
||||
|
||||
class TestGetChartInfo:
|
||||
"""Test suite for get_chart_info tool."""
|
||||
|
||||
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
|
||||
def test_get_chart_info_by_id_success(self, mock_dao):
|
||||
"""Test successful chart lookup by ID."""
|
||||
|
||||
# Setup mock
|
||||
mock_chart = Mock()
|
||||
mock_chart.id = 1
|
||||
mock_chart.slice_name = "Test Chart"
|
||||
mock_chart.viz_type = "line"
|
||||
mock_dao.find_by_id.return_value = mock_chart
|
||||
|
||||
# Execute
|
||||
request = GetChartInfoRequest(identifier=1)
|
||||
response = get_chart_info(request)
|
||||
|
||||
# Verify
|
||||
assert response.chart_id == 1
|
||||
assert response.slice_name == "Test Chart"
|
||||
mock_dao.find_by_id.assert_called_once_with(1)
|
||||
|
||||
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
|
||||
def test_get_chart_info_not_found(self, mock_dao):
|
||||
"""Test chart not found scenario."""
|
||||
|
||||
# Setup mock
|
||||
mock_dao.find_by_id.return_value = None
|
||||
|
||||
# Execute
|
||||
request = GetChartInfoRequest(identifier=999)
|
||||
response = get_chart_info(request)
|
||||
|
||||
# Verify error response
|
||||
assert hasattr(response, 'error')
|
||||
assert "not found" in response.error.lower()
|
||||
```
|
||||
|
||||
### Integration Test Patterns
|
||||
|
||||
```python
|
||||
# tests/integration_tests/mcp_service/test_chart_integration.py
|
||||
import pytest
|
||||
from superset.app import create_app
|
||||
from superset.mcp_service.mcp_app import mcp
|
||||
from tests.integration_tests.base_tests import SupersetTestCase
|
||||
|
||||
class TestChartIntegration(SupersetTestCase):
|
||||
"""Integration tests for chart tools."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app = create_app()
|
||||
self.app_context = self.app.app_context()
|
||||
self.app_context.push()
|
||||
|
||||
def tearDown(self):
|
||||
self.app_context.pop()
|
||||
super().tearDown()
|
||||
|
||||
def test_chart_workflow_integration(self):
|
||||
"""Test complete chart workflow."""
|
||||
|
||||
# Create chart
|
||||
create_request = {
|
||||
"dataset_id": "1",
|
||||
"config": {
|
||||
"chart_type": "table",
|
||||
"columns": [{"name": "region"}]
|
||||
}
|
||||
}
|
||||
|
||||
create_response = mcp.call_tool("generate_chart", create_request)
|
||||
chart_id = create_response["chart_id"]
|
||||
|
||||
# Get chart info
|
||||
info_request = {"identifier": chart_id}
|
||||
info_response = mcp.call_tool("get_chart_info", info_request)
|
||||
|
||||
assert info_response["chart_id"] == chart_id
|
||||
assert info_response["viz_type"] == "table"
|
||||
|
||||
# Get chart data
|
||||
data_request = {"identifier": chart_id, "limit": 10}
|
||||
data_response = mcp.call_tool("get_chart_data", data_request)
|
||||
|
||||
assert "data" in data_response
|
||||
assert len(data_response["data"]) <= 10
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
The MCP service leverages Superset's existing cache layers:
|
||||
|
||||
```python
|
||||
# Cache control in tools
|
||||
@mcp.tool
|
||||
def get_chart_data(request: GetChartDataRequest):
|
||||
"""Tool with cache control."""
|
||||
|
||||
cache_config = {
|
||||
'use_cache': request.use_cache,
|
||||
'force_refresh': request.force_refresh,
|
||||
'cache_timeout': request.cache_timeout
|
||||
}
|
||||
|
||||
# Use Superset's cache infrastructure
|
||||
result = execute_with_cache(query, cache_config)
|
||||
|
||||
return ChartDataResponse(
|
||||
data=result.data,
|
||||
cache_status=result.cache_status
|
||||
)
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```python
|
||||
# Efficient pagination
|
||||
def list_objects(query, page, page_size):
|
||||
"""Optimized pagination pattern."""
|
||||
|
||||
# Count query optimization
|
||||
total = query.count()
|
||||
|
||||
# Limit columns for list operations
|
||||
query = query.options(load_only('id', 'name', 'created_on'))
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * page_size
|
||||
results = query.offset(offset).limit(page_size).all()
|
||||
|
||||
return results, total
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```python
|
||||
# JWT validation and user context
|
||||
@mcp_auth_hook(['chart:read'])
|
||||
def secure_tool(request):
|
||||
"""Tool with proper security context."""
|
||||
|
||||
# g.user is set by auth hook
|
||||
user_id = g.user.id
|
||||
|
||||
# Apply user-specific filtering
|
||||
query = ChartDAO.find_all().filter(
|
||||
Chart.owners.contains(g.user)
|
||||
)
|
||||
|
||||
return execute_query(query)
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```python
|
||||
# Comprehensive request validation
|
||||
class CreateChartRequest(BaseModel):
|
||||
"""Validated chart creation request."""
|
||||
|
||||
dataset_id: Union[int, str] = Field(
|
||||
...,
|
||||
description="Dataset ID or UUID"
|
||||
)
|
||||
|
||||
config: ChartConfig = Field(
|
||||
...,
|
||||
description="Chart configuration"
|
||||
)
|
||||
|
||||
@validator('dataset_id')
|
||||
def validate_dataset_id(cls, v):
|
||||
"""Validate dataset exists and user has access."""
|
||||
# Validation logic
|
||||
return v
|
||||
|
||||
@validator('config')
|
||||
def validate_chart_config(cls, v):
|
||||
"""Validate chart configuration."""
|
||||
# Configuration validation
|
||||
return v
|
||||
```
|
||||
|
||||
This development guide provides comprehensive coverage of the MCP service's internal architecture and development patterns, enabling team members to effectively extend and maintain the system.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
### 📚 **Ready to Use Your New Tools?**
|
||||
Test your implementations with examples from the [API Reference](./api-reference).
|
||||
|
||||
### 🔐 **Securing Your Extensions?**
|
||||
Add authentication to your tools using the [Authentication Guide](./authentication).
|
||||
|
||||
### 🏗️ **Understanding the Big Picture?**
|
||||
See the complete system design in the [Architecture Overview](./architecture).
|
||||
|
||||
### 🏢 **Building Enterprise Features?**
|
||||
Explore advanced patterns in the [Preset Integration Guide](./preset-integration).
|
||||
|
||||
> 📖 **Back to Documentation Index**: [MCP Service](./intro)
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
title: MCP Service
|
||||
sidebar_position: 1
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Superset MCP Service
|
||||
|
||||
The Superset Model Context Protocol (MCP) service provides programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built for LLM agents and automation tools.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is an open standard that allows AI assistants to securely connect to data sources and tools. Superset's MCP service exposes **16 production-ready tools** that enable:
|
||||
|
||||
- 📊 **Data Exploration**: List and query dashboards, charts, and datasets
|
||||
- 🔧 **Chart Creation**: Generate visualizations programmatically
|
||||
- 📈 **Data Export**: Extract data in multiple formats (JSON, CSV, Excel)
|
||||
- 🔗 **Navigation**: Generate explore links and SQL Lab sessions
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
:::note
|
||||
The MCP service is included with Superset development setup. FastMCP dependencies are installed automatically with `make install`.
|
||||
:::
|
||||
|
||||
```bash
|
||||
# MCP service is included with Superset development setup
|
||||
git clone https://github.com/apache/superset.git
|
||||
cd superset
|
||||
make venv && source venv/bin/activate
|
||||
make install
|
||||
|
||||
# Start Superset
|
||||
superset run -p 8088 --with-threads --reload --debugger
|
||||
|
||||
# Start MCP service (separate terminal)
|
||||
source venv/bin/activate
|
||||
superset mcp run --port 5008 --debug
|
||||
```
|
||||
|
||||
### Claude Desktop Integration
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Superset MCP": {
|
||||
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 🔧 **16 Production Tools**
|
||||
| Category | Tools | Purpose |
|
||||
|----------|-------|---------|
|
||||
| **Dashboard** (5) | List, get info, create, add charts | Dashboard management |
|
||||
| **Chart** (8) | Full CRUD, data export, previews | Chart operations |
|
||||
| **Dataset** (3) | List, get info, discover filters | Dataset exploration |
|
||||
| **System** (2) | Instance info, explore links | System integration |
|
||||
| **SQL Lab** (1) | Pre-configured sessions | SQL development |
|
||||
|
||||
### 🔐 **Enterprise Security**
|
||||
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
|
||||
- **RBAC Integration**: Scope-based permissions with Superset's security model
|
||||
- **Audit Logging**: Comprehensive MCP context tracking
|
||||
|
||||
### 📊 **Advanced Capabilities**
|
||||
- **Multi-format Export**: JSON, CSV, Excel data export
|
||||
- **Chart Previews**: Screenshots, ASCII art, table representations
|
||||
- **Cache Control**: Leverage Superset's existing cache infrastructure
|
||||
- **Request Schemas**: Eliminates LLM parameter validation issues
|
||||
|
||||
## Example Usage
|
||||
|
||||
```python
|
||||
# List dashboards
|
||||
dashboards = client.call_tool("list_dashboards", {
|
||||
"search": "sales",
|
||||
"page_size": 10
|
||||
})
|
||||
|
||||
# Create a chart
|
||||
chart = client.call_tool("generate_chart", {
|
||||
"dataset_id": "1",
|
||||
"config": {
|
||||
"chart_type": "line",
|
||||
"x": {"name": "date"},
|
||||
"y": [{"name": "revenue", "aggregate": "SUM"}]
|
||||
}
|
||||
})
|
||||
|
||||
# Export chart data
|
||||
data = client.call_tool("get_chart_data", {
|
||||
"identifier": chart["chart_id"],
|
||||
"format": "json",
|
||||
"limit": 1000
|
||||
})
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Phase 1 Complete** - Core functionality stable, authentication production-ready, comprehensive testing coverage.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
### Getting Started
|
||||
- **[Overview](./overview)** - Features, use cases, and examples
|
||||
- **[API Reference](./api-reference)** - Complete tool documentation
|
||||
|
||||
### Development
|
||||
- **[Development Guide](./development)** - Internal architecture and adding tools
|
||||
- **[Architecture](./architecture)** - System design and patterns
|
||||
|
||||
### Production
|
||||
- **[Authentication](./authentication)** - JWT setup and security
|
||||
- **[Preset Integration](./preset-integration)** - Enterprise features
|
||||
|
||||
> 🚀 **Ready to start?** Continue with the [Overview](./overview) for detailed examples and use cases.
|
||||
@@ -1,196 +0,0 @@
|
||||
---
|
||||
title: MCP Service Overview
|
||||
sidebar_position: 1
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Superset MCP Service
|
||||
|
||||
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP, it's designed for LLM agents and automation tools.
|
||||
|
||||
**Status:** ✅ Phase 1 Complete. Core functionality stable, authentication production-ready, comprehensive testing coverage.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is an open standard for connecting AI assistants to data sources and tools. Superset's MCP service exposes 16 tools that allow LLM agents to:
|
||||
|
||||
- **Explore data**: List and query dashboards, charts, and datasets
|
||||
- **Create visualizations**: Generate charts and dashboards programmatically
|
||||
- **Export data**: Extract chart data in multiple formats
|
||||
- **Navigate interfaces**: Generate explore links and SQL Lab sessions
|
||||
|
||||
## Key Features
|
||||
|
||||
### 🔧 **16 Production-Ready Tools**
|
||||
- **Dashboard Tools (5)**: List, get info, create dashboards, add charts
|
||||
- **Chart Tools (8)**: Full CRUD operations, data export, screenshot previews
|
||||
- **Dataset Tools (3)**: List, get info, discover filterable columns
|
||||
- **System Tools (2)**: Instance info, explore link generation
|
||||
- **SQL Lab Tools (1)**: Pre-configured SQL sessions
|
||||
|
||||
### 🔐 **Enterprise Authentication**
|
||||
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
|
||||
- **RBAC Integration**: Scope-based permissions with Superset's security model
|
||||
- **Audit Logging**: Comprehensive MCP context tracking with impersonation support
|
||||
|
||||
### 📊 **Advanced Capabilities**
|
||||
- **Multi-format Export**: JSON, CSV, Excel data export
|
||||
- **Chart Previews**: Screenshots, ASCII art, and table representations
|
||||
- **Cache Control**: Comprehensive control over Superset's cache layers
|
||||
- **Request Schema Pattern**: Eliminates LLM parameter validation issues
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Client Layer"
|
||||
LLM[LLM/Agent Client]
|
||||
Claude[Claude Desktop]
|
||||
SDK[Custom SDK]
|
||||
end
|
||||
|
||||
subgraph "MCP Service Layer"
|
||||
FastMCP[FastMCP Server<br/>Port 5008]
|
||||
Auth[JWT Auth Hook]
|
||||
Tools[16 MCP Tools]
|
||||
end
|
||||
|
||||
subgraph "Superset Integration"
|
||||
DAOs[Superset DAOs]
|
||||
Commands[Superset Commands]
|
||||
Cache[Cache Layer]
|
||||
end
|
||||
|
||||
subgraph "Data Layer"
|
||||
DB[(Superset Database)]
|
||||
DataWarehouse[(Data Warehouse)]
|
||||
end
|
||||
|
||||
LLM --> FastMCP
|
||||
Claude --> FastMCP
|
||||
SDK --> FastMCP
|
||||
|
||||
FastMCP --> Auth
|
||||
Auth --> Tools
|
||||
|
||||
Tools --> DAOs
|
||||
Tools --> Commands
|
||||
Tools --> Cache
|
||||
|
||||
DAOs --> DB
|
||||
Commands --> DB
|
||||
Commands --> DataWarehouse
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Quick Setup
|
||||
|
||||
```bash
|
||||
# Clone and install Superset
|
||||
git clone https://github.com/apache/superset.git
|
||||
cd superset
|
||||
make venv && source venv/bin/activate
|
||||
make install
|
||||
|
||||
# Start Superset
|
||||
superset run -p 8088 --with-threads --reload --debugger
|
||||
|
||||
# Start MCP service (in separate terminal)
|
||||
source venv/bin/activate
|
||||
superset mcp run --port 5008 --debug
|
||||
```
|
||||
|
||||
### Connect to Claude Desktop
|
||||
|
||||
:::note
|
||||
The MCP service runs on HTTP and requires a proxy for Claude Desktop integration.
|
||||
:::
|
||||
|
||||
```bash
|
||||
# Install FastMCP proxy
|
||||
pip install fastmcp
|
||||
```
|
||||
|
||||
Configure Claude Desktop (`~/.config/Claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Superset MCP": {
|
||||
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Data Exploration
|
||||
- "List all dashboards related to sales"
|
||||
- "Show me the charts in the Q4 Performance dashboard"
|
||||
- "What datasets are available for customer analysis?"
|
||||
|
||||
### Chart Creation
|
||||
- "Create a line chart showing revenue trends by month"
|
||||
- "Generate a table showing top 10 products by sales"
|
||||
- "Build a bar chart comparing regional performance"
|
||||
|
||||
### Data Export
|
||||
- "Export the sales data from this chart as CSV"
|
||||
- "Get the underlying data for this dashboard as JSON"
|
||||
- "Show me a preview of this chart as ASCII art"
|
||||
|
||||
### Dashboard Management
|
||||
- "Create a new dashboard with these 4 charts"
|
||||
- "Add this revenue chart to the executive dashboard"
|
||||
- "Generate an explore link for this chart configuration"
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```python
|
||||
# List available dashboards
|
||||
dashboards = client.call_tool("list_dashboards", {
|
||||
"search": "sales",
|
||||
"page_size": 10
|
||||
})
|
||||
|
||||
# Get detailed dashboard info
|
||||
dashboard = client.call_tool("get_dashboard_info", {
|
||||
"identifier": dashboards["dashboards"][0]["id"]
|
||||
})
|
||||
|
||||
# Create a new chart
|
||||
chart = client.call_tool("generate_chart", {
|
||||
"dataset_id": "1",
|
||||
"config": {
|
||||
"chart_type": "line",
|
||||
"x": {"name": "date"},
|
||||
"y": [{"name": "revenue", "aggregate": "SUM"}]
|
||||
}
|
||||
})
|
||||
|
||||
# Export chart data
|
||||
data = client.call_tool("get_chart_data", {
|
||||
"identifier": chart["chart_id"],
|
||||
"format": "json",
|
||||
"limit": 1000
|
||||
})
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Ready to Use MCP?
|
||||
- **[📚 API Reference](./api-reference)** - Try all 16 tools with request/response examples
|
||||
- **[🔐 Authentication](./authentication)** - Set up JWT security for production use
|
||||
|
||||
### Want to Extend MCP?
|
||||
- **[🔧 Development Guide](./development)** - Learn internal architecture and add new tools
|
||||
- **[🏗️ Architecture](./architecture)** - Understand system design and deployment patterns
|
||||
|
||||
### Enterprise Deployment?
|
||||
- **[🏢 Preset Integration](./preset-integration)** - RBAC extensions and OIDC integration for enterprise
|
||||
|
||||
> 💡 **Getting started?** Return to the [MCP Service intro](./intro) for a complete overview.
|
||||
@@ -1,483 +0,0 @@
|
||||
---
|
||||
title: Preset.io Integration
|
||||
sidebar_position: 6
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Preset.io Integration Guide
|
||||
|
||||
This document outlines integration points for the Preset.io team to extend the Superset MCP service with enterprise features, RBAC customizations, and OIDC integration.
|
||||
|
||||
## RBAC Extension Points
|
||||
|
||||
### Custom Authorization Factory
|
||||
|
||||
The MCP service supports custom authorization logic through the factory pattern:
|
||||
|
||||
```python
|
||||
# In preset_config.py or superset_config.py
|
||||
def create_preset_mcp_auth(app):
|
||||
"""Custom auth factory for Preset.io environments."""
|
||||
from superset.mcp_service.auth import create_auth_provider
|
||||
from preset.auth.mcp import PresetMCPAuthProvider
|
||||
|
||||
return PresetMCPAuthProvider(
|
||||
jwks_uri=app.config["PRESET_JWKS_URI"],
|
||||
issuer=app.config["PRESET_JWT_ISSUER"],
|
||||
audience=app.config["PRESET_JWT_AUDIENCE"],
|
||||
tenant_resolver=preset_tenant_resolver,
|
||||
rbac_manager=app.security_manager,
|
||||
)
|
||||
|
||||
MCP_AUTH_FACTORY = create_preset_mcp_auth
|
||||
```
|
||||
|
||||
### Multi-Tenant RBAC
|
||||
|
||||
Extend the base auth hook for tenant-aware permissions:
|
||||
|
||||
```python
|
||||
# preset/mcp/auth.py
|
||||
from superset.mcp_service.auth import mcp_auth_hook
|
||||
from functools import wraps
|
||||
|
||||
def preset_tenant_auth_hook(required_permissions=None):
|
||||
"""Preset-specific auth hook with tenant isolation."""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
@mcp_auth_hook(required_permissions)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Extract tenant from JWT claims
|
||||
tenant_id = g.user.tenant_id if hasattr(g.user, 'tenant_id') else None
|
||||
|
||||
# Inject tenant context
|
||||
g.mcp_tenant_id = tenant_id
|
||||
g.mcp_tenant_context = get_tenant_context(tenant_id)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
```
|
||||
|
||||
### Custom Permission Scopes
|
||||
|
||||
Define Preset-specific permission scopes:
|
||||
|
||||
```python
|
||||
# preset/mcp/permissions.py
|
||||
PRESET_MCP_SCOPES = {
|
||||
# Tenant-level permissions
|
||||
"tenant:admin": "Full tenant administration",
|
||||
"tenant:read": "Read tenant resources",
|
||||
|
||||
# Workspace-level permissions
|
||||
"workspace:admin": "Full workspace administration",
|
||||
"workspace:read": "Read workspace resources",
|
||||
|
||||
# Enhanced dashboard permissions
|
||||
"dashboard:publish": "Publish dashboards to marketplace",
|
||||
"dashboard:embed": "Generate embed tokens",
|
||||
|
||||
# Enhanced chart permissions
|
||||
"chart:export": "Export chart data and configs",
|
||||
"chart:alerts": "Manage chart alerts and notifications",
|
||||
|
||||
# Dataset permissions with row-level security
|
||||
"dataset:rls": "Apply row-level security filters",
|
||||
"dataset:pii": "Access PII-flagged columns",
|
||||
}
|
||||
|
||||
def get_preset_required_scopes(tool_name: str, context: dict = None) -> List[str]:
|
||||
"""Map tool calls to Preset-specific permission requirements."""
|
||||
base_scopes = get_base_required_scopes(tool_name)
|
||||
|
||||
# Add tenant-aware scopes
|
||||
if context and context.get('tenant_id'):
|
||||
base_scopes.append(f"tenant:{context['tenant_id']}")
|
||||
|
||||
# Add workspace-aware scopes
|
||||
if context and context.get('workspace_id'):
|
||||
base_scopes.append(f"workspace:{context['workspace_id']}")
|
||||
|
||||
return base_scopes
|
||||
```
|
||||
|
||||
### Row-Level Security Integration
|
||||
|
||||
Extend data access tools with RLS:
|
||||
|
||||
```python
|
||||
# preset/mcp/rls.py
|
||||
def apply_preset_rls_filters(query_context: dict, user_context: dict) -> dict:
|
||||
"""Apply Preset row-level security filters to query context."""
|
||||
|
||||
# Get user's RLS rules from Preset metadata
|
||||
rls_rules = get_user_rls_rules(
|
||||
user_id=user_context['user_id'],
|
||||
tenant_id=user_context['tenant_id'],
|
||||
workspace_id=user_context.get('workspace_id')
|
||||
)
|
||||
|
||||
# Apply RLS filters to query
|
||||
for rule in rls_rules:
|
||||
if rule.applies_to_dataset(query_context['datasource']['id']):
|
||||
query_context = rule.apply_filter(query_context)
|
||||
|
||||
return query_context
|
||||
|
||||
# Usage in custom tools
|
||||
@mcp.tool
|
||||
@preset_tenant_auth_hook(['dataset:read', 'dataset:rls'])
|
||||
def preset_get_chart_data(request: GetChartDataRequest) -> ChartDataResponse:
|
||||
"""Get chart data with Preset RLS applied."""
|
||||
|
||||
# Apply RLS before executing query
|
||||
query_context = build_query_context(request)
|
||||
query_context = apply_preset_rls_filters(
|
||||
query_context,
|
||||
{'user_id': g.user.id, 'tenant_id': g.mcp_tenant_id}
|
||||
)
|
||||
|
||||
return execute_chart_data_query(query_context)
|
||||
```
|
||||
|
||||
## OIDC Integration Points
|
||||
|
||||
### Preset OIDC Provider
|
||||
|
||||
Custom OIDC integration for Preset environments:
|
||||
|
||||
```python
|
||||
# preset/mcp/oidc.py
|
||||
from superset.mcp_service.auth.providers.bearer import BearerAuthProvider
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
|
||||
class PresetOIDCAuthProvider(BearerAuthProvider):
|
||||
"""OIDC-specific auth provider for Preset.io."""
|
||||
|
||||
def __init__(self,
|
||||
oidc_discovery_url: str,
|
||||
client_id: str,
|
||||
client_secret: str = None,
|
||||
**kwargs):
|
||||
|
||||
# Discover OIDC endpoints
|
||||
self.discovery_doc = self._fetch_discovery_document(oidc_discovery_url)
|
||||
|
||||
super().__init__(
|
||||
jwks_uri=self.discovery_doc['jwks_uri'],
|
||||
issuer=self.discovery_doc['issuer'],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
def _fetch_discovery_document(self, discovery_url: str) -> Dict[str, Any]:
|
||||
"""Fetch OIDC discovery document."""
|
||||
response = requests.get(discovery_url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def validate_token(self, token: str) -> Dict[str, Any]:
|
||||
"""Validate JWT token with OIDC-specific claims."""
|
||||
claims = super().validate_token(token)
|
||||
|
||||
# Validate OIDC-specific claims
|
||||
if claims.get('aud') != self.client_id:
|
||||
raise ValueError("Invalid audience claim")
|
||||
|
||||
# Extract Preset-specific claims
|
||||
claims['preset_tenant_id'] = claims.get('tenant_id')
|
||||
claims['preset_workspace_id'] = claims.get('workspace_id')
|
||||
claims['preset_roles'] = claims.get('roles', [])
|
||||
|
||||
return claims
|
||||
|
||||
def resolve_user(self, claims: Dict[str, Any]) -> Any:
|
||||
"""Resolve Superset user from OIDC claims."""
|
||||
from preset.auth.user_resolver import resolve_preset_user
|
||||
|
||||
return resolve_preset_user(
|
||||
subject=claims['sub'],
|
||||
email=claims.get('email'),
|
||||
tenant_id=claims.get('preset_tenant_id'),
|
||||
roles=claims.get('preset_roles', [])
|
||||
)
|
||||
```
|
||||
|
||||
### Configuration for OIDC
|
||||
|
||||
```python
|
||||
# In preset_config.py
|
||||
def create_preset_oidc_auth(app):
|
||||
"""Factory for Preset OIDC authentication."""
|
||||
from preset.mcp.oidc import PresetOIDCAuthProvider
|
||||
|
||||
return PresetOIDCAuthProvider(
|
||||
oidc_discovery_url=app.config["PRESET_OIDC_DISCOVERY_URL"],
|
||||
client_id=app.config["PRESET_OIDC_CLIENT_ID"],
|
||||
client_secret=app.config["PRESET_OIDC_CLIENT_SECRET"],
|
||||
audience=app.config["PRESET_MCP_AUDIENCE"],
|
||||
required_scopes=app.config.get("PRESET_MCP_REQUIRED_SCOPES", [])
|
||||
)
|
||||
|
||||
# MCP Configuration
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_AUTH_FACTORY = create_preset_oidc_auth
|
||||
|
||||
# OIDC Configuration
|
||||
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
|
||||
PRESET_OIDC_CLIENT_ID = "preset-mcp-service"
|
||||
PRESET_OIDC_CLIENT_SECRET = os.environ.get("PRESET_OIDC_CLIENT_SECRET")
|
||||
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
|
||||
PRESET_MCP_REQUIRED_SCOPES = [
|
||||
"openid", "profile", "email",
|
||||
"superset:read", "superset:write"
|
||||
]
|
||||
```
|
||||
|
||||
## Preset-Specific Tools
|
||||
|
||||
### Tenant Management Tools
|
||||
|
||||
```python
|
||||
# preset/mcp/tools/tenant.py
|
||||
@mcp.tool
|
||||
@preset_tenant_auth_hook(['tenant:read'])
|
||||
def get_tenant_info(request: GetTenantInfoRequest) -> TenantInfoResponse:
|
||||
"""Get Preset tenant information and quotas."""
|
||||
|
||||
tenant_id = g.mcp_tenant_id
|
||||
tenant = get_tenant_by_id(tenant_id)
|
||||
|
||||
return TenantInfoResponse(
|
||||
tenant_id=tenant.id,
|
||||
name=tenant.name,
|
||||
plan=tenant.plan,
|
||||
quotas=tenant.quotas,
|
||||
usage=get_tenant_usage(tenant_id),
|
||||
workspaces=list_tenant_workspaces(tenant_id)
|
||||
)
|
||||
|
||||
@mcp.tool
|
||||
@preset_tenant_auth_hook(['workspace:read'])
|
||||
def list_workspace_assets(request: ListWorkspaceAssetsRequest) -> ListWorkspaceAssetsResponse:
|
||||
"""List all assets in a Preset workspace."""
|
||||
|
||||
workspace_id = request.workspace_id
|
||||
tenant_id = g.mcp_tenant_id
|
||||
|
||||
# Validate workspace belongs to tenant
|
||||
validate_workspace_access(workspace_id, tenant_id)
|
||||
|
||||
assets = {
|
||||
'dashboards': list_workspace_dashboards(workspace_id),
|
||||
'charts': list_workspace_charts(workspace_id),
|
||||
'datasets': list_workspace_datasets(workspace_id)
|
||||
}
|
||||
|
||||
return ListWorkspaceAssetsResponse(
|
||||
workspace_id=workspace_id,
|
||||
assets=assets,
|
||||
total_count=sum(len(v) for v in assets.values())
|
||||
)
|
||||
```
|
||||
|
||||
### Embed Token Generation
|
||||
|
||||
```python
|
||||
# preset/mcp/tools/embed.py
|
||||
@mcp.tool
|
||||
@preset_tenant_auth_hook(['dashboard:embed'])
|
||||
def generate_embed_token(request: GenerateEmbedTokenRequest) -> EmbedTokenResponse:
|
||||
"""Generate secure embed token for dashboard/chart."""
|
||||
|
||||
# Validate resource access
|
||||
resource = validate_embed_resource_access(
|
||||
resource_type=request.resource_type,
|
||||
resource_id=request.resource_id,
|
||||
tenant_id=g.mcp_tenant_id
|
||||
)
|
||||
|
||||
# Generate signed embed token
|
||||
embed_token = create_embed_token(
|
||||
resource=resource,
|
||||
user_id=g.user.id,
|
||||
tenant_id=g.mcp_tenant_id,
|
||||
permissions=request.permissions,
|
||||
expiry=request.expiry_hours
|
||||
)
|
||||
|
||||
return EmbedTokenResponse(
|
||||
embed_token=embed_token,
|
||||
embed_url=f"{get_preset_base_url()}/embed/{embed_token}",
|
||||
expires_at=embed_token.expires_at
|
||||
)
|
||||
```
|
||||
|
||||
## Audit and Compliance Extensions
|
||||
|
||||
### Enhanced Audit Logging
|
||||
|
||||
```python
|
||||
# preset/mcp/audit.py
|
||||
from superset.mcp_service.auth import get_audit_context
|
||||
|
||||
def create_preset_audit_context(user_context: dict, tool_name: str,
|
||||
request_data: dict) -> dict:
|
||||
"""Create Preset-specific audit context."""
|
||||
|
||||
base_context = get_audit_context(user_context, tool_name, request_data)
|
||||
|
||||
# Add Preset-specific fields
|
||||
preset_context = {
|
||||
**base_context,
|
||||
'tenant_id': user_context.get('tenant_id'),
|
||||
'workspace_id': user_context.get('workspace_id'),
|
||||
'preset_user_role': user_context.get('preset_role'),
|
||||
'data_classification': classify_request_data(request_data),
|
||||
'compliance_flags': get_compliance_flags(tool_name, request_data)
|
||||
}
|
||||
|
||||
return preset_context
|
||||
|
||||
def log_preset_mcp_access(audit_context: dict):
|
||||
"""Log MCP access to Preset audit systems."""
|
||||
|
||||
# Log to Superset's audit system
|
||||
log_superset_audit_event(audit_context)
|
||||
|
||||
# Log to Preset's compliance system
|
||||
log_preset_compliance_event(audit_context)
|
||||
|
||||
# Log to external SIEM if configured
|
||||
if app.config.get('PRESET_SIEM_ENABLED'):
|
||||
log_to_siem(audit_context)
|
||||
```
|
||||
|
||||
### Data Classification
|
||||
|
||||
```python
|
||||
# preset/mcp/classification.py
|
||||
def classify_request_data(request_data: dict) -> dict:
|
||||
"""Classify data sensitivity in MCP requests."""
|
||||
|
||||
classification = {
|
||||
'contains_pii': False,
|
||||
'data_level': 'public',
|
||||
'retention_policy': 'standard'
|
||||
}
|
||||
|
||||
# Check for PII in request
|
||||
if contains_pii_fields(request_data):
|
||||
classification['contains_pii'] = True
|
||||
classification['data_level'] = 'restricted'
|
||||
classification['retention_policy'] = 'pii_compliant'
|
||||
|
||||
# Check for sensitive datasets
|
||||
if references_sensitive_datasets(request_data):
|
||||
classification['data_level'] = 'confidential'
|
||||
|
||||
return classification
|
||||
```
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
### Multi-Region Deployment
|
||||
|
||||
```python
|
||||
# preset/mcp/deployment.py
|
||||
def get_region_specific_config():
|
||||
"""Get region-specific MCP configuration."""
|
||||
|
||||
region = os.environ.get('PRESET_REGION', 'us-east-1')
|
||||
|
||||
config_map = {
|
||||
'us-east-1': {
|
||||
'jwks_uri': 'https://auth-us.preset.io/.well-known/jwks.json',
|
||||
'base_url': 'https://app.preset.io',
|
||||
'data_residency': 'US'
|
||||
},
|
||||
'eu-west-1': {
|
||||
'jwks_uri': 'https://auth-eu.preset.io/.well-known/jwks.json',
|
||||
'base_url': 'https://eu.preset.io',
|
||||
'data_residency': 'EU'
|
||||
}
|
||||
}
|
||||
|
||||
return config_map.get(region, config_map['us-east-1'])
|
||||
|
||||
# Usage in config
|
||||
region_config = get_region_specific_config()
|
||||
PRESET_JWKS_URI = region_config['jwks_uri']
|
||||
SUPERSET_WEBSERVER_ADDRESS = region_config['base_url']
|
||||
```
|
||||
|
||||
### Health Check Extensions
|
||||
|
||||
```python
|
||||
# preset/mcp/health.py
|
||||
@mcp.tool
|
||||
def preset_health_check() -> HealthCheckResponse:
|
||||
"""Preset-specific health check for MCP service."""
|
||||
|
||||
checks = {
|
||||
'mcp_service': check_mcp_service_health(),
|
||||
'database': check_database_health(),
|
||||
'auth_provider': check_auth_provider_health(),
|
||||
'tenant_isolation': check_tenant_isolation(),
|
||||
'rls_engine': check_rls_engine_health()
|
||||
}
|
||||
|
||||
overall_status = 'healthy' if all(
|
||||
check['status'] == 'healthy' for check in checks.values()
|
||||
) else 'degraded'
|
||||
|
||||
return HealthCheckResponse(
|
||||
status=overall_status,
|
||||
checks=checks,
|
||||
region=os.environ.get('PRESET_REGION'),
|
||||
version=get_preset_mcp_version()
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration Templates
|
||||
|
||||
### Production Configuration
|
||||
|
||||
```python
|
||||
# preset_production_config.py
|
||||
from preset.mcp.auth import create_preset_oidc_auth
|
||||
from preset.mcp.audit import create_preset_audit_context
|
||||
|
||||
# MCP Service Configuration
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_AUTH_FACTORY = create_preset_oidc_auth
|
||||
MCP_AUDIT_CONTEXT_FACTORY = create_preset_audit_context
|
||||
|
||||
# Preset OIDC Configuration
|
||||
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
|
||||
PRESET_OIDC_CLIENT_ID = "preset-mcp-production"
|
||||
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
|
||||
|
||||
# Security Configuration
|
||||
PRESET_MCP_REQUIRED_SCOPES = [
|
||||
"openid", "profile", "email",
|
||||
"tenant:read", "workspace:read",
|
||||
"dashboard:read", "chart:read", "dataset:read"
|
||||
]
|
||||
|
||||
# Audit Configuration
|
||||
PRESET_AUDIT_ENABLED = True
|
||||
PRESET_SIEM_ENABLED = True
|
||||
PRESET_COMPLIANCE_MODE = "SOC2"
|
||||
|
||||
# Performance Configuration
|
||||
PRESET_MCP_CACHE_ENABLED = True
|
||||
PRESET_MCP_RATE_LIMIT = "1000/hour"
|
||||
PRESET_MCP_TIMEOUT = 30
|
||||
```
|
||||
|
||||
This integration guide provides the Preset.io team with concrete extension points for implementing enterprise features while maintaining compatibility with the base MCP service architecture.
|
||||
@@ -28,6 +28,9 @@ 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,
|
||||
@@ -36,7 +39,7 @@ module.exports = defineConfig([
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
@@ -68,5 +71,5 @@ module.exports = defineConfig([
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
}
|
||||
])
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc",
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
"eslint": "eslint ."
|
||||
},
|
||||
"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.3",
|
||||
"antd": "^5.26.7",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less": "^4.4.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.26.0"
|
||||
"swagger-ui-react": "^5.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@eslint/js": "^9.32.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.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"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.99.9"
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -87,16 +87,6 @@ const sidebars = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'MCP Service',
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'mcp-service',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'FAQ',
|
||||
|
||||
133
docs/yarn.lock
133
docs/yarn.lock
@@ -2205,11 +2205,16 @@
|
||||
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"
|
||||
@@ -2512,10 +2517,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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/portal" "^1.1.0"
|
||||
@@ -3422,10 +3427,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@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/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/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
|
||||
version "5.0.6"
|
||||
@@ -3966,6 +3971,11 @@ 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"
|
||||
@@ -3978,12 +3988,7 @@ 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.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:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -4107,10 +4112,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.3:
|
||||
version "5.26.3"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
|
||||
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
|
||||
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==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4123,7 +4128,7 @@ antd@^5.26.3:
|
||||
"@rc-component/mutate-observer" "^1.1.0"
|
||||
"@rc-component/qrcode" "~1.0.0"
|
||||
"@rc-component/tour" "~1.15.1"
|
||||
"@rc-component/trigger" "^2.2.7"
|
||||
"@rc-component/trigger" "^2.3.0"
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
@@ -4153,7 +4158,7 @@ antd@^5.26.3:
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.51.1"
|
||||
rc-tabs "~15.6.1"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-textarea "~1.10.1"
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
@@ -4508,17 +4513,7 @@ 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:
|
||||
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:
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, 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==
|
||||
@@ -4620,7 +4615,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.30001688, caniuse-lite@^1.0.30001702:
|
||||
caniuse-lite@^1.0.0, 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==
|
||||
@@ -5903,11 +5898,6 @@ 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"
|
||||
@@ -5952,10 +5942,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
@@ -6161,10 +6151,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.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-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-plugin-prettier@^5.5.1:
|
||||
version "5.5.1"
|
||||
@@ -8063,10 +8053,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.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
|
||||
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
|
||||
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==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
@@ -10714,10 +10704,10 @@ rc-tabs@~15.6.1:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
@@ -12100,10 +12090,10 @@ swagger-client@^3.35.5:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
@@ -12525,7 +12515,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.1, update-browserslist-db@^1.1.3:
|
||||
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==
|
||||
@@ -12794,26 +12784,27 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
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-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@^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==
|
||||
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==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/estree" "^1.0.8"
|
||||
"@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.14.0"
|
||||
acorn "^8.15.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.24.0"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.17.1"
|
||||
enhanced-resolve "^5.17.2"
|
||||
es-module-lexer "^1.2.1"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -12827,7 +12818,7 @@ webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.2.3"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
|
||||
486
package-lock.json
generated
Normal file
486
package-lock.json
generated
Normal file
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,6 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = ["fastmcp>=2.8.1"]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
@@ -203,7 +202,6 @@ development = [
|
||||
"pyinstrument>=4.0.2,<5",
|
||||
"pylint",
|
||||
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
|
||||
"pytest-asyncio", # need this due to not using latest pytest
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
"python-ldap>=3.4.4",
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
-e .[development,bigquery,druid,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
|
||||
@@ -10,14 +10,6 @@ amqp==5.3.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# kombu
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.9.0
|
||||
# via
|
||||
# httpx
|
||||
# mcp
|
||||
# sse-starlette
|
||||
# starlette
|
||||
apispec==6.6.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -32,14 +24,11 @@ attrs==25.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cattrs
|
||||
# cyclopts
|
||||
# jsonschema
|
||||
# outcome
|
||||
# referencing
|
||||
# requests-cache
|
||||
# trio
|
||||
authlib==1.6.1
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -88,8 +77,6 @@ celery==5.5.2
|
||||
certifi==2025.6.15
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
# selenium
|
||||
cffi==1.17.1
|
||||
@@ -114,7 +101,6 @@ click==8.2.1
|
||||
# click-repl
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
# uvicorn
|
||||
click-didyoumean==0.3.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -154,13 +140,10 @@ cryptography==44.0.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# authlib
|
||||
# paramiko
|
||||
# pyopenssl
|
||||
cycler==0.12.1
|
||||
# via matplotlib
|
||||
cyclopts==3.22.2
|
||||
# via fastmcp
|
||||
db-dtypes==1.3.1
|
||||
# via pandas-gbq
|
||||
defusedxml==0.7.1
|
||||
@@ -185,23 +168,14 @@ dnspython==2.7.0
|
||||
# email-validator
|
||||
docker==7.0.0
|
||||
# via apache-superset
|
||||
docstring-parser==0.17.0
|
||||
# via cyclopts
|
||||
docutils==0.21.2
|
||||
# via rich-rst
|
||||
email-validator==2.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
# pydantic
|
||||
et-xmlfile==2.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openpyxl
|
||||
exceptiongroup==1.3.0
|
||||
# via fastmcp
|
||||
fastmcp==2.10.6
|
||||
# via apache-superset
|
||||
filelock==3.12.2
|
||||
# via virtualenv
|
||||
flask==2.3.3
|
||||
@@ -353,8 +327,6 @@ gunicorn==23.0.0
|
||||
h11==0.16.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# httpcore
|
||||
# uvicorn
|
||||
# wsproto
|
||||
hashids==1.3.1
|
||||
# via
|
||||
@@ -365,14 +337,6 @@ holidays==0.25
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# prophet
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via
|
||||
# fastmcp
|
||||
# mcp
|
||||
httpx-sse==0.4.1
|
||||
# via mcp
|
||||
humanize==4.12.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -382,9 +346,7 @@ identify==2.5.36
|
||||
idna==3.10
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# anyio
|
||||
# email-validator
|
||||
# httpx
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
@@ -416,7 +378,6 @@ jsonschema==4.23.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
# mcp
|
||||
# openapi-schema-validator
|
||||
# openapi-spec-validator
|
||||
jsonschema-path==0.3.4
|
||||
@@ -476,8 +437,6 @@ matplotlib==3.9.0
|
||||
# via prophet
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
mcp==1.12.0
|
||||
# via fastmcp
|
||||
mdurl==0.1.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -516,8 +475,6 @@ odfpy==1.4.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# pandas
|
||||
openapi-pydantic==0.5.1
|
||||
# via fastmcp
|
||||
openapi-schema-validator==0.6.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -650,16 +607,6 @@ pycparser==2.22
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cffi
|
||||
pydantic==2.11.7
|
||||
# via
|
||||
# fastmcp
|
||||
# mcp
|
||||
# openapi-pydantic
|
||||
# pydantic-settings
|
||||
pydantic-core==2.33.2
|
||||
# via pydantic
|
||||
pydantic-settings==2.10.1
|
||||
# via mcp
|
||||
pydata-google-auth==1.9.0
|
||||
# via pandas-gbq
|
||||
pydruid==0.6.9
|
||||
@@ -695,8 +642,6 @@ pyparsing==3.2.3
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
pyperclip==1.9.0
|
||||
# via fastmcp
|
||||
pysocks==1.7.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -704,11 +649,8 @@ pysocks==1.7.1
|
||||
pytest==7.4.4
|
||||
# via
|
||||
# apache-superset
|
||||
# pytest-asyncio
|
||||
# pytest-cov
|
||||
# pytest-mock
|
||||
pytest-asyncio==0.23.8
|
||||
# via apache-superset
|
||||
pytest-cov==6.0.0
|
||||
# via apache-superset
|
||||
pytest-mock==3.10.0
|
||||
@@ -732,16 +674,12 @@ python-dotenv==1.1.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# fastmcp
|
||||
# pydantic-settings
|
||||
python-geohash==0.8.5
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
python-ldap==3.4.4
|
||||
# via apache-superset
|
||||
python-multipart==0.0.20
|
||||
# via mcp
|
||||
pytz==2025.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -796,12 +734,7 @@ rfc3339-validator==0.1.4
|
||||
rich==13.9.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cyclopts
|
||||
# fastmcp
|
||||
# flask-limiter
|
||||
# rich-rst
|
||||
rich-rst==1.3.1
|
||||
# via cyclopts
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -846,7 +779,6 @@ slack-sdk==3.35.0
|
||||
sniffio==1.3.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# anyio
|
||||
# trio
|
||||
sortedcontainers==2.4.0
|
||||
# via
|
||||
@@ -876,14 +808,10 @@ sqlglot==27.3.0
|
||||
# apache-superset
|
||||
sqloxide==0.1.51
|
||||
# via apache-superset
|
||||
sse-starlette==2.4.1
|
||||
# via mcp
|
||||
sshtunnel==0.4.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
starlette==0.47.2
|
||||
# via mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
tabulate==0.9.0
|
||||
@@ -911,23 +839,13 @@ typing-extensions==4.14.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# anyio
|
||||
# apache-superset
|
||||
# cattrs
|
||||
# exceptiongroup
|
||||
# limits
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# selenium
|
||||
# shillelagh
|
||||
# starlette
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
tzdata==2025.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -946,8 +864,6 @@ urllib3==2.5.0
|
||||
# requests
|
||||
# requests-cache
|
||||
# selenium
|
||||
uvicorn==0.35.0
|
||||
# via mcp
|
||||
vine==5.1.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
|
||||
@@ -33,4 +33,4 @@ superset load-test-users
|
||||
|
||||
echo "Running tests"
|
||||
|
||||
pytest --durations-min=2 --maxfail=1 --cov-report= --cov=superset ./tests/integration_tests "$@"
|
||||
pytest --durations-min=2 --cov-report= --cov=superset ./tests/integration_tests "$@"
|
||||
|
||||
@@ -94,67 +94,12 @@ 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');
|
||||
|
||||
@@ -31,6 +31,52 @@ 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) {
|
||||
@@ -62,14 +108,20 @@ 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"]')
|
||||
.then($el => {
|
||||
cy.wrap($el)
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.trigger('keydown', { keyCode: 13, which: 13, force: true });
|
||||
});
|
||||
.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');
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
@@ -230,17 +282,19 @@ 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('opens the modal from the context menu', () => {
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
@@ -384,6 +438,7 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -547,6 +602,7 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import {
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
@@ -160,6 +161,74 @@ 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 },
|
||||
|
||||
@@ -68,11 +68,13 @@ function verifyDashboardSearch() {
|
||||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover', {
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
.click({ force: true });
|
||||
cy.wait('@get');
|
||||
}
|
||||
|
||||
|
||||
1611
superset-frontend/package-lock.json
generated
1611
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
@@ -208,7 +208,7 @@
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
@@ -243,7 +243,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -331,7 +331,7 @@
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.19.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.99.9",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.2",
|
||||
"jest": "^30.0.4",
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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} />;
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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} />
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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} />;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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} />;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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} />;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -32,7 +33,26 @@ 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';
|
||||
|
||||
@@ -18,6 +18,20 @@
|
||||
*/
|
||||
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,
|
||||
@@ -26,30 +40,34 @@ import {
|
||||
} from '../shared-controls';
|
||||
|
||||
const controlsWithoutXAxis: ControlSetRow[] = [
|
||||
['metrics'],
|
||||
['groupby'],
|
||||
[MetricsControl()],
|
||||
[GroupByControl()],
|
||||
[contributionModeControl],
|
||||
['adhoc_filters'],
|
||||
['limit', 'group_others_when_limit_reached'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
['row_limit'],
|
||||
['truncate_metric'],
|
||||
['show_empty_columns'],
|
||||
[AdhocFiltersControl()],
|
||||
[LimitControl(), GroupOthersWhenLimitReachedControl()],
|
||||
[TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
[RowLimitControl()],
|
||||
[TruncateMetricControl()],
|
||||
[ShowEmptyColumnsControl()],
|
||||
];
|
||||
|
||||
export const echartsTimeSeriesQuery: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [['x_axis'], ['time_grain_sqla'], ...controlsWithoutXAxis],
|
||||
controlSetRows: [
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
...controlsWithoutXAxis,
|
||||
],
|
||||
};
|
||||
|
||||
export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['x_axis'],
|
||||
['time_grain_sqla'],
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
[xAxisForceCategoricalControl],
|
||||
[xAxisSortControl],
|
||||
[xAxisSortAscControl],
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
*/
|
||||
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.
|
||||
@@ -31,10 +40,10 @@ const baseTimeSection = {
|
||||
export const legacyTimeseriesTime: ControlPanelSectionConfig = {
|
||||
...baseTimeSection,
|
||||
controlSetRows: [
|
||||
['granularity'],
|
||||
['granularity_sqla'],
|
||||
['time_grain_sqla'],
|
||||
['time_range'],
|
||||
[GranularityControl()],
|
||||
[GranularitySqlaControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
[TimeRangeControl()],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -42,8 +51,8 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
label: t('Datasource & Chart Type'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['datasource'],
|
||||
['viz_type'],
|
||||
[DatasourceControl()],
|
||||
[VizTypeControl()],
|
||||
[
|
||||
{
|
||||
name: 'slice_id',
|
||||
@@ -91,7 +100,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
|
||||
export const colorScheme: ControlPanelSectionConfig = {
|
||||
label: t('Color Scheme'),
|
||||
controlSetRows: [['color_scheme']],
|
||||
controlSetRows: [[ColorSchemeControl()]],
|
||||
};
|
||||
|
||||
export const annotations: ControlPanelSectionConfig = {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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={() => {}}
|
||||
/>,
|
||||
],
|
||||
],
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -16,14 +16,42 @@
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Shared chart controls. Can be referred via string shortcuts in chart control
|
||||
* configs.
|
||||
*/
|
||||
export default {
|
||||
RadioButtonControl,
|
||||
};
|
||||
// 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
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
export { default as sharedControls } from './sharedControls';
|
||||
// React control components
|
||||
export { default as sharedControlComponents } from './components';
|
||||
// sharedControlComponents is deprecated - import components directly instead
|
||||
// export { default as sharedControlComponents } from './components';
|
||||
export { aggregationControl } from './customControls';
|
||||
export * from './components';
|
||||
export * from './customControls';
|
||||
|
||||
@@ -177,6 +177,7 @@ 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'> = {
|
||||
@@ -204,6 +205,7 @@ 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'> = {
|
||||
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
QueryFormMetric,
|
||||
QueryResponse,
|
||||
} from '@superset-ui/core';
|
||||
import { sharedControls, sharedControlComponents } from './shared-controls';
|
||||
import { sharedControls } from './shared-controls';
|
||||
|
||||
export type { Metric } from '@superset-ui/core';
|
||||
export type { ControlComponentProps } from './shared-controls/components/types';
|
||||
@@ -45,8 +45,6 @@ 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
|
||||
@@ -185,7 +183,7 @@ export type InternalControlType =
|
||||
| 'Select'
|
||||
| 'Slider'
|
||||
| 'Input'
|
||||
| keyof SharedControlComponents; // expanded in `expandControlConfig`
|
||||
| 'RadioButtonControl';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ControlType = InternalControlType | ComponentType<any>;
|
||||
@@ -359,7 +357,7 @@ export type SharedSectionAlias =
|
||||
| 'NVD3TimeSeries';
|
||||
|
||||
export interface OverrideSharedControlItem<
|
||||
A extends SharedControlAlias = SharedControlAlias,
|
||||
A extends keyof SharedControls = keyof SharedControls,
|
||||
> {
|
||||
name: A;
|
||||
override: Partial<SharedControls[A]>;
|
||||
@@ -382,16 +380,16 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
|
||||
// interfere with other ControlSetItem types
|
||||
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
|
||||
|
||||
export type ControlSetItem =
|
||||
| SharedControlAlias
|
||||
| OverrideSharedControlItem
|
||||
| ExpandedControlItem;
|
||||
// All controls must be React components or control configuration objects
|
||||
export type ControlSetItem = 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;
|
||||
@@ -428,6 +426,7 @@ 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;
|
||||
@@ -437,7 +436,7 @@ export interface ControlPanelConfig {
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
[P in SharedControlAlias]?: Partial<SharedControls[P]>;
|
||||
[P in keyof SharedControls]?: Partial<SharedControls[P]>;
|
||||
};
|
||||
|
||||
export type SectionOverrides = {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -17,28 +17,15 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { isValidElement, ReactElement } from 'react';
|
||||
import { sharedControls, sharedControlComponents } from '../shared-controls';
|
||||
import { sharedControls } 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 shorthand control config item to full config in the format of
|
||||
* Expand a control config item to full config in the format of
|
||||
* {
|
||||
* name: ...,
|
||||
* config: {
|
||||
@@ -46,26 +33,31 @@ export function expandControlType(controlType: ControlType) {
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 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 || typeof control === 'string' || isValidElement(control)) {
|
||||
if (!control || isValidElement(control)) {
|
||||
return control as ReactElement;
|
||||
}
|
||||
// Check if it's a modern panel component (function with isModernPanel flag)
|
||||
if (typeof control === 'function' && (control as any).isModernPanel) {
|
||||
console.log('expandControlConfig - Found modern panel, returning as-is');
|
||||
return control as any;
|
||||
}
|
||||
// String controls are no longer supported - they must be migrated to React components
|
||||
if (typeof control === 'string') {
|
||||
throw new Error(
|
||||
`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',
|
||||
@@ -74,13 +66,7 @@ export function expandControlConfig(
|
||||
// }
|
||||
// }
|
||||
if ('name' in control && 'config' in control) {
|
||||
return {
|
||||
...control,
|
||||
config: {
|
||||
...control.config,
|
||||
type: expandControlType(control.config.type as ControlType),
|
||||
},
|
||||
};
|
||||
return control;
|
||||
}
|
||||
// apply overrides with shared controls
|
||||
if ('override' in control && control.name in sharedControls) {
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,15 +20,19 @@ import {
|
||||
expandControlConfig,
|
||||
sharedControls,
|
||||
CustomControlItem,
|
||||
sharedControlComponents,
|
||||
} from '../../src';
|
||||
|
||||
describe('expandControlConfig()', () => {
|
||||
it('expands shared control alias', () => {
|
||||
expect(expandControlConfig('metrics')).toEqual({
|
||||
name: 'metrics',
|
||||
config: sharedControls.metrics,
|
||||
});
|
||||
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 control with overrides', () => {
|
||||
@@ -69,7 +73,7 @@ describe('expandControlConfig()', () => {
|
||||
};
|
||||
expect(
|
||||
(expandControlConfig(input) as CustomControlItem).config.type,
|
||||
).toEqual(sharedControlComponents.RadioButtonControl);
|
||||
).toEqual('RadioButtonControl');
|
||||
});
|
||||
|
||||
it('leave NULL and ReactElement untouched', () => {
|
||||
@@ -78,11 +82,6 @@ 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),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { DrawerProps } from './types';
|
||||
|
||||
export { Drawer } from 'antd';
|
||||
export type { DrawerProps };
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { DrawerProps } from 'antd/es/drawer';
|
||||
|
||||
export { DrawerProps };
|
||||
@@ -132,7 +132,7 @@ const VirtualTable = <RecordType extends object>(
|
||||
if (gridRef.current) {
|
||||
return gridRef.current?.state?.scrollLeft;
|
||||
}
|
||||
return null;
|
||||
return 0;
|
||||
},
|
||||
set: (scrollLeft: number) => {
|
||||
if (gridRef.current) {
|
||||
|
||||
@@ -76,6 +76,7 @@ export { CronPicker, type CronError } from './CronPicker';
|
||||
export * from './DatePicker';
|
||||
export { DeleteModal, type DeleteModalProps } from './DeleteModal';
|
||||
export { Divider, type DividerProps } from './Divider';
|
||||
export { Drawer, type DrawerProps } from './Drawer';
|
||||
export {
|
||||
Dropdown,
|
||||
MenuDotsDropdown,
|
||||
|
||||
@@ -49,6 +49,21 @@ describe('isProbablyHTML', () => {
|
||||
const trickyText = 'a <= 10 and b > 10';
|
||||
expect(isProbablyHTML(trickyText)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for strings with angle brackets that are not HTML', () => {
|
||||
// Test case from issue #25561
|
||||
expect(isProbablyHTML('<abcdef:12345>')).toBe(false);
|
||||
|
||||
// Other similar cases
|
||||
expect(isProbablyHTML('<foo:bar>')).toBe(false);
|
||||
expect(isProbablyHTML('<123>')).toBe(false);
|
||||
expect(isProbablyHTML('<test@example.com>')).toBe(false);
|
||||
expect(isProbablyHTML('<custom-element>')).toBe(false);
|
||||
|
||||
// Mathematical expressions
|
||||
expect(isProbablyHTML('if x < 5 and y > 10')).toBe(false);
|
||||
expect(isProbablyHTML('price < $100')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeHtmlIfNeeded', () => {
|
||||
|
||||
@@ -68,9 +68,87 @@ export function isProbablyHTML(text: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the string contains common HTML patterns
|
||||
if (!hasHtmlTagPattern(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(cleanedStr, 'text/html');
|
||||
return Array.from(doc.body.childNodes).some(({ nodeType }) => nodeType === 1);
|
||||
|
||||
// Check if parsing created actual HTML elements (not just text nodes)
|
||||
const elements = Array.from(doc.body.childNodes).filter(
|
||||
node => node.nodeType === 1,
|
||||
) as Element[];
|
||||
|
||||
// If no elements were created, it's not HTML
|
||||
if (elements.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the elements are known HTML tags (not custom/unknown tags)
|
||||
// This prevents strings like "<abcdef:12345>" from being treated as HTML
|
||||
return elements.some(element => {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
// List of common HTML tags we want to recognize
|
||||
const knownHtmlTags = [
|
||||
'div',
|
||||
'span',
|
||||
'p',
|
||||
'a',
|
||||
'b',
|
||||
'i',
|
||||
'u',
|
||||
'em',
|
||||
'strong',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'th',
|
||||
'tbody',
|
||||
'thead',
|
||||
'tfoot',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'img',
|
||||
'br',
|
||||
'hr',
|
||||
'pre',
|
||||
'code',
|
||||
'blockquote',
|
||||
'section',
|
||||
'article',
|
||||
'nav',
|
||||
'header',
|
||||
'footer',
|
||||
'form',
|
||||
'input',
|
||||
'button',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'label',
|
||||
'fieldset',
|
||||
'legend',
|
||||
'video',
|
||||
'audio',
|
||||
'canvas',
|
||||
'iframe',
|
||||
'script',
|
||||
'style',
|
||||
'link',
|
||||
'meta',
|
||||
'title',
|
||||
];
|
||||
return knownHtmlTags.includes(tagName);
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitizeHtmlIfNeeded(htmlString: string) {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@storybook/types": "8.4.7",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"core-js": "3.40.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"jquery": "^3.7.1",
|
||||
"memoize-one": "^5.2.1",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -22,6 +22,15 @@ import {
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
AdhocFiltersControl,
|
||||
GranularitySqlaControl,
|
||||
LinearColorSchemeControl,
|
||||
MetricsControl,
|
||||
TimeRangeControl,
|
||||
YAxisFormatControl,
|
||||
InlineSelectControl,
|
||||
InlineTextControl,
|
||||
InlineCheckboxControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -30,51 +39,43 @@ const config: ControlPanelConfig = {
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
controlSetRows: [[GranularitySqlaControl()], [TimeRangeControl()]],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
InlineSelectControl('domain_granularity', {
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
}),
|
||||
InlineSelectControl('subdomain_granularity', {
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
}),
|
||||
],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
[MetricsControl()],
|
||||
[AdhocFiltersControl()],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -82,109 +83,77 @@ const config: ControlPanelConfig = {
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
[LinearColorSchemeControl()],
|
||||
[
|
||||
{
|
||||
name: 'cell_size',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
default: 10,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
label: t('Cell Size'),
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cell_padding',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
label: t('Cell Padding'),
|
||||
description: t('The distance between cells, in pixels'),
|
||||
},
|
||||
},
|
||||
InlineTextControl('cell_size', {
|
||||
label: t('Cell Size'),
|
||||
default: 10,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
}),
|
||||
InlineTextControl('cell_padding', {
|
||||
label: t('Cell Padding'),
|
||||
default: 2,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The distance between cells, in pixels'),
|
||||
}),
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'cell_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 0,
|
||||
label: t('Cell Radius'),
|
||||
description: t('The pixel radius'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'steps',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 10,
|
||||
label: t('Color Steps'),
|
||||
description: t('The number color "steps"'),
|
||||
},
|
||||
},
|
||||
InlineTextControl('cell_radius', {
|
||||
label: t('Cell Radius'),
|
||||
default: 0,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The pixel radius'),
|
||||
}),
|
||||
InlineTextControl('steps', {
|
||||
label: t('Color Steps'),
|
||||
default: 10,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The number color "steps"'),
|
||||
}),
|
||||
],
|
||||
[
|
||||
'y_axis_format',
|
||||
{
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
YAxisFormatControl(),
|
||||
InlineSelectControl('x_axis_time_format', {
|
||||
label: t('Time Format'),
|
||||
default: 'smart_date',
|
||||
freeForm: true,
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
}),
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_legend',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Legend'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_values',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Values'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
},
|
||||
},
|
||||
InlineCheckboxControl('show_legend', {
|
||||
label: t('Legend'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
}),
|
||||
InlineCheckboxControl('show_values', {
|
||||
label: t('Show Values'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
}),
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Names'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
},
|
||||
},
|
||||
InlineCheckboxControl('show_metric_name', {
|
||||
label: t('Show Metric Names'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
}),
|
||||
null,
|
||||
],
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user