Compare commits

..

2 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,12 +47,10 @@ jobs:
java-version: '21'
- name: Install Graphviz
run: sudo apt-get install -y graphviz
- name: Generate documentation artifacts
- name: Compute Entity Relationship diagram (ERD)
env:
SUPERSET_SECRET_KEY: not-a-secret
CI: true
run: |
# Generate ERD
python scripts/erd/erd.py
curl -L http://sourceforge.net/projects/plantuml/files/1.2023.7/plantuml.1.2023.7.jar/download > ~/plantuml.jar
java -jar ~/plantuml.jar -v -tsvg -r -o "${{ github.workspace }}/docs/static/img/" "${{ github.workspace }}/scripts/erd/erd.puml"

View File

@@ -64,11 +64,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: './docs/.nvmrc'
- name: Setup Python Backend
uses: ./.github/actions/setup-backend
with:
python-version: 'current'
requirements-type: 'base'
- name: yarn install
run: |
yarn install --check-cache

View File

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

3
.gitignore vendored
View File

@@ -131,6 +131,3 @@ superset/static/stats/statistics.html
# LLM-related
CLAUDE.local.md
.aider*
# Temporary scratchpad for development
.scratchpad/

View File

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

View File

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

45
LLMS.md
View File

@@ -22,11 +22,6 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
- **MyPy compliance** - Run `pre-commit run mypy` to validate
- **SQLAlchemy typing** - Use proper model annotations
### UUID Migration
- **Prefer UUIDs over auto-incrementing IDs** - New models should use UUID primary keys
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
## Key Directories
```
@@ -94,10 +89,6 @@ superset/
- **`selectOption()`** - Select component helper
- **React Testing Library** - NO Enzyme (removed)
### Test Database Patterns
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
- **API tests**: Update expected columns when adding new model fields
### Running Tests
```bash
# Frontend
@@ -129,10 +120,6 @@ curl -f http://localhost:8088/health || echo "❌ Setup required - see https://s
- `pyproject.toml` - Python tooling (ruff, mypy configs)
- `requirements/` folder - Python dependencies (base.txt, development.txt)
## SQLAlchemy Query Best Practices
- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
- **Example**: `~Model.is_active` instead of `Model.is_active == False`
## Pre-commit Validation
**Use pre-commit hooks for quality validation:**
@@ -141,43 +128,13 @@ curl -f http://localhost:8088/health || echo "❌ Setup required - see https://s
# Install hooks
pre-commit install
# IMPORTANT: Stage your changes first!
git add . # Pre-commit only checks staged files
# Quick validation (faster than --all-files)
pre-commit run # Staged files only
pre-commit run # Staged files only
pre-commit run mypy # Python type checking
pre-commit run prettier # Code formatting
pre-commit run eslint # Frontend linting
```
**Important pre-commit usage notes:**
- **Stage files first**: Run `git add .` before `pre-commit run` to check only changed files (much faster)
- **Virtual environment**: Activate your Python virtual environment before running pre-commit
```bash
# Common virtual environment locations (yours may differ):
source .venv/bin/activate # if using .venv
source venv/bin/activate # if using venv
source ~/venvs/superset/bin/activate # if using a central location
```
If you get a "command not found" error, ask the user which virtual environment to activate
- **Auto-fixes**: Some hooks auto-fix issues (e.g., trailing whitespace). Re-run after fixes are applied
## Common File Patterns
### API Structure
- **`/api.py`** - REST endpoints with decorators and OpenAPI docstrings
- **`/schemas.py`** - Marshmallow validation schemas for OpenAPI spec
- **`/commands/`** - Business logic classes with @transaction() decorators
- **`/models/`** - SQLAlchemy database models
- **OpenAPI docs**: Auto-generated at `/swagger/v1` from docstrings and schemas
### Migration Files
- **Location**: `superset/migrations/versions/`
- **Naming**: `YYYY-MM-DD_HH-MM_hash_description.py`
- **Utilities**: Use helpers from `superset.migrations.shared.utils` for database compatibility
- **Pattern**: Import utilities instead of raw SQLAlchemy operations
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools

View File

@@ -23,8 +23,6 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
- [34204](https://github.com/apache/superset/pull/33603) OpenStreetView has been promoted as the new default for Deck.gl visualization since it can be enabled by default without requiring an API key. If you have Mapbox set up and want to disable OpenStreeView in your environment, please follow the steps documented here [https://superset.apache.org/docs/configuration/map-tiles].
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.

View File

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

View File

@@ -1,157 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# -----------------------------------------------------------------------
# Lightweight docker-compose for running multiple Superset instances
# This includes only essential services: database, Redis, and Superset app
#
# IMPORTANT: To run multiple instances in parallel:
# - Use different project names: docker-compose -p project1 -f docker-compose-light.yml up
# - Use different NODE_PORT values: NODE_PORT=9002 docker-compose -p project2 -f docker-compose-light.yml up
# - Volumes are isolated by project name (e.g., project1_db_home_light, project2_db_home_light)
# - Database name is intentionally different (superset_light) to prevent accidental cross-connections
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
x-superset-user: &superset-user root
x-superset-volumes: &superset-volumes
# /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- ./superset:/app/superset
- ./superset-frontend:/app/superset-frontend
- superset_home_light:/app/superset_home
- ./tests:/app/tests
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-bookworm
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
services:
db-light:
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
image: postgres:16
restart: unless-stopped
# No host port mapping - only accessible within Docker network
volumes:
- db_home_light:/var/lib/postgresql/data
- ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
# Override database name to avoid conflicts
POSTGRES_DB: superset_light
superset-light:
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
build:
<<: *common-build
command: ["/app/docker/docker-bootstrap.sh", "app"]
restart: unless-stopped
# No host port mapping - accessed via webpack dev server proxy
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user
depends_on:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
environment:
# Override DB connection for light service
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
EXAMPLES_HOST: db-light
EXAMPLES_DB: superset_light
EXAMPLES_USER: superset
EXAMPLES_PASSWORD: superset
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
superset-init-light:
build:
<<: *common-build
command: ["/app/docker/docker-init.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
depends_on:
db-light:
condition: service_started
user: *superset-user
volumes: *superset-volumes
environment:
# Override DB connection for light service
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
EXAMPLES_HOST: db-light
EXAMPLES_DB: superset_light
EXAMPLES_USER: superset
EXAMPLES_PASSWORD: superset
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
healthcheck:
disable: true
superset-node-light:
build:
context: .
target: superset-node
args:
# This prevents building the frontend bundle since we'll mount local folder
# and build it on startup while firing docker-frontend.sh in dev mode, where
# it'll mount and watch local files and rebuild as you update them
DEV_MODE: "true"
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
environment:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
# configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset-light:8088"
ports:
- "127.0.0.1:${NODE_PORT:-9001}:9000" # Parameterized port
command: ["/app/docker/docker-frontend.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
volumes: *superset-volumes
volumes:
superset_home_light:
external: false
db_home_light:
external: false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Configuration for docker-compose-light.yml - disables Redis and uses minimal services
# Import all settings from the main config first
from flask_caching.backends.filesystemcache import FileSystemCache
from superset_config import * # noqa: F403
# Override caching to use simple in-memory cache instead of Redis
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab")
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_KEY_PREFIX": "superset_light_",
}
DATA_CACHE_CONFIG = CACHE_CONFIG
THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
# Disable Celery entirely for lightweight mode
CELERY_CONFIG = None # type: ignore[assignment,misc]

View File

@@ -1,86 +0,0 @@
---
title: Configuration Reference
hide_title: true
sidebar_position: 1
version: 1
---
import ConfigurationTable from '@site/src/components/ConfigurationTable';
# Configuration Reference
This page provides a comprehensive reference for all Superset configuration options. These settings can be configured in your `superset_config.py` file or through environment variables.
## How to Use This Reference
- **Search**: Use the search box to find specific configuration settings
- **Filter by Category**: Use the dropdown to filter by configuration category
- **Environment Variables**: All configurations can be set via environment variables with the `SUPERSET__` prefix
- **Impact Level**: Each setting shows its impact level (low, medium, high)
- **Restart Required**: Settings marked with "RESTART" require a server restart to take effect
## Configuration Settings
<ConfigurationTable showEnvironmentVariables={true} />
## Setting Configuration Values
### In superset_config.py
```python
# Example configuration in superset_config.py
SECRET_KEY = 'your-secret-key-here'
SQLALCHEMY_DATABASE_URI = 'postgresql://user:pass@localhost/superset'
CACHE_DEFAULT_TIMEOUT = 300
```
### Via Environment Variables
```bash
# All configuration keys can be set via environment variables
export SUPERSET__SECRET_KEY="your-secret-key-here"
export SUPERSET__SQLALCHEMY_DATABASE_URI="postgresql://user:pass@localhost/superset"
export SUPERSET__CACHE_DEFAULT_TIMEOUT=300
```
### Configuration Precedence
Configuration values are loaded in the following order (later values override earlier ones):
1. **Default values** from `superset/config_defaults.py`
2. **Base configuration** from `superset/config.py`
3. **Custom configuration file** (if specified via `SUPERSET_CONFIG_PATH`)
4. **superset_config module** (if available in PYTHONPATH)
5. **Environment variables** with `SUPERSET__` prefix
## Configuration Categories
The configuration settings are organized into the following categories:
- **Security**: Authentication, authorization, and security-related settings
- **Database**: Database connection and SQL-related configurations
- **Performance**: Caching, timeouts, and performance optimization settings
- **Features**: Feature flags and optional functionality toggles
- **UI**: User interface and theming configurations
- **Logging**: Logging and monitoring configurations
- **Email**: Email and notification settings
- **Async**: Asynchronous processing and Celery settings
- **General**: Miscellaneous configuration options
## Important Security Notes
- Always set a strong `SECRET_KEY` in production
- Use environment variables for sensitive configuration values
- Never commit sensitive configuration values to version control
- Regularly rotate secrets and passwords
- Review security-related configurations before deploying
## Need Help?
For detailed information about specific configuration topics, see:
- [Configuring Superset](./configuring-superset.mdx) - General configuration guide
- [Security](../security/security.mdx) - Security configuration
- [Database Configuration](./databases.mdx) - Database-specific settings
- [Cache Configuration](./cache.mdx) - Caching setup
- [Async Queries](./async-queries-celery.mdx) - Celery configuration

View File

@@ -1,36 +1,19 @@
---
title: Configuring Superset
hide_title: true
sidebar_position: 2
sidebar_position: 1
version: 1
---
# Configuring Superset
## Configuration Overview
Superset provides a flexible, multi-layered configuration system that supports:
1. **File-based configuration** - Traditional Python configuration files
2. **Environment variables** - For containerized deployments and CI/CD
3. **Database-backed settings** - Runtime configuration changes (coming soon)
4. **Structured metadata** - Rich documentation and validation schemas
### Configuration Priority
Configuration values are loaded in the following order (later values override earlier ones):
1. **Default configuration** - Built-in defaults from `superset/config_defaults.py`
2. **Environment variables** - Values prefixed with `SUPERSET__`
3. **User configuration file** - Your custom `superset_config.py` file
### superset_config.py
## superset_config.py
Superset exposes hundreds of configurable parameters through its
[config_defaults.py module](https://github.com/apache/superset/blob/master/superset/config_defaults.py). The
[config.py module](https://github.com/apache/superset/blob/master/superset/config.py). The
variables and objects exposed act as a public interface of the bulk of what you may want
to configure, alter and interface with. In this python module, you'll find all these
parameters, sensible defaults, as well as structured metadata documentation.
parameters, sensible defaults, as well as rich documentation in the form of comments
To configure your application, you need to create your own configuration module, which
will allow you to override few or many of these parameters. Instead of altering the core module,
@@ -94,12 +77,12 @@ MAPBOX_API_KEY = ''
:::tip
Note that it is typical to copy and paste [only] the portions of the
core [superset/config_defaults.py](https://github.com/apache/superset/blob/master/superset/config_defaults.py) that
core [superset/config.py](https://github.com/apache/superset/blob/master/superset/config.py) that
you want to alter, along with the related comments into your own `superset_config.py` file.
:::
All the parameters and default values defined
in [superset/config_defaults.py](https://github.com/apache/superset/blob/master/superset/config_defaults.py)
in [superset/config.py](https://github.com/apache/superset/blob/master/superset/config.py)
can be altered in your local `superset_config.py`. Administrators will want to read through the file
to understand what can be configured locally as well as the default values in place.
@@ -114,102 +97,6 @@ for more information on how to configure it.
At the very least, you'll want to change `SECRET_KEY` and `SQLALCHEMY_DATABASE_URI`. Continue reading for more about each of these.
## Environment Variables Configuration
For containerized deployments and CI/CD pipelines, Superset supports configuration through environment variables. This is particularly useful for:
- **Docker deployments** - Configure containers without rebuilding images
- **Kubernetes environments** - Use ConfigMaps and Secrets
- **CI/CD pipelines** - Set configuration dynamically based on environment
- **Development workflows** - Override settings locally without changing files
### Environment Variable Format
All Superset environment variables must use the `SUPERSET__` prefix (note the double underscore):
```bash
# Basic settings
export SUPERSET__ROW_LIMIT=100000
export SUPERSET__SQLLAB_TIMEOUT=60
# Database configuration
export SUPERSET__SQLALCHEMY_DATABASE_URI="postgresql://user:pass@localhost/superset"
# Secret key
export SUPERSET__SECRET_KEY="your-secret-key-here"
```
### JSON and Complex Values
Environment variables automatically parse JSON values for complex configuration:
```bash
# Feature flags as JSON
export SUPERSET__FEATURE_FLAGS='{"ENABLE_TEMPLATE_PROCESSING": true, "ENABLE_EXPLORE_DRAG_AND_DROP": true}'
# Database configuration
export SUPERSET__DATABASE_CONFIG='{"timeout": 60, "pool_size": 10}'
```
### Nested Configuration
For nested configuration objects, use triple underscores (`___`) to separate levels:
```bash
# This sets FEATURE_FLAGS["ENABLE_TEMPLATE_PROCESSING"] = true
export SUPERSET__FEATURE_FLAGS__ENABLE_TEMPLATE_PROCESSING=true
# This sets THEME_DEFAULT["token"]["colorPrimary"] = "#ff0000"
export SUPERSET__THEME_DEFAULT__token__colorPrimary="#ff0000"
```
### Environment Variable Examples
You can view examples of environment variable configuration:
```bash
# Show all available environment variable examples
superset config env-examples
# Show current configuration and sources
superset config show --verbose
# Get a specific configuration value
superset config get ROW_LIMIT
```
### Docker Environment Variables
When using Docker, you can set environment variables in your `docker-compose.yml`:
```yaml
services:
superset:
image: apache/superset:latest
environment:
- SUPERSET__ROW_LIMIT=100000
- SUPERSET__SQLLAB_TIMEOUT=60
- SUPERSET__FEATURE_FLAGS__ENABLE_TEMPLATE_PROCESSING=true
# ... other configuration
```
Or use an environment file:
```bash
# .env file
SUPERSET__ROW_LIMIT=100000
SUPERSET__SQLLAB_TIMEOUT=60
SUPERSET__SECRET_KEY=your-secret-key-here
```
```yaml
services:
superset:
image: apache/superset:latest
env_file:
- .env
```
## Specifying a SECRET_KEY
### Adding an initial SECRET_KEY
@@ -659,93 +546,3 @@ FEATURE_FLAGS = {
```
A current list of feature flags can be found in [RESOURCES/FEATURE_FLAGS.md](https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md).
## Configuration Introspection
Superset provides CLI commands to inspect and understand your current configuration:
### View Current Configuration
```bash
# Show all configuration as YAML
superset config show
# Filter configuration by pattern
superset config show --filter "ROW_LIMIT"
# Show configuration with sources (where each value comes from)
superset config show --verbose
```
### Get Specific Configuration Values
```bash
# Get a specific configuration value with source information
superset config get ROW_LIMIT
# Output shows both the value and where it came from:
# ROW_LIMIT:
# source: environment (SUPERSET__ROW_LIMIT)
# value: 100000
```
### Environment Variable Examples
```bash
# Show examples of environment variables for all documented settings
superset config env-examples
# This shows:
# - Basic environment variable syntax
# - JSON formatting examples
# - Nested configuration examples
# - All documented settings with their metadata
```
### Configuration Sources
The CLI will show you where each configuration value comes from:
- **`environment (SUPERSET__KEY)`** - Value set via environment variable
- **`superset_config.py`** - Value set in your custom configuration file
- **`config_defaults.py`** - Default value from Superset's built-in configuration
This helps you understand the configuration precedence and troubleshoot configuration issues.
## Configuration Reference
The following table shows all documented configuration settings with their metadata:
import ConfigurationTable from '@site/src/components/ConfigurationTable';
<ConfigurationTable showEnvironmentVariables={true} />
## Environment Variables Examples
Here are ready-to-use environment variable examples:
import EnvironmentVariablesExample from '@site/src/components/EnvironmentVariablesExample';
<EnvironmentVariablesExample />
## Configuration Metadata and Documentation
Superset's configuration system includes rich metadata for many settings, providing:
- **Type information** - Whether a setting expects an integer, string, boolean, or object
- **Validation rules** - Minimum/maximum values, allowed options
- **Documentation** - Detailed descriptions of what each setting does
- **Impact levels** - How significant changes to this setting are
- **Restart requirements** - Whether changing this setting requires a restart
This metadata is used for:
- **CLI documentation** - The `superset config env-examples` command shows this information
- **Future admin UI** - Settings management interface (coming soon)
- **Validation** - Ensuring configuration values are valid
- **API documentation** - Automatic generation of configuration schemas
You can also access this information via CLI:
```bash
superset config env-examples
```

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,8 @@
"scripts": {
"docusaurus": "docusaurus",
"_init": "cat src/intro_header.txt ../README.md > docs/intro.md",
"_update-config": "bash scripts/generate_docs.sh",
"start": "yarn run _init && yarn run _update-config && docusaurus start",
"build": "yarn run _init && yarn run _update-config && DEBUG=docusaurus:* docusaurus build",
"start": "yarn run _init && docusaurus start",
"build": "yarn run _init && DEBUG=docusaurus:* docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -27,8 +26,6 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"ag-grid-community": "^34.1.0",
"ag-grid-react": "^34.1.0",
"antd": "^5.26.3",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.3.0",
@@ -37,7 +34,6 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-markdown": "^10.1.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.26.0"
},
@@ -69,6 +65,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
}

View File

@@ -1,116 +0,0 @@
#!/usr/bin/env python3
"""
Export configuration metadata to JSON for documentation generation.
This script loads configuration metadata from the Python metadata module
and exports it in JSON format for the documentation React components.
This script is called by docs/scripts/generate_docs.sh as part of the
unified documentation generation process.
"""
import json as json_module
import sys
from pathlib import Path
from typing import Any, Dict, List
# Add the superset directory to Python path
superset_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(superset_root))
def infer_impact(key: str) -> str:
"""Infer the impact level based on the configuration key name."""
name_lower = key.lower()
# High impact - security, database, core functionality
if any(
term in name_lower
for term in [
"secret",
"key",
"password",
"database",
"uri",
"url",
"security",
"auth",
]
):
return "high"
# Medium impact - performance, features, UI
elif any(
term in name_lower
for term in ["limit", "timeout", "cache", "feature", "flag", "theme"]
):
return "medium"
# Low impact - logging, debugging, minor settings
else:
return "low"
def infer_requires_restart(key: str) -> bool:
"""Infer if the configuration requires a restart based on the key name."""
name_lower = key.lower()
# These typically require restart
if any(
term in name_lower
for term in [
"secret",
"key",
"database",
"uri",
"url",
"security",
"auth",
"ssl",
"tls",
]
):
return True
# These typically don't require restart
elif any(
term in name_lower for term in ["limit", "timeout", "cache", "log", "debug"]
):
return False
# Default to requiring restart for safety
return True
def export_config_metadata() -> List[Dict[str, Any]]:
"""Export configuration metadata as JSON."""
try:
# Import from Python metadata module
from superset.config_metadata import export_for_documentation
# Get metadata from Python source
metadata_export = export_for_documentation()
# Export as JSON for documentation
output_dir = Path(__file__).parent.parent / "src" / "resources"
output_dir.mkdir(exist_ok=True)
# Write the full export (includes categories, etc.)
with open(output_dir / "config_metadata.json", "w") as f:
json_module.dump(metadata_export, f, indent=2)
output_file = output_dir / "config_metadata.json"
print(
f"Exported {len(metadata_export['all_settings'])} configuration settings to {output_file}"
)
return metadata_export["all_settings"]
except ImportError as e:
print(f"Error importing config_metadata: {e}")
print("Please ensure superset/config_metadata.py exists")
return []
if __name__ == "__main__":
export_config_metadata()

View File

@@ -1,101 +0,0 @@
#!/bin/bash
# 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.
# Unified documentation generation script
# This script generates all dynamic documentation artifacts needed for the docs build
set -e
echo "🚀 Generating documentation artifacts..."
# Navigate to the docs directory
cd "$(dirname "$0")/.."
# Track any failures
FAILED_TASKS=()
# 1. Extract configuration schema and export metadata
echo "📊 Extracting configuration schema and exporting metadata..."
if python ../scripts/extract_config_schema.py && python scripts/export_config_metadata.py; then
echo "✅ Configuration metadata exported successfully"
else
echo "⚠️ Warning: Failed to export configuration metadata"
echo " The documentation build will continue with existing metadata"
FAILED_TASKS+=("config_metadata")
fi
# 2. Generate OpenAPI documentation
echo "🔌 Generating OpenAPI documentation..."
if python -c "
import sys
sys.path.insert(0, '..')
from superset.app import create_app
from superset.cli.update import update_api_docs
import os
# Set required environment variables
os.environ['SUPERSET_SECRET_KEY'] = 'not-a-secret'
app = create_app()
with app.app_context():
update_api_docs()
"; then
echo "✅ OpenAPI documentation generated successfully"
else
echo "⚠️ Warning: Failed to generate OpenAPI documentation"
echo " The documentation build will continue with existing OpenAPI spec"
FAILED_TASKS+=("openapi")
fi
# 3. Generate ERD (Entity Relationship Diagram) if in CI environment
if [ -n "$CI" ] && [ -f "../scripts/erd/erd.py" ]; then
echo "🗂️ Generating Entity Relationship Diagram..."
if python ../scripts/erd/erd.py; then
echo "✅ ERD generated successfully"
else
echo "⚠️ Warning: Failed to generate ERD"
echo " The documentation build will continue without updated ERD"
FAILED_TASKS+=("erd")
fi
fi
# Summary
echo ""
echo "📝 Documentation generation summary:"
echo " - Configuration metadata: ${FAILED_TASKS[*]}" | grep -q "config_metadata" && echo " - Configuration metadata: ❌ Failed" || echo " - Configuration metadata: ✅ Success"
echo " - OpenAPI documentation: ${FAILED_TASKS[*]}" | grep -q "openapi" && echo " - OpenAPI documentation: ❌ Failed" || echo " - OpenAPI documentation: ✅ Success"
if [ -n "$CI" ]; then
echo " - ERD generation: ${FAILED_TASKS[*]}" | grep -q "erd" && echo " - ERD generation: ❌ Failed" || echo " - ERD generation: ✅ Success"
fi
if [ ${#FAILED_TASKS[@]} -eq 0 ]; then
echo ""
echo "🎉 All documentation artifacts generated successfully!"
else
echo ""
echo "⚠️ Some tasks failed but documentation build can continue"
echo " Failed tasks: ${FAILED_TASKS[*]}"
echo " To fix missing dependencies, run: pip install -e ."
fi
echo ""
echo "📁 Generated files:"
[ -f "src/resources/config_metadata.json" ] && echo " - src/resources/config_metadata.json"
[ -f "static/resources/openapi.json" ] && echo " - static/resources/openapi.json"
[ -f "static/img/erd.svg" ] && echo " - static/img/erd.svg"

View File

@@ -1,329 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { ColDef, GridReadyEvent, GridApi, ModuleRegistry, AllCommunityModule } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-material.css';
import configMetadata from '../resources/config_metadata.json';
// Register AG Grid modules
ModuleRegistry.registerModules([AllCommunityModule]);
// ConfigSetting interface is defined for type safety but not directly used
// as AG Grid uses dynamic property access
// interface ConfigSetting {
// key: string;
// title: string;
// description: string;
// details: string;
// type: string;
// category: string;
// group: string;
// default: any;
// env_var: string;
// external: boolean;
// source: string;
// supports_callable: boolean;
// }
interface ConfigurationTableProps {
category?: string;
showEnvironmentVariables?: boolean;
}
// Custom cell renderers
const KeyCellRenderer = (props: { value: string }) => {
return <span style={{ fontWeight: 'bold' }}>{props.value}</span>;
};
const TypeCellRenderer = (props: { value: string }) => {
return <code>{props.value}</code>;
};
const DefaultCellRenderer = (props: { value: unknown }) => {
const formatDefault = (value: unknown): string => {
if (value === null || value === undefined || value === 'None') return 'None';
if (typeof value === 'object') {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
return String(value);
};
const formatted = formatDefault(props.value);
const isLong = formatted.length > 50;
return (
<code
style={{
whiteSpace: isLong ? 'pre-wrap' : 'nowrap',
wordBreak: isLong ? 'break-all' : 'normal',
}}
title={isLong ? formatted : undefined}
>
{isLong ? formatted.substring(0, 50) + '...' : formatted}
</code>
);
};
const BooleanCellRenderer = (props: { value: boolean }) => {
return props.value ? '✅ Yes' : '❌ No';
};
const GroupCellRenderer = (props: { value: string | null }) => {
if (!props.value) return null;
return (
<span
style={{
backgroundColor: '#f0f0f0',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '0.9em',
}}
>
{props.value}
</span>
);
};
const DescriptionCellRenderer = (props: { value: string; data: { details?: string } }) => {
const hasDetails = props.data.details && props.data.details.trim() !== '';
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span>{props.value || 'No description available'}</span>
{hasDetails && (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '16px',
height: '16px',
backgroundColor: '#e8e8e8',
color: '#666',
borderRadius: '50%',
fontSize: '0.8em',
fontWeight: 'bold',
cursor: 'help',
flexShrink: 0,
border: '1px solid #d0d0d0',
}}
title={props.data.details}
>
i
</span>
)}
</div>
);
};
const ConfigurationTable: React.FC<ConfigurationTableProps> = ({
category, // eslint-disable-line @typescript-eslint/no-unused-vars
showEnvironmentVariables = false,
}) => {
const [gridApi, setGridApi] = useState<GridApi | null>(null);
const [searchText, setSearchText] = useState('');
// Process data to include only enriched configs
const rowData = useMemo(() => {
return configMetadata.all_settings;
}, []);
// Column definitions
const columnDefs = useMemo<ColDef[]>(() => {
const columns: ColDef[] = [
{
field: 'key',
headerName: 'Configuration Key',
cellRenderer: KeyCellRenderer,
width: 280,
pinned: 'left',
filter: 'agTextColumnFilter',
floatingFilter: true,
},
{
field: 'description',
headerName: 'Description',
cellRenderer: DescriptionCellRenderer,
flex: 2,
minWidth: 300,
wrapText: true,
autoHeight: true,
filter: 'agTextColumnFilter',
floatingFilter: true,
},
{
field: 'type',
headerName: 'Type',
cellRenderer: TypeCellRenderer,
width: 120,
filter: 'agTextColumnFilter',
},
{
field: 'default',
headerName: 'Default',
cellRenderer: DefaultCellRenderer,
width: 200,
filter: 'agTextColumnFilter',
},
{
field: 'category',
headerName: 'Category',
width: 120,
filter: 'agTextColumnFilter',
floatingFilter: true,
},
{
field: 'group',
headerName: 'Group',
cellRenderer: GroupCellRenderer,
width: 180,
filter: 'agTextColumnFilter',
floatingFilter: true,
},
];
if (showEnvironmentVariables) {
columns.push({
field: 'env_var',
headerName: 'Environment Variable',
width: 250,
filter: 'agTextColumnFilter',
cellRenderer: (props: { value: string }) => (
<code>{props.value}</code>
),
});
}
columns.push(
{
field: 'external',
headerName: 'External',
cellRenderer: BooleanCellRenderer,
width: 100,
filter: true,
},
);
return columns;
}, [showEnvironmentVariables]);
const defaultColDef = useMemo<ColDef>(() => ({
sortable: true,
resizable: true,
}), []);
const onGridReady = useCallback((params: GridReadyEvent) => {
setGridApi(params.api);
}, []);
const onFilterTextBoxChanged = useCallback(() => {
if (gridApi) {
gridApi.setGridOption('quickFilterText', searchText);
}
}, [gridApi, searchText]);
const exportToCsv = useCallback(() => {
if (gridApi) {
gridApi.exportDataAsCsv({
fileName: 'superset_configuration.csv',
});
}
}, [gridApi]);
return (
<div style={{ width: '100%', height: '800px' }}>
{/* Controls */}
<div style={{ marginBottom: '20px', display: 'flex', gap: '15px', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<input
type="text"
placeholder="Quick filter across all columns..."
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
onFilterTextBoxChanged();
}}
style={{
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
width: '100%',
maxWidth: '400px',
}}
/>
</div>
<button
onClick={exportToCsv}
style={{
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
}}
>
Export to CSV
</button>
<div style={{ color: '#666' }}>
{rowData.length} configurations
</div>
</div>
{/* AG Grid */}
<div className="ag-theme-material" style={{ height: '100%', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onGridReady={onGridReady}
animateRows={true}
enableCellTextSelection={true}
ensureDomOrder={true}
tooltipShowDelay={500}
pagination={true}
paginationPageSize={50}
paginationPageSizeSelector={[20, 50, 100, 200]}
/>
</div>
{/* Help text */}
<div style={{ marginTop: '15px', color: '#666' }}>
<p>
<strong>Tips:</strong> Click column headers to sort. Use the filter row below headers for column-specific filtering.
Hold Shift to sort by multiple columns. Right-click headers for more options.
</p>
</div>
</div>
);
};
export default ConfigurationTable;

View File

@@ -1,181 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import configMetadata from '../resources/config_metadata.json';
interface EnvironmentVariablesExampleProps {
category?: string;
}
const EnvironmentVariablesExample: React.FC<
EnvironmentVariablesExampleProps
> = ({ category }) => {
const [showAll, setShowAll] = useState(false);
// Get settings based on category
const getSettings = () => {
if (category && configMetadata.by_category[category]) {
return configMetadata.by_category[category];
}
return configMetadata.all_settings;
};
const settings = getSettings();
const displaySettings = showAll ? settings : settings.slice(0, 5);
const formatDefaultForEnv = (value: unknown): string => {
if (value === null || value === undefined) return '""';
if (typeof value === 'object') {
return `'${JSON.stringify(value)}'`;
}
if (typeof value === 'string') {
return `"${value}"`;
}
return String(value);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const generateEnvExample = (setting: { default: unknown; env_var: string }): string => {
const example = formatDefaultForEnv(setting.default);
return `export ${setting.env_var}=${example}`;
};
const generateAllEnvVars = (): string => {
return [
'# Superset Configuration Environment Variables',
'# Generated from configuration metadata',
'',
...displaySettings.map(setting =>
[
`# ${setting.title}`,
`# ${setting.description}`,
`# Type: ${setting.type}`,
`# Impact: ${setting.impact}${
setting.requires_restart ? ' (requires restart)' : ''
}`,
generateEnvExample(setting),
'',
].join('\n'),
),
].join('\n');
};
return (
<div style={{ margin: '20px 0' }}>
<div
style={{
backgroundColor: '#f6f8fa',
border: '1px solid #e1e4e8',
borderRadius: '6px',
padding: '16px',
position: 'relative',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px',
}}
>
<h4 style={{ margin: 0, color: '#24292e' }}>
Environment Variables {category && `(${category})`}
</h4>
<button
onClick={() => copyToClipboard(generateAllEnvVars())}
style={{
backgroundColor: '#0366d6',
color: 'white',
border: 'none',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
}}
title="Copy all environment variables"
>
📋 Copy All
</button>
</div>
<pre
style={{
backgroundColor: '#f6f8fa',
border: 'none',
padding: '0',
margin: '0',
fontFamily:
'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace',
fontSize: '12px',
lineHeight: '1.45',
overflow: 'auto',
maxHeight: '400px',
}}
>
<code>{generateAllEnvVars()}</code>
</pre>
{!showAll && settings.length > 5 && (
<div
style={{
textAlign: 'center',
marginTop: '10px',
borderTop: '1px solid #e1e4e8',
paddingTop: '10px',
}}
>
<button
onClick={() => setShowAll(true)}
style={{
backgroundColor: 'transparent',
border: '1px solid #0366d6',
color: '#0366d6',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
}}
>
Show all {settings.length} settings
</button>
</div>
)}
</div>
<div
style={{
marginTop: '10px',
fontSize: '14px',
color: '#586069',
}}
>
<strong>Usage:</strong> Save to a <code>.env</code> file or export
directly in your shell.
{category && ` Showing ${settings.length} ${category} settings.`}
</div>
</div>
);
};
export default EnvironmentVariablesExample;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3993,26 +3993,6 @@ address@^1.0.1:
resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==
ag-charts-types@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/ag-charts-types/-/ag-charts-types-12.1.0.tgz#75104b90e5f6ae01b7248ec3f6a8dabc65c3cbb6"
integrity sha512-qeODwJ1EqKjpwEbp0mQ2wQ0arRNYaZo2BafdAGfcuOwjOBlagSwJvUg5MCvAYZ/W/mg2uEmt7jKMNfDy4ul4+Q==
ag-grid-community@34.1.0, ag-grid-community@^34.1.0:
version "34.1.0"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-34.1.0.tgz#6356562b3a544a50bbab6a3d0929029567bbd7bc"
integrity sha512-3rZiOyyCGqSNqqTsrWafDVj1WfK43jfb53Ka5sqzdOG/yu6ySUFmdc0h/OuGLnkzwW5PC29coQwbS2rkb4c9dA==
dependencies:
ag-charts-types "12.1.0"
ag-grid-react@^34.1.0:
version "34.1.0"
resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-34.1.0.tgz#9d89a75f5994a5187cbdf1f44132eb06c2741623"
integrity sha512-CY1p4/JnvcwOt2HipmsqME9CWz7M21nb3OB1DhJGOvNUaxo1wF6Hb/pKpa20F3F/E93wQCNmCz+gfrSLPuJrQw==
dependencies:
ag-grid-community "34.1.0"
prop-types "^15.8.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -4340,12 +4320,12 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-loader@^9.2.1:
@@ -6673,7 +6653,7 @@ form-data-encoder@^2.1.2:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.4:
form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
@@ -7240,11 +7220,6 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-url-attributes@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87"
integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
@@ -10912,23 +10887,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@types/react" "*"
react-markdown@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca"
integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
hast-util-to-jsx-runtime "^2.0.0"
html-url-attributes "^3.0.0"
mdast-util-to-hast "^13.0.0"
remark-parse "^11.0.0"
remark-rehype "^11.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-redux@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,291 +0,0 @@
#!/usr/bin/env python3
"""
Extract configuration schema from config_defaults.py.
This script parses the existing config_defaults.py file and extracts:
- All configuration keys and their default values
- Comments above each key as descriptions
- Types inferred from the default values
The output is a comprehensive JSON schema that can be used for:
- Documentation generation
- Configuration validation
- IDE autocomplete
"""
import ast
import json
import sys
from pathlib import Path
from typing import Any, Dict, List
# Import the complex object handlers
sys.path.append(str(Path(__file__).parent.parent))
try:
from superset.config_objects import (
get_default_for_complex_object,
get_fully_qualified_type,
get_object_import_info,
is_complex_object,
)
except ImportError:
# Fallback if import fails
def get_default_for_complex_object(key: str) -> tuple[Any, str]:
return f"<Complex object: {key}>", "unknown"
def is_complex_object(key: str) -> bool:
return False
def get_fully_qualified_type(obj: Any) -> str:
return type(obj).__name__
def get_object_import_info(obj: Any) -> dict[str, Any]:
return {
"module": None,
"name": str(type(obj).__name__),
"import_statement": None,
}
def infer_type(value: Any) -> str:
"""Infer the configuration type from the default value."""
if value is None:
return "null"
elif isinstance(value, bool):
return "boolean"
elif isinstance(value, int):
return "integer"
elif isinstance(value, float):
return "number"
elif isinstance(value, str):
return "string"
elif isinstance(value, (list, tuple)):
return "array"
elif isinstance(value, dict):
return "object"
else:
return "unknown"
def extract_comments_before_line(lines: List[str], line_num: int) -> List[str]:
"""Extract comments immediately before a configuration line."""
comments: List[str] = []
current_line = line_num - 2 # line_num is 1-based, so -2 to get previous line
# Look backwards for comments, but only go back a few lines to avoid
# picking up unrelated comments
max_lookback = min(5, current_line + 1)
for i in range(max_lookback):
if current_line - i < 0:
break
line = lines[current_line - i].strip()
if line.startswith("#"):
# Remove the '#' and clean up the comment
comment = line[1:].strip()
if comment: # Only add non-empty comments
comments.insert(0, comment)
elif line == "":
# Empty line - continue looking
continue
else:
# Non-comment, non-empty line - stop looking
break
return comments
def safe_eval(node: ast.AST) -> Any: # noqa: C901
"""Safely evaluate an AST node to get its value."""
try:
# Handle basic constant values
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.Num): # Python < 3.8
return node.n
elif isinstance(node, ast.Str): # Python < 3.8
return node.s
elif isinstance(node, ast.List):
return [safe_eval(item) for item in node.elts]
elif isinstance(node, ast.Dict):
return {
safe_eval(k): safe_eval(v)
for k, v in zip(node.keys, node.values, strict=False)
if k is not None
}
elif isinstance(node, ast.Name):
# Handle common constants
if node.id in ("True", "False", "None"):
return {"True": True, "False": False, "None": None}[node.id]
else:
return f"<{node.id}>" # Placeholder for variables
elif isinstance(node, ast.Call):
# Handle function calls - try to identify the function being called
if isinstance(node.func, ast.Name):
func_name = node.func.id
if func_name in ("int", "float", "str", "bool"):
# Handle type constructors
if node.args:
arg_val = safe_eval(node.args[0])
if isinstance(arg_val, (int, float, str, bool)):
try:
return eval(func_name)(arg_val) # noqa: S307
except Exception:
return f"<{func_name}()>"
return f"<{func_name}()>"
elif func_name == "timedelta":
# Handle timedelta calls
return "<timedelta()>"
else:
return f"<{func_name}()>"
elif isinstance(node.func, ast.Attribute):
# Handle method calls like obj.method()
method_name = (
ast.unparse(node.func) if hasattr(ast, "unparse") else "method_call"
)
return f"<{method_name}()>"
else:
return "<function_call>"
elif isinstance(node, ast.Attribute):
# Handle attribute access like obj.attr
try:
attr_str = ast.unparse(node) if hasattr(ast, "unparse") else "attribute"
return f"<{attr_str}>"
except Exception:
return "<attribute>"
else:
# For everything else, just return a descriptive placeholder
return f"<{type(node).__name__}>"
except Exception:
return "<unknown>"
def extract_config_schema(config_file: Path) -> Dict[str, Any]:
"""Extract configuration schema from config_defaults.py."""
with open(config_file, "r") as f:
content = f.read()
lines = content.splitlines()
# Parse the Python file
tree = ast.parse(content)
schema = {}
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
# Check if this is a simple assignment to a variable
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
var_name = node.targets[0].id
# Only include uppercase variables (configuration convention)
if var_name.isupper():
# Get the default value
default_value = safe_eval(node.value)
# Check if this is a complex object
if is_complex_object(var_name):
# Get the proper default value and type for complex objects
default_value, type_name = get_default_for_complex_object(
var_name
)
config_type = type_name
else:
# Infer type from default value
config_type = infer_type(default_value)
# Get comments before this line
comments = extract_comments_before_line(lines, node.lineno)
description = " ".join(comments) if comments else ""
# Determine category based on variable name patterns
category = categorize_config(var_name)
schema[var_name] = {
"type": config_type,
"default": default_value,
"description": description,
"category": category,
}
# Add additional metadata for complex objects
if is_complex_object(var_name):
schema[var_name]["is_complex_object"] = True
return schema
def categorize_config(var_name: str) -> str:
"""Categorize configuration variables based on their names."""
name_lower = var_name.lower()
if any(term in name_lower for term in ["limit", "timeout", "cache", "pool"]):
return "performance"
elif any(term in name_lower for term in ["feature", "flag", "enable", "disable"]):
return "features"
elif any(term in name_lower for term in ["theme", "color", "style", "ui"]):
return "ui"
elif any(term in name_lower for term in ["db", "database", "sql", "query"]):
return "database"
elif any(term in name_lower for term in ["auth", "security", "login", "oauth"]):
return "security"
elif any(term in name_lower for term in ["log", "debug", "stats"]):
return "logging"
elif any(term in name_lower for term in ["mail", "smtp", "email"]):
return "email"
elif any(term in name_lower for term in ["celery", "async", "worker"]):
return "async"
else:
return "general"
def main() -> None:
"""Extract configuration schema and save to JSON."""
superset_root = Path(__file__).parent.parent
config_file = superset_root / "superset" / "config_defaults.py"
if not config_file.exists():
print(f"Error: {config_file} not found")
sys.exit(1)
print("Extracting configuration schema...")
schema = extract_config_schema(config_file)
# Create output structure
output = {
"metadata": {
"generated_from": str(config_file),
"total_configs": len(schema),
"description": (
"Superset configuration schema extracted from config_defaults.py"
),
},
"configs": schema,
"by_category": {},
}
# Group by category
for key, config in schema.items():
category = config["category"]
if category not in output["by_category"]:
output["by_category"][category] = {}
output["by_category"][category][key] = config
# Save to JSON
output_file = superset_root / "superset" / "config_schema.json"
with open(output_file, "w") as f:
json.dump(output, f, indent=2, default=str)
print("✅ Schema extracted successfully!")
print(f"📊 Total configurations: {len(schema)}")
print(f"📂 Categories: {list(output['by_category'].keys())}")
print(f"💾 Saved to: {output_file}")
# Show some stats
print("\n📈 Category breakdown:")
for category, configs in output["by_category"].items():
print(f" {category}: {len(configs)} configs")
if __name__ == "__main__":
main()

View File

@@ -1,324 +0,0 @@
#!/usr/bin/env python3
# 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.
"""Extract configuration types from runtime inspection of config.py.
This script imports the actual config module and extracts type information
through runtime introspection, providing more accurate type data than
static analysis.
"""
import ast
import inspect
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
# Add superset to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def get_source_comment(module_path: str, var_name: str) -> Optional[str]:
"""Extract comment from source code for a variable."""
try:
with open(module_path, "r") as f:
content = f.read()
tree = ast.parse(content)
lines = content.splitlines()
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
if node.targets[0].id == var_name:
# Look for comments above this line
line_num = node.lineno - 1 # Convert to 0-based
comments = []
# Look backwards for comments
for i in range(min(5, line_num)):
check_line = line_num - i - 1
if check_line < 0:
break
line = lines[check_line].strip()
if line.startswith("#"):
comment = line[1:].strip()
if comment:
comments.insert(0, comment)
elif line and not line.startswith("#"):
break
return " ".join(comments) if comments else None
return None
except Exception:
return None
def analyze_value(value: Any) -> Dict[str, Any]:
"""Analyze a configuration value to extract type information."""
analysis = {
"python_type": type(value),
"type_name": type(value).__name__,
"module": getattr(type(value), "__module__", None),
"is_callable": callable(value),
"is_none": value is None,
}
# Basic type categorization
if value is None:
analysis["category"] = "null"
elif isinstance(value, bool):
analysis["category"] = "boolean"
elif isinstance(value, int):
analysis["category"] = "integer"
elif isinstance(value, float):
analysis["category"] = "number"
elif isinstance(value, str):
analysis["category"] = "string"
elif isinstance(value, (list, tuple)):
analysis["category"] = "array"
# Sample item types
if value:
item_types = list(set(type(item).__name__ for item in value[:5]))
analysis["item_types"] = item_types
elif isinstance(value, dict):
analysis["category"] = "object"
# Sample key/value types
if value:
keys = list(value.keys())[:5]
key_types = list(set(type(k).__name__ for k in keys))
val_types = list(set(type(value[k]).__name__ for k in keys))
analysis["key_types"] = key_types
analysis["value_types"] = val_types
elif callable(value):
analysis["category"] = "function"
try:
analysis["signature"] = str(inspect.signature(value))
except Exception:
pass
else:
analysis["category"] = "object"
analysis["class_name"] = f"{type(value).__module__}.{type(value).__name__}"
# Serialization check
try:
import json
json.dumps(value)
analysis["serializable"] = True
except Exception:
analysis["serializable"] = False
return analysis
def categorize_config_key(key: str) -> str:
"""Categorize a configuration key based on its name."""
key_lower = key.lower()
if any(
term in key_lower
for term in ["secret", "key", "password", "auth", "oauth", "login"]
):
return "security"
elif any(
term in key_lower for term in ["db", "database", "sql", "query", "engine"]
):
return "database"
elif any(
term in key_lower for term in ["limit", "timeout", "cache", "pool", "async"]
):
return "performance"
elif any(term in key_lower for term in ["feature", "flag", "enable", "disable"]):
return "features"
elif any(
term in key_lower for term in ["theme", "color", "style", "ui", "frontend"]
):
return "ui"
elif any(term in key_lower for term in ["log", "debug", "stats", "event"]):
return "logging"
elif any(term in key_lower for term in ["mail", "smtp", "email"]):
return "email"
elif any(term in key_lower for term in ["celery", "worker", "beat", "task"]):
return "async"
else:
return "general"
def extract_config_types() -> Dict[str, Any]:
"""Extract type information from the config module."""
try:
# Import the config module
from superset import config
# Get module path for comment extraction
config_path = inspect.getfile(config)
results = {}
# Get all uppercase attributes (configuration convention)
for name in dir(config):
if name.isupper() and not name.startswith("_"):
value = getattr(config, name)
# Analyze the value
analysis = analyze_value(value)
# Get source comment
comment = get_source_comment(config_path, name)
# Categorize
category = categorize_config_key(name)
results[name] = {
"key": name,
"value_analysis": analysis,
"description": comment,
"category": category,
"current_value": value
if analysis.get("serializable")
else f"<{analysis['type_name']} instance>",
}
return results
except ImportError as e:
print(f"Error importing config: {e}")
return {}
def compare_with_metadata() -> Dict[str, Any]:
"""Compare runtime config with defined metadata."""
from superset.config_metadata import CONFIG_METADATA
runtime_configs = extract_config_types()
comparison = {
"in_metadata_only": [],
"in_runtime_only": [],
"type_mismatches": [],
"matching": [],
}
metadata_keys = set(CONFIG_METADATA.keys())
runtime_keys = set(runtime_configs.keys())
# Keys only in metadata
comparison["in_metadata_only"] = sorted(metadata_keys - runtime_keys)
# Keys only in runtime
comparison["in_runtime_only"] = sorted(runtime_keys - metadata_keys)
# Check for type mismatches
for key in metadata_keys & runtime_keys:
metadata_type = CONFIG_METADATA[key].type
runtime_type = runtime_configs[key]["value_analysis"]["python_type"]
if metadata_type != runtime_type:
comparison["type_mismatches"].append(
{
"key": key,
"metadata_type": str(metadata_type),
"runtime_type": str(runtime_type),
}
)
else:
comparison["matching"].append(key)
return comparison
def suggest_metadata_entries() -> List[str]:
"""Suggest metadata entries for configs not yet documented."""
runtime_configs = extract_config_types()
from superset.config_metadata import CONFIG_METADATA
suggestions = []
for key, info in runtime_configs.items():
if key not in CONFIG_METADATA:
analysis = info["value_analysis"]
# Build suggested metadata entry
suggestion = f""" "{key}": ConfigMetadata(
key="{key}",
type={analysis["type_name"]},
default={repr(info["current_value"]) if analysis["serializable"] else f"{analysis['type_name']}()"},
description="{info.get("description", "TODO: Add description")}",
category="{info["category"]}",
impact="medium",
requires_restart={"True" if info["category"] in ["security", "database"] else "False"},"""
if analysis["category"] == "integer":
suggestion += "\n min_value=1,"
if not analysis["serializable"]:
suggestion += f'\n serializable=False,\n doc_default="<{analysis["type_name"]} instance>",'
suggestion += "\n ),"
suggestions.append(suggestion)
return suggestions
def main():
"""Main function to run type extraction."""
print("Extracting configuration types from runtime...")
# Extract types
runtime_configs = extract_config_types()
print(f"Found {len(runtime_configs)} configuration variables")
# Compare with metadata
print("\nComparing with defined metadata...")
comparison = compare_with_metadata()
print(f" - Matching: {len(comparison['matching'])}")
print(f" - Only in metadata: {len(comparison['in_metadata_only'])}")
print(f" - Only in runtime: {len(comparison['in_runtime_only'])}")
print(f" - Type mismatches: {len(comparison['type_mismatches'])}")
if comparison["in_runtime_only"]:
print(f"\nConfigs missing metadata: {len(comparison['in_runtime_only'])}")
print("Generating suggestions...")
suggestions = suggest_metadata_entries()
# Save suggestions to file
output_file = Path(__file__).parent / "suggested_metadata.py"
with open(output_file, "w") as f:
f.write("# Suggested metadata entries for undocumented configs\n\n")
f.write("\n\n".join(suggestions))
print(f"Suggestions saved to: {output_file}")
# Show type distribution
type_dist = {}
for config in runtime_configs.values():
cat = config["value_analysis"]["category"]
type_dist[cat] = type_dist.get(cat, 0) + 1
print("\nType distribution:")
for cat, count in sorted(type_dist.items()):
print(f" {cat}: {count}")
if __name__ == "__main__":
main()

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -54,7 +54,7 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
interceptV1ChartData();
}
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
.should('be.visible')
.find("[role='menu'] [role='menuitem']")
.contains(/^Drill by$/)
@@ -65,16 +65,11 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
)
.should('be.visible')
.find('[role="menuitem"]')
.contains(new RegExp(`^${targetDrillByColumn}$`))
.click();
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).should('not.exist');
.then($el => {
cy.wrap($el)
.contains(new RegExp(`^${targetDrillByColumn}$`))
.trigger('keydown', { keyCode: 13, which: 13, force: true });
});
if (isLegacy) {
return cy.wait('@legacyData');
@@ -245,7 +240,7 @@ describe('Drill by modal', () => {
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
it.only('opens the modal from the context menu', () => {
it('opens the modal from the context menu', () => {
openTableContextMenu('boy');
drillBy('state').then(intercepted => {
verifyExpectedFormData(intercepted, {
@@ -534,7 +529,7 @@ describe('Drill by modal', () => {
]);
});
it.skip('Bar Chart', () => {
it('Bar Chart', () => {
testEchart('echarts_timeseries_bar', 'Bar Chart', [
[85, 94],
[490, 68],
@@ -617,7 +612,7 @@ describe('Drill by modal', () => {
]);
});
it.skip('Mixed Chart', () => {
it('Mixed Chart', () => {
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
// click 'boy'
cy.wrap($canvas).scrollIntoView();

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
"type": "tsc --noEmit",
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
"validate-release": "../RELEASING/validate_this_release.sh"
},
"browserslist": [
@@ -121,8 +121,8 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"ag-grid-community": "33.1.1",
"ag-grid-react": "33.1.1",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
@@ -145,6 +145,7 @@
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^130.0.0",
"html-webpack-plugin": "^5.6.3",
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
@@ -316,7 +317,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.6.2",
"prettier": "3.5.3",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -349,7 +350,7 @@
"regenerator-runtime": "^0.14.1"
},
"engines": {
"node": "^20.18.1",
"node": "^20.16.0",
"npm": "^10.8.1"
},
"overrides": {

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ const controlsWithoutXAxis: ControlSetRow[] = [
['groupby'],
[contributionModeControl],
['adhoc_filters'],
['limit', 'group_others_when_limit_reached'],
['limit'],
['timeseries_limit_metric'],
['order_desc'],
['row_limit'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,75 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {
it('returns empty array for valid JSON', () => {
const { result } = renderHook(() => useJsonValidation('{"key": "value"}'));
expect(result.current).toEqual([]);
});
it('returns empty array when disabled', () => {
const { result } = renderHook(() =>
useJsonValidation('invalid json', { enabled: false }),
);
expect(result.current).toEqual([]);
});
it('returns empty array for empty input', () => {
const { result } = renderHook(() => useJsonValidation(''));
expect(result.current).toEqual([]);
});
it('extracts line and column from error message with parentheses', () => {
// Since we can't control the exact error message from JSON.parse,
// let's test with a mock that demonstrates the pattern matching
const mockError = {
message:
"Expected ',' or '}' after property value in JSON at position 19 (line 3 column 2)",
};
// Test the regex pattern directly
const match = mockError.message.match(/\(line (\d+) column (\d+)\)/);
expect(match).toBeTruthy();
expect(match![1]).toBe('3');
expect(match![2]).toBe('2');
});
it('returns error on first line when no line/column info in message', () => {
const invalidJson = '{invalid}';
const { result } = renderHook(() => useJsonValidation(invalidJson));
expect(result.current).toHaveLength(1);
expect(result.current[0]).toMatchObject({
type: 'error',
row: 0,
column: 0,
text: expect.stringContaining('Invalid JSON'),
});
});
it('uses custom error prefix', () => {
const { result } = renderHook(() =>
useJsonValidation('{invalid}', { errorPrefix: 'Custom error' }),
);
expect(result.current[0].text).toContain('Custom error');
});
});

View File

@@ -1,82 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo } from 'react';
export interface JsonValidationAnnotation {
type: 'error' | 'warning' | 'info';
row: number;
column: number;
text: string;
}
export interface UseJsonValidationOptions {
/** Whether to enable JSON validation. Default: true */
enabled?: boolean;
/** Custom error message prefix. Default: 'Invalid JSON' */
errorPrefix?: string;
}
/**
* Hook for JSON validation that returns AceEditor-compatible annotations.
* Based on the SQL Lab validation pattern.
*
* @param jsonValue - The JSON string to validate
* @param options - Validation options
* @returns Array of annotation objects for AceEditor
*/
export function useJsonValidation(
jsonValue?: string,
options: UseJsonValidationOptions = {},
): JsonValidationAnnotation[] {
const { enabled = true, errorPrefix = 'Invalid JSON' } = options;
return useMemo(() => {
// Skip validation if disabled or empty value
if (!enabled || !jsonValue?.trim()) {
return [];
}
try {
JSON.parse(jsonValue);
return []; // Valid JSON - no annotations
} catch (error: any) {
const errorMessage = error.message || 'syntax error';
// Try to extract line/column from error message
// Look for pattern: (line X column Y) - often at the end of error messages
let row = 0;
let column = 0;
const match = errorMessage.match(/\(line (\d+) column (\d+)\)/);
if (match) {
row = parseInt(match[1], 10) - 1; // Convert to 0-based
column = parseInt(match[2], 10) - 1;
}
return [
{
type: 'error' as const,
row,
column,
text: `${errorPrefix}: ${errorMessage}`,
},
];
}
}, [enabled, jsonValue, errorPrefix]);
}

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { Children, ReactElement, Fragment } from 'react';
import cx from 'classnames';
import { Button as AntdButton } from 'antd';
import { useTheme } from '@superset-ui/core';
@@ -87,7 +88,6 @@ export function Button(props: ButtonProps) {
const element = children as ReactElement;
let renderedChildren = [];
if (element && element.type === Fragment) {
renderedChildren = Children.toArray(element.props.children);
} else {
@@ -118,7 +118,7 @@ export function Button(props: ButtonProps) {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1,
lineHeight: 1.5715,
fontSize: fontSizeSM,
fontWeight: fontWeightStrong,
height,

View File

@@ -20,7 +20,7 @@ import { useEffect, useState } from 'react';
import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night';
import { useTheme, isThemeDark } from '@superset-ui/core';
import { themeObject } from '@superset-ui/core';
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
@@ -77,7 +77,6 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
wrapLines = true,
style: overrideStyle,
}) => {
const theme = useTheme();
const [isLanguageReady, setIsLanguageReady] = useState(
registeredLanguages.has(language),
);
@@ -93,14 +92,14 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
loadLanguage();
}, [language]);
const isDark = isThemeDark(theme);
const isDark = themeObject.isThemeDark();
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
const defaultCustomStyle: React.CSSProperties = {
background: theme.colorBgElevated,
padding: theme.sizeUnit * 4,
background: themeObject.theme.colorBgElevated,
padding: themeObject.theme.sizeUnit * 4,
border: 0,
borderRadius: theme.borderRadius,
borderRadius: themeObject.theme.borderRadius,
...customStyle,
};

View File

@@ -15,7 +15,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useTheme, css } from '@superset-ui/core';
import { useTheme } from '@superset-ui/core';
import { Typography } from '@superset-ui/core/components/Typography';
import { Icons } from '@superset-ui/core/components';
@@ -29,7 +29,7 @@ interface CollapseLabelInModalProps {
export const CollapseLabelInModal: React.FC<CollapseLabelInModalProps> = ({
title,
subtitle,
validateCheckStatus,
validateCheckStatus = false,
testId,
}) => {
const theme = useTheme();
@@ -37,38 +37,36 @@ export const CollapseLabelInModal: React.FC<CollapseLabelInModalProps> = ({
return (
<div data-test={testId}>
<Typography.Title
css={css`
&& {
margin-top: 0;
margin-bottom: ${theme.sizeUnit / 2}px;
font-size: ${theme.fontSizeLG}px;
}
`}
style={{
marginTop: 0,
marginBottom: theme.sizeUnit / 2,
fontSize: theme.fontSizeLG,
}}
>
{title}{' '}
{validateCheckStatus !== undefined &&
(validateCheckStatus ? (
<Icons.CheckCircleOutlined
iconColor={theme.colorSuccess}
aria-label="check-circle"
/>
) : (
<span
css={css`
color: ${theme.colorErrorText};
font-size: ${theme.fontSizeLG}px;
`}
>
*
</span>
))}
{validateCheckStatus ? (
<Icons.CheckCircleOutlined
style={{ color: theme.colorSuccess }}
aria-label="check-circle"
role="img"
/>
) : (
<span
style={{
color: theme.colorErrorText,
fontSize: theme.fontSizeLG,
}}
>
*
</span>
)}
</Typography.Title>
<Typography.Paragraph
css={css`
margin: 0;
font-size: ${theme.fontSizeSM}px;
color: ${theme.colorTextDescription};
`}
style={{
margin: 0,
fontSize: theme.fontSizeSM,
color: theme.colorTextDescription,
}}
>
{subtitle}
</Typography.Paragraph>

View File

@@ -68,7 +68,6 @@ export function ConfirmStatusChange({
onConfirm={confirm}
onHide={hide}
open={open}
name="please confirm"
title={title}
/>
</>

View File

@@ -37,7 +37,6 @@ export function DeleteModal({
onHide,
open,
title,
name,
}: DeleteModalProps) {
const [disableChange, setDisableChange] = useState(true);
const [confirmation, setConfirmation] = useState<string>('');
@@ -79,7 +78,6 @@ export function DeleteModal({
primaryButtonName={t('Delete')}
primaryButtonStyle="danger"
show={open}
name={name}
title={title}
centered
>

View File

@@ -25,5 +25,4 @@ export interface DeleteModalProps {
onHide: () => void;
open: boolean;
title: ReactNode;
name?: string;
}

View File

@@ -128,7 +128,7 @@ const ImageContainer = ({
<Empty
description={false}
image={mappedImage}
styles={{ image: getImageHeight(size) }}
imageStyle={getImageHeight(size)}
/>
</div>
);
@@ -144,7 +144,6 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
description = t('There is currently no information to display.'),
image = 'empty.svg',
buttonText,
buttonIcon,
buttonAction,
size = 'medium',
children,
@@ -166,7 +165,6 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
)}
{buttonText && buttonAction && (
<Button
icon={buttonIcon}
buttonStyle="primary"
onClick={buttonAction}
onMouseDown={handleMouseDown}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { ReactNode, SyntheticEvent } from 'react';
import type { IconType } from '@superset-ui/core/components';
export type EmptyStateSize = 'small' | 'medium' | 'large';
@@ -26,7 +25,6 @@ export type EmptyStateProps = {
description?: ReactNode;
image?: ReactNode | string;
buttonText?: ReactNode;
buttonIcon?: IconType;
buttonAction?: (event: SyntheticEvent) => void;
size?: EmptyStateSize;
children?: ReactNode;

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { css, useTheme, getFontSize } from '../..';
import { css, useTheme, themeObject } from '../..';
import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
const genAriaLabel = (fileName: string) => {
@@ -52,7 +52,7 @@ export const BaseIconComponent: React.FC<
const style = {
color: iconColor,
fontSize: iconSize
? `${getFontSize(theme, iconSize)}px`
? `${themeObject.getFontSize(iconSize)}px`
: `${theme.fontSize}px`,
cursor: rest?.onClick ? 'pointer' : undefined,
};
@@ -76,12 +76,12 @@ export const BaseIconComponent: React.FC<
style={style}
width={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
? `${themeObject.getFontSize(iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
height={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
? `${themeObject.getFontSize(iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
{...(rest as CustomIconType)}

View File

@@ -19,7 +19,7 @@
import { KeyboardEvent, useMemo } from 'react';
import { SerializedStyles, CSSObject } from '@emotion/react';
import { kebabCase } from 'lodash';
import { css, t, useTheme, getFontSize } from '@superset-ui/core';
import { css, t, useTheme, themeObject } from '@superset-ui/core';
import {
CloseCircleOutlined,
InfoCircleOutlined,
@@ -68,7 +68,7 @@ export const InfoTooltip = ({
const iconCss = css`
color: ${variant?.color ?? theme.colorIcon};
font-size: ${getFontSize(theme, iconSize)}px;
font-size: ${themeObject.getFontSize(iconSize)}px;
`;
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {

View File

@@ -18,7 +18,7 @@
*/
import { Tag } from '@superset-ui/core/components/Tag';
import { css } from '@emotion/react';
import { useTheme, getColorVariants } from '@superset-ui/core';
import { useTheme, themeObject } from '@superset-ui/core';
import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
import { PublishedLabel } from './reusable/PublishedLabel';
import type { LabelProps } from './types';
@@ -37,7 +37,7 @@ export function Label(props: LabelProps) {
...rest
} = props;
const baseColor = getColorVariants(theme, type);
const baseColor = themeObject.getColorVariants(type);
const color = baseColor.active;
const borderColor = baseColor.border;
const backgroundColor = baseColor.bg;

View File

@@ -18,7 +18,7 @@
*/
import { useEffect, useState, FunctionComponent } from 'react';
import { t, styled, css, useTheme } from '@superset-ui/core';
import { t, styled, css } from '@superset-ui/core';
import dayjs from 'dayjs';
import { extendedDayjs } from '../../utils/dates';
import { Icons } from '../Icons';
@@ -38,14 +38,24 @@ extendedDayjs.updateLocale('en', {
});
const TextStyles = styled.span`
color: ${({ theme }) => theme.colorText};
color: ${({ theme }) => theme.colors.grayscale.base};
`;
const RefreshIcon = styled(Icons.SyncOutlined)`
${({ theme }) => `
width: auto;
height: ${theme.sizeUnit * 5}px;
position: relative;
top: ${theme.sizeUnit}px;
left: ${theme.sizeUnit}px;
cursor: pointer;
`};
`;
export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
updatedAt,
update,
}) => {
const theme = useTheme();
const [timeSince, setTimeSince] = useState<dayjs.Dayjs>(
extendedDayjs(updatedAt),
);
@@ -65,9 +75,10 @@ export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
<TextStyles>
{t('Last Updated %s', timeSince.isValid() ? timeSince.calendar() : '--')}
{update && (
<Icons.SyncOutlined
<RefreshIcon
iconSize="l"
css={css`
margin-left: ${theme.sizeUnit * 2}px;
vertical-align: text-bottom;
`}
onClick={update}
/>

View File

@@ -33,7 +33,6 @@ export function FormModal({
formSubmitHandler,
bodyStyle = {},
requiredFields = [],
name,
}: FormModalProps) {
const [form] = Form.useForm();
const [isSaving, setIsSaving] = useState(false);
@@ -79,7 +78,6 @@ export function FormModal({
return (
<Modal
name={name}
show={show}
title={title}
onHide={handleClose}

View File

@@ -121,8 +121,8 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
.ant-modal-body {
flex: 0 1 auto;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
padding: ${theme.sizeUnit * 4}px;
padding-bottom: ${theme.sizeUnit * 2}px;
overflow: auto;
${!resizable && height && `height: ${height};`}
}
@@ -333,7 +333,7 @@ const CustomModal = ({
}
footer={!hideFooter ? modalFooter : null}
hideFooter={hideFooter}
wrapProps={{ 'data-test': `${name || 'antd'}-modal`, ...wrapProps }}
wrapProps={{ 'data-test': `${name || title}-modal`, ...wrapProps }}
modalRender={modal =>
resizable || draggable ? (
<Draggable

View File

@@ -110,7 +110,6 @@ export const ModalTrigger = forwardRef(
className={className}
show={showModal}
onHide={close}
name={modalTitle}
title={modalTitle}
footer={modalFooter}
hideFooter={!modalFooter}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render } from '@superset-ui/core/spec';
import { TelemetryPixel } from '.';
import TelemetryPixel from '.';
const OLD_ENV = process.env;

View File

@@ -39,7 +39,7 @@ interface TelemetryPixelProps {
const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0';
export const TelemetryPixel = ({
const TelemetryPixel = ({
version = 'unknownVersion',
sha = 'unknownSHA',
build = 'unknownBuild',
@@ -56,3 +56,4 @@ export const TelemetryPixel = ({
/>
);
};
export default TelemetryPixel;

View File

@@ -0,0 +1,150 @@
/**
* 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 { Modal, Tooltip, Flex, Select } from 'antd';
import { Button, JsonEditor } from '@superset-ui/core/components';
import {
themeObject,
exampleThemes,
SerializableThemeConfig,
Theme,
AnyThemeConfig,
} from '@superset-ui/core';
import { useState } from 'react';
import { Icons } from '@superset-ui/core/components/Icons';
interface ThemeEditorProps {
tooltipTitle?: string;
modalTitle?: string;
theme?: Theme;
setTheme?: (config: AnyThemeConfig) => void;
}
const ThemeEditor: React.FC<ThemeEditorProps> = ({
tooltipTitle = 'Edit Theme',
modalTitle = 'Theme Editor',
theme,
setTheme,
}) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const jsonTheme =
JSON.stringify(theme?.toSerializedConfig(), null, 2) || '{}';
const [jsonMetadata, setJsonMetadata] = useState<string>(jsonTheme);
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
// Get theme names for the Select options
const themeOptions: { value: string; label: string }[] = Object.keys(
exampleThemes,
).map(key => ({
value: key,
label: key,
}));
const handleOpenModal = (): void => {
setIsModalOpen(true);
setJsonMetadata(JSON.stringify(theme?.toSerializedConfig(), null, 2));
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
const handleSave = (): void => {
try {
const parsedTheme = JSON.parse(jsonMetadata);
setTheme?.(parsedTheme);
setIsModalOpen(false);
} catch (error) {
console.error('Invalid JSON in theme editor:', error);
alert('Error parsing JSON. Please check your input.');
}
};
const handleThemeChange = (value: string): void => {
setSelectedTheme(value);
// When a theme is selected, update the JSON editor with the theme definition
const themeData = exampleThemes[value] || ({} as SerializableThemeConfig);
setJsonMetadata(JSON.stringify(themeData, null, 2));
};
return (
<>
<Tooltip title={tooltipTitle} placement="bottom">
<Button
buttonStyle="link"
icon={
<Icons.BgColorsOutlined
iconSize="l"
iconColor={themeObject.theme.colorPrimary}
/>
}
onClick={handleOpenModal}
aria-label="Edit theme"
size="large"
/>
</Tooltip>
<Modal
title={modalTitle}
open={isModalOpen}
onCancel={handleCancel}
width={800}
centered
styles={{
body: {
padding: '24px',
},
}}
footer={
<Flex justify="end" gap="small">
<Button onClick={handleCancel} buttonStyle="secondary">
Cancel
</Button>
<Button type="primary" onClick={handleSave}>
Apply Theme
</Button>
</Flex>
}
>
<Flex vertical gap="middle">
<div>
Select a theme template:
<Select
placeholder="Choose a theme"
style={{ width: '100%', marginTop: '8px' }}
options={themeOptions}
onChange={handleThemeChange}
value={selectedTheme}
/>
</div>
<JsonEditor
showLoadingForImport
name="json_metadata"
value={jsonMetadata}
onChange={setJsonMetadata}
tabSize={2}
width="100%"
height="200px"
wrapEnabled
/>
</Flex>
</Modal>
</>
);
};
export default ThemeEditor;

View File

@@ -0,0 +1,79 @@
/**
* 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 { Tooltip } from 'antd';
import { Dropdown, Icons } from '@superset-ui/core/components';
import { t } from '@superset-ui/core';
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
export interface ThemeSelectProps {
setThemeMode: (newMode: ThemeMode) => void;
tooltipTitle?: string;
themeMode: ThemeMode;
}
const ThemeSelect: React.FC<ThemeSelectProps> = ({
setThemeMode,
tooltipTitle = 'Select theme',
themeMode,
}) => {
const handleSelect = (mode: ThemeMode) => {
setThemeMode(mode);
};
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
};
return (
<Tooltip title={tooltipTitle} placement="bottom">
<Dropdown
menu={{
items: [
{
key: ThemeMode.DEFAULT,
label: t('Light'),
onClick: () => handleSelect(ThemeMode.DEFAULT),
icon: <Icons.SunOutlined />,
},
{
key: ThemeMode.DARK,
label: t('Dark'),
onClick: () => handleSelect(ThemeMode.DARK),
icon: <Icons.MoonOutlined />,
},
{
key: ThemeMode.SYSTEM,
label: t('Match system'),
onClick: () => handleSelect(ThemeMode.SYSTEM),
icon: <Icons.FormatPainterOutlined />,
},
],
}}
trigger={['click']}
>
{themeIconMap[themeMode] || <Icons.FormatPainterOutlined />}
</Dropdown>
</Tooltip>
);
};
export default ThemeSelect;

View File

@@ -1,273 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
render,
screen,
userEvent,
waitFor,
within,
} from '@superset-ui/core/spec';
import { ThemeMode } from '@superset-ui/core';
import { Menu } from '@superset-ui/core/components';
import { ThemeSubMenu } from '.';
// Mock the translation function
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
t: (key: string) => key,
}));
describe('ThemeSubMenu', () => {
const defaultProps = {
allowOSPreference: true,
setThemeMode: jest.fn(),
themeMode: ThemeMode.DEFAULT,
hasLocalOverride: false,
onClearLocalSettings: jest.fn(),
};
const renderThemeSubMenu = (props = defaultProps) =>
render(
<Menu>
<ThemeSubMenu {...props} />
</Menu>,
);
const findMenuWithText = async (text: string) => {
await waitFor(() => {
const found = screen
.getAllByRole('menu')
.some(m => within(m).queryByText(text));
if (!found) throw new Error(`Menu with text "${text}" not yet rendered`);
});
return screen.getAllByRole('menu').find(m => within(m).queryByText(text))!;
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders Light and Dark theme options by default', async () => {
renderThemeSubMenu();
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Light');
expect(within(menu!).getByText('Light')).toBeInTheDocument();
expect(within(menu!).getByText('Dark')).toBeInTheDocument();
});
it('does not render Match system option when allowOSPreference is false', async () => {
renderThemeSubMenu({ ...defaultProps, allowOSPreference: false });
userEvent.hover(await screen.findByRole('menuitem'));
await waitFor(() => {
expect(screen.queryByText('Match system')).not.toBeInTheDocument();
});
});
it('renders with allowOSPreference as true by default', async () => {
renderThemeSubMenu();
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Match system');
expect(within(menu).getByText('Match system')).toBeInTheDocument();
});
it('renders clear option when both hasLocalOverride and onClearLocalSettings are provided', async () => {
const mockClear = jest.fn();
renderThemeSubMenu({
...defaultProps,
hasLocalOverride: true,
onClearLocalSettings: mockClear,
});
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Clear local theme');
expect(within(menu).getByText('Clear local theme')).toBeInTheDocument();
});
it('does not render clear option when hasLocalOverride is false', async () => {
const mockClear = jest.fn();
renderThemeSubMenu({
...defaultProps,
hasLocalOverride: false,
onClearLocalSettings: mockClear,
});
userEvent.hover(await screen.findByRole('menuitem'));
await waitFor(() => {
expect(screen.queryByText('Clear local theme')).not.toBeInTheDocument();
});
});
it('calls setThemeMode with DEFAULT when Light is clicked', async () => {
const mockSet = jest.fn();
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Light');
userEvent.click(within(menu).getByText('Light'));
expect(mockSet).toHaveBeenCalledWith(ThemeMode.DEFAULT);
});
it('calls setThemeMode with DARK when Dark is clicked', async () => {
const mockSet = jest.fn();
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Dark');
userEvent.click(within(menu).getByText('Dark'));
expect(mockSet).toHaveBeenCalledWith(ThemeMode.DARK);
});
it('calls setThemeMode with SYSTEM when Match system is clicked', async () => {
const mockSet = jest.fn();
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Match system');
userEvent.click(within(menu).getByText('Match system'));
expect(mockSet).toHaveBeenCalledWith(ThemeMode.SYSTEM);
});
it('calls onClearLocalSettings when Clear local theme is clicked', async () => {
const mockClear = jest.fn();
renderThemeSubMenu({
...defaultProps,
hasLocalOverride: true,
onClearLocalSettings: mockClear,
});
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Clear local theme');
userEvent.click(within(menu).getByText('Clear local theme'));
expect(mockClear).toHaveBeenCalledTimes(1);
});
it('displays sun icon for DEFAULT theme', () => {
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DEFAULT });
expect(screen.getByTestId('sun')).toBeInTheDocument();
});
it('displays moon icon for DARK theme', () => {
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DARK });
expect(screen.getByTestId('moon')).toBeInTheDocument();
});
it('displays format-painter icon for SYSTEM theme', () => {
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.SYSTEM });
expect(screen.getByTestId('format-painter')).toBeInTheDocument();
});
it('displays override icon when hasLocalOverride is true', () => {
renderThemeSubMenu({ ...defaultProps, hasLocalOverride: true });
expect(screen.getByTestId('format-painter')).toBeInTheDocument();
});
it('renders Theme group header', async () => {
renderThemeSubMenu();
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Theme');
expect(within(menu).getByText('Theme')).toBeInTheDocument();
});
it('renders sun icon for Light theme option', async () => {
renderThemeSubMenu();
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Light');
const lightOption = within(menu).getByText('Light').closest('li');
expect(within(lightOption!).getByTestId('sun')).toBeInTheDocument();
});
it('renders moon icon for Dark theme option', async () => {
renderThemeSubMenu();
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Dark');
const darkOption = within(menu).getByText('Dark').closest('li');
expect(within(darkOption!).getByTestId('moon')).toBeInTheDocument();
});
it('renders format-painter icon for Match system option', async () => {
renderThemeSubMenu({ ...defaultProps, allowOSPreference: true });
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Match system');
const matchOption = within(menu).getByText('Match system').closest('li');
expect(
within(matchOption!).getByTestId('format-painter'),
).toBeInTheDocument();
});
it('renders clear icon for Clear local theme option', async () => {
renderThemeSubMenu({
...defaultProps,
hasLocalOverride: true,
onClearLocalSettings: jest.fn(),
});
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Clear local theme');
const clearOption = within(menu)
.getByText('Clear local theme')
.closest('li');
expect(within(clearOption!).getByTestId('clear')).toBeInTheDocument();
});
it('renders divider before clear option when clear option is present', async () => {
renderThemeSubMenu({
...defaultProps,
hasLocalOverride: true,
onClearLocalSettings: jest.fn(),
});
userEvent.hover(await screen.findByRole('menuitem'));
const menu = await findMenuWithText('Clear local theme');
const divider = within(menu).queryByRole('separator');
expect(divider).toBeInTheDocument();
});
it('does not render divider when clear option is not present', async () => {
renderThemeSubMenu({ ...defaultProps });
userEvent.hover(await screen.findByRole('menuitem'));
const divider = document.querySelector('.ant-menu-item-divider');
expect(divider).toBeNull();
});
});

View File

@@ -1,170 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo } from 'react';
import { Icons, Menu } from '@superset-ui/core/components';
import {
css,
styled,
t,
ThemeMode,
useTheme,
ThemeAlgorithm,
} from '@superset-ui/core';
const StyledThemeSubMenu = styled(Menu.SubMenu)`
${({ theme }) => css`
[data-icon='caret-down'] {
color: ${theme.colorIcon};
font-size: ${theme.fontSizeXS}px;
margin-left: ${theme.sizeUnit}px;
}
&.ant-menu-submenu-active {
.ant-menu-title-content {
color: ${theme.colorPrimary};
}
}
`}
`;
const StyledThemeSubMenuItem = styled(Menu.Item)<{ selected: boolean }>`
${({ theme, selected }) => css`
&:hover {
color: ${theme.colorPrimary} !important;
cursor: pointer !important;
}
${selected &&
css`
background-color: ${theme.colors.primary.light4} !important;
color: ${theme.colors.primary.dark1} !important;
`}
`}
`;
export interface ThemeSubMenuOption {
key: ThemeMode;
label: string;
icon: React.ReactNode;
onClick: () => void;
}
export interface ThemeSubMenuProps {
setThemeMode: (newMode: ThemeMode) => void;
themeMode: ThemeMode;
hasLocalOverride?: boolean;
onClearLocalSettings?: () => void;
allowOSPreference?: boolean;
}
export const ThemeSubMenu: React.FC<ThemeSubMenuProps> = ({
setThemeMode,
themeMode,
hasLocalOverride = false,
onClearLocalSettings,
allowOSPreference = true,
}: ThemeSubMenuProps) => {
const theme = useTheme();
const handleSelect = (mode: ThemeMode) => {
setThemeMode(mode);
};
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> =
useMemo(
() => ({
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
}),
[],
);
const selectedThemeModeIcon = useMemo(
() =>
hasLocalOverride ? (
<Icons.FormatPainterOutlined
style={{ color: theme.colors.error.base }}
/>
) : (
themeIconMap[themeMode]
),
[hasLocalOverride, theme.colors.error.base, themeIconMap, themeMode],
);
const themeOptions: ThemeSubMenuOption[] = [
{
key: ThemeMode.DEFAULT,
label: t('Light'),
icon: <Icons.SunOutlined />,
onClick: () => handleSelect(ThemeMode.DEFAULT),
},
{
key: ThemeMode.DARK,
label: t('Dark'),
icon: <Icons.MoonOutlined />,
onClick: () => handleSelect(ThemeMode.DARK),
},
...(allowOSPreference
? [
{
key: ThemeMode.SYSTEM,
label: t('Match system'),
icon: <Icons.FormatPainterOutlined />,
onClick: () => handleSelect(ThemeMode.SYSTEM),
},
]
: []),
];
// Add clear settings option only when there's a local theme active
const clearOption =
onClearLocalSettings && hasLocalOverride
? {
key: 'clear-local',
label: t('Clear local theme'),
icon: <Icons.ClearOutlined />,
onClick: onClearLocalSettings,
}
: null;
return (
<StyledThemeSubMenu
key="theme-sub-menu"
title={selectedThemeModeIcon}
icon={<Icons.CaretDownOutlined iconSize="xs" />}
>
<Menu.ItemGroup title={t('Theme')} />
{themeOptions.map(option => (
<StyledThemeSubMenuItem
key={option.key}
onClick={option.onClick}
selected={option.key === themeMode}
>
{option.icon} {option.label}
</StyledThemeSubMenuItem>
))}
{clearOption && [
<Menu.Divider key="theme-divider" />,
<Menu.Item key={clearOption.key} onClick={clearOption.onClick}>
{clearOption.icon} {clearOption.label}
</Menu.Item>,
]}
</StyledThemeSubMenu>
);
};

View File

@@ -19,7 +19,6 @@
import { styled, css } from '@superset-ui/core';
import { Typography as AntdTypography } from 'antd';
export type { TitleProps } from 'antd/es/typography/Title';
export type { ParagraphProps } from 'antd/es/typography/Paragraph';
const StyledLink = styled(AntdTypography.Link)`

View File

@@ -16,10 +16,52 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/core';
import { Icons, Modal, Typography, Button } from '@superset-ui/core/components';
import { t, styled, css } from '@superset-ui/core';
import { Icons, Modal, Typography } from '@superset-ui/core/components';
import { Button } from '@superset-ui/core/components/Button';
import type { FC, ReactElement } from 'react';
const StyledModalTitle = styled(Typography.Title)`
&& {
font-weight: 600;
margin: 0;
}
`;
const StyledModalBody = styled(Typography.Text)`
${({ theme }) => css`
padding: 0 ${theme.sizeUnit * 2}px;
&& {
margin: 0;
}
`}
`;
const StyledDiscardBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 22}px;
height: ${theme.sizeUnit * 8}px;
`}
`;
const StyledSaveBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 17}px;
height: ${theme.sizeUnit * 8}px;
span > :first-of-type {
margin-right: 0;
}
`}
`;
const StyledWarningIcon = styled(Icons.WarningOutlined)`
${({ theme }) => css`
color: ${theme.colorWarning};
margin-right: ${theme.sizeUnit * 4}px;
`}
`;
export type UnsavedChangesModalProps = {
showModal: boolean;
onHide: () => void;
@@ -44,22 +86,44 @@ export const UnsavedChangesModal: FC<UnsavedChangesModalProps> = ({
show={showModal}
width="444px"
title={
<>
<Icons.WarningOutlined iconSize="m" style={{ marginRight: 8 }} />
{title}
</>
<div
css={css`
align-items: center;
display: flex;
`}
>
<StyledWarningIcon iconSize="xl" />
<StyledModalTitle type="secondary" level={5}>
{title}
</StyledModalTitle>
</div>
}
footer={
<>
<Button buttonStyle="secondary" onClick={onConfirmNavigation}>
<div
css={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
<StyledDiscardBtn
htmlType="button"
buttonSize="small"
onClick={onConfirmNavigation}
>
{t('Discard')}
</Button>
<Button buttonStyle="primary" onClick={handleSave}>
</StyledDiscardBtn>
<StyledSaveBtn
htmlType="button"
buttonSize="small"
buttonStyle="primary"
onClick={handleSave}
>
{t('Save')}
</Button>
</>
</StyledSaveBtn>
</div>
}
>
<Typography.Text>{body}</Typography.Text>
<StyledModalBody type="secondary">{body}</StyledModalBody>
</Modal>
);

View File

@@ -148,7 +148,6 @@ export {
Typography,
type TypographyProps,
type ParagraphProps,
type TitleProps,
} from './Typography';
export { Image, type ImageProps } from './Image';
@@ -164,8 +163,6 @@ export * from './Steps';
export * from './Table';
export * from './TableView';
export * from './Tag';
export * from './TelemetryPixel';
export * from './ThemeSubMenu';
export * from './UnsavedChangesModal';
export * from './constants';
export * from './Result';

View File

@@ -32,7 +32,7 @@ export const CACHE_KEY = '@SUPERSET-UI/CONNECTION';
export const DEFAULT_FETCH_RETRY_OPTIONS: FetchRetryOptions = {
retries: 3,
retryDelay: 1000,
retryOn: [502, 503, 504],
retryOn: [503],
};
export const COMMON_ERR_MESSAGES = {

View File

@@ -63,7 +63,6 @@ export default function buildQueryObject<T extends QueryFormData>(
series_columns,
series_limit,
series_limit_metric,
group_others_when_limit_reached,
...residualFormData
} = formData;
const {
@@ -129,7 +128,6 @@ export default function buildQueryObject<T extends QueryFormData>(
normalizeSeriesLimitMetric(series_limit_metric) ??
timeseries_limit_metric ??
undefined,
group_others_when_limit_reached: group_others_when_limit_reached ?? false,
order_desc: typeof order_desc === 'undefined' ? true : order_desc,
url_params: url_params || undefined,
custom_params,

View File

@@ -189,6 +189,24 @@ describe('Theme', () => {
});
});
describe('getFontSize', () => {
it('returns correct font size for given key', () => {
const theme = Theme.fromConfig();
// Test different font size keys
expect(theme.getFontSize('xs')).toBe('8');
expect(theme.getFontSize('m')).toBeTruthy();
expect(theme.getFontSize('xxl')).toBe('28');
});
it('defaults to medium font size when no key is provided', () => {
const theme = Theme.fromConfig();
const mediumSize = theme.getFontSize('m');
expect(theme.getFontSize()).toBe(mediumSize);
});
});
describe('toSerializedConfig', () => {
it('serializes theme config correctly', () => {
const theme = Theme.fromConfig({

View File

@@ -20,6 +20,7 @@
// eslint-disable-next-line no-restricted-syntax
import React from 'react';
import { theme as antdThemeImport, ConfigProvider } from 'antd';
import tinycolor from 'tinycolor2';
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
/* eslint-disable import/extensions */
@@ -43,7 +44,6 @@ import {
} from '@emotion/react';
import createCache from '@emotion/cache';
import { noop } from 'lodash';
import { isThemeDark } from './utils/themeUtils';
import { GlobalStyles } from './GlobalStyles';
import {
@@ -53,7 +53,9 @@ import {
SupersetTheme,
allowedAntdTokens,
SharedAntdTokens,
ColorVariants,
DeprecatedThemeColors,
FontSizeKey,
} from './types';
import {
@@ -100,6 +102,15 @@ export class Theme {
private antdConfig: AntdThemeConfig;
private static readonly sizeMap: Record<FontSizeKey, string> = {
xs: 'fontSizeXS',
s: 'fontSizeSM',
m: 'fontSize',
l: 'fontSizeLG',
xl: 'fontSizeXL',
xxl: 'fontSizeXXL',
};
private constructor({ config }: { config?: AnyThemeConfig }) {
this.SupersetThemeProvider = this.SupersetThemeProvider.bind(this);
@@ -172,7 +183,7 @@ export class Theme {
// Second phase: Now that theme is initialized, we can determine if it's dark
// and generate the legacy colors correctly
const systemColors = getSystemColors(tokens);
const isDark = isThemeDark(this.theme); // Use utility function with theme
const isDark = this.isThemeDark(); // Now we can safely call this
this.theme.colors = getDeprecatedColors(systemColors, isDark);
// Update the providers with the fully formed theme
@@ -190,6 +201,22 @@ export class Theme {
return serializeThemeConfig(this.antdConfig);
}
private getToken(token: string): any {
return (this.theme as Record<string, any>)[token];
}
public getFontSize(size?: FontSizeKey): string {
const fontSizeKey = Theme.sizeMap[size || 'm'];
return this.getToken(fontSizeKey) || this.getToken('fontSize');
}
/**
* Check if the current theme is dark based on background color
*/
isThemeDark(): boolean {
return tinycolor(this.theme.colorBgContainer).isDark();
}
toggleDarkMode(isDark: boolean): void {
// Create a new config based on the current one
const newConfig = { ...this.antdConfig };
@@ -223,6 +250,45 @@ export class Theme {
return JSON.stringify(serializeThemeConfig(this.antdConfig), null, 2);
}
getColorVariants(color: string): ColorVariants {
const firstLetterCapped = color.charAt(0).toUpperCase() + color.slice(1);
if (color === 'default' || color === 'grayscale') {
const isDark = this.isThemeDark();
const flipBrightness = (baseColor: string): string => {
if (!isDark) return baseColor;
const { r, g, b } = tinycolor(baseColor).toRgb();
const invertedColor = tinycolor({ r: 255 - r, g: 255 - g, b: 255 - b });
return invertedColor.toHexString();
};
return {
active: flipBrightness('#222'),
textActive: flipBrightness('#444'),
text: flipBrightness('#555'),
textHover: flipBrightness('#666'),
hover: flipBrightness('#888'),
borderHover: flipBrightness('#AAA'),
border: flipBrightness('#CCC'),
bgHover: flipBrightness('#DDD'),
bg: flipBrightness('#F4F4F4'),
};
}
const theme = this.getToken.bind(this);
return {
active: theme(`color${firstLetterCapped}Active`),
textActive: theme(`color${firstLetterCapped}TextActive`),
text: theme(`color${firstLetterCapped}Text`),
textHover: theme(`color${firstLetterCapped}TextHover`),
hover: theme(`color${firstLetterCapped}Hover`),
borderHover: theme(`color${firstLetterCapped}BorderHover`),
border: theme(`color${firstLetterCapped}Border`),
bgHover: theme(`color${firstLetterCapped}BgHover`),
bg: theme(`color${firstLetterCapped}Bg`),
};
}
private updateProviders(
theme: SupersetTheme,
antdConfig: AntdThemeConfig,

View File

@@ -26,9 +26,7 @@ import {
type ThemeStorage,
type ThemeControllerOptions,
type ThemeContextType,
type SupersetThemeConfig,
ThemeAlgorithm,
ThemeMode,
} from './types';
export {
@@ -68,16 +66,7 @@ const themeObject: Theme = Theme.fromConfig({
const { theme } = themeObject;
const supersetTheme = theme;
export {
Theme,
ThemeAlgorithm,
ThemeMode,
themeObject,
styled,
theme,
supersetTheme,
};
export { Theme, themeObject, styled, theme, supersetTheme };
export type {
SupersetTheme,
SerializableThemeConfig,
@@ -85,8 +74,4 @@ export type {
ThemeStorage,
ThemeControllerOptions,
ThemeContextType,
SupersetThemeConfig,
};
// Export theme utility functions
export * from './utils/themeUtils';

View File

@@ -411,7 +411,6 @@ export interface ThemeControllerOptions {
onChange?: (theme: Theme) => void;
canUpdateTheme?: () => boolean;
canUpdateMode?: () => boolean;
isGlobalContext?: boolean;
}
export interface ThemeContextType {
@@ -420,25 +419,4 @@ export interface ThemeContextType {
setTheme: (config: AnyThemeConfig) => void;
setThemeMode: (newMode: ThemeMode) => void;
resetTheme: () => void;
setTemporaryTheme: (config: AnyThemeConfig) => void;
clearLocalOverrides: () => void;
getCurrentCrudThemeId: () => string | null;
hasDevOverride: () => boolean;
canSetMode: () => boolean;
canSetTheme: () => boolean;
canDetectOSPreference: () => boolean;
createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
}
/**
* Configuration object for complete theme setup including default, dark themes and settings
*/
export interface SupersetThemeConfig {
theme_default: AnyThemeConfig;
theme_dark?: AnyThemeConfig;
theme_settings?: {
enforced?: boolean;
allowSwitching?: boolean;
allowOSPreference?: boolean;
};
}

View File

@@ -1,134 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getFontSize, getColorVariants, isThemeDark } from './themeUtils';
import { Theme } from '../Theme';
import { ThemeAlgorithm } from '../types';
// Mock emotion's cache to avoid actual DOM operations
jest.mock('@emotion/cache', () => ({
__esModule: true,
default: jest.fn().mockReturnValue({}),
}));
describe('themeUtils', () => {
let lightTheme: Theme;
let darkTheme: Theme;
beforeEach(() => {
jest.clearAllMocks();
// Create actual theme instances for testing
lightTheme = Theme.fromConfig({
token: {
colorPrimary: '#1890ff',
fontSizeXS: '8',
fontSize: '14',
fontSizeLG: '16',
},
});
darkTheme = Theme.fromConfig({
algorithm: ThemeAlgorithm.DARK,
token: {
colorPrimary: '#1890ff',
fontSizeXS: '8',
fontSize: '14',
fontSizeLG: '16',
},
});
});
describe('getFontSize', () => {
it('returns correct font size for given key', () => {
expect(getFontSize(lightTheme.theme, 'xs')).toBe('8');
expect(getFontSize(lightTheme.theme, 'm')).toBe('14');
expect(getFontSize(lightTheme.theme, 'l')).toBe('16');
});
it('defaults to medium font size when no key is provided', () => {
expect(getFontSize(lightTheme.theme)).toBe('14');
});
it('uses antd default when specific size not overridden', () => {
// Create theme with minimal config - antd will provide defaults
const minimalTheme = Theme.fromConfig({
token: { fontSize: '14' },
});
// Ant Design provides fontSizeXS: '8' by default
expect(getFontSize(minimalTheme.theme, 'xs')).toBe('8');
expect(getFontSize(minimalTheme.theme, 'm')).toBe('14');
});
});
describe('isThemeDark', () => {
it('returns false for light theme', () => {
expect(isThemeDark(lightTheme.theme)).toBe(false);
});
it('returns true for dark theme', () => {
expect(isThemeDark(darkTheme.theme)).toBe(true);
});
});
describe('getColorVariants', () => {
it('returns correct variants for primary color', () => {
const variants = getColorVariants(lightTheme.theme, 'primary');
expect(variants.text).toBeDefined();
expect(variants.bg).toBeDefined();
expect(variants.border).toBeDefined();
expect(variants.active).toBeDefined();
});
it('returns grayscale variants for default color in light theme', () => {
const variants = getColorVariants(lightTheme.theme, 'default');
expect(variants.active).toBe('#222');
expect(variants.textActive).toBe('#444');
expect(variants.text).toBe('#555');
expect(variants.bg).toBe('#F4F4F4');
});
it('returns inverted grayscale variants for default color in dark theme', () => {
const variants = getColorVariants(darkTheme.theme, 'default');
// In dark theme, colors should be inverted
expect(variants.active).toBe('#dddddd'); // Inverted #222
expect(variants.textActive).toBe('#bbbbbb'); // Inverted #444
expect(variants.text).toBe('#aaaaaa'); // Inverted #555
});
it('returns same variants for grayscale color as default', () => {
const defaultVariants = getColorVariants(lightTheme.theme, 'default');
const grayscaleVariants = getColorVariants(lightTheme.theme, 'grayscale');
expect(defaultVariants).toEqual(grayscaleVariants);
});
it('handles missing color tokens gracefully', () => {
const variants = getColorVariants(lightTheme.theme, 'nonexistent');
// Should return undefined for missing tokens
expect(variants.active).toBeUndefined();
expect(variants.text).toBeUndefined();
expect(variants.bg).toBeUndefined();
});
});
});

View File

@@ -1,113 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import tinycolor from 'tinycolor2';
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
xs: 'fontSizeXS',
s: 'fontSizeSM',
m: 'fontSize',
l: 'fontSizeLG',
xl: 'fontSizeXL',
xxl: 'fontSizeXXL',
};
/**
* Get font size from theme tokens based on size key
* @param theme - Theme tokens from useTheme()
* @param size - Font size key
* @returns Font size as string
*/
export function getFontSize(theme: SupersetTheme, size?: FontSizeKey): string {
const key = fontSizeMap[size || 'm'];
return String(theme[key] || theme.fontSize);
}
/**
* Get color variants for a given color type from theme tokens
* @param theme - Theme tokens from useTheme()
* @param color - Color type (e.g., 'primary', 'error', 'success')
* @returns ColorVariants object with bg, border, text colors etc.
*/
export function getColorVariants(
theme: SupersetTheme,
color: string,
): ColorVariants {
const firstLetterCapped = color.charAt(0).toUpperCase() + color.slice(1);
if (color === 'default' || color === 'grayscale') {
const isDark = isThemeDark(theme);
const flipBrightness = (baseColor: string): string => {
if (!isDark) return baseColor;
const { r, g, b } = tinycolor(baseColor).toRgb();
const invertedColor = tinycolor({ r: 255 - r, g: 255 - g, b: 255 - b });
return invertedColor.toHexString();
};
return {
active: flipBrightness('#222'),
textActive: flipBrightness('#444'),
text: flipBrightness('#555'),
textHover: flipBrightness('#666'),
hover: flipBrightness('#888'),
borderHover: flipBrightness('#AAA'),
border: flipBrightness('#CCC'),
bgHover: flipBrightness('#DDD'),
bg: flipBrightness('#F4F4F4'),
};
}
return {
active: theme[
`color${firstLetterCapped}Active` as keyof SupersetTheme
] as string,
textActive: theme[
`color${firstLetterCapped}TextActive` as keyof SupersetTheme
] as string,
text: theme[
`color${firstLetterCapped}Text` as keyof SupersetTheme
] as string,
textHover: theme[
`color${firstLetterCapped}TextHover` as keyof SupersetTheme
] as string,
hover: theme[
`color${firstLetterCapped}Hover` as keyof SupersetTheme
] as string,
borderHover: theme[
`color${firstLetterCapped}BorderHover` as keyof SupersetTheme
] as string,
border: theme[
`color${firstLetterCapped}Border` as keyof SupersetTheme
] as string,
bgHover: theme[
`color${firstLetterCapped}BgHover` as keyof SupersetTheme
] as string,
bg: theme[`color${firstLetterCapped}Bg` as keyof SupersetTheme] as string,
};
}
/**
* Check if the current theme is dark mode based on background color
* @param theme - Theme tokens from useTheme()
* @returns true if theme is dark, false if light
*/
export function isThemeDark(theme: SupersetTheme): boolean {
return tinycolor(theme.colorBgContainer).isDark();
}

View File

@@ -30,7 +30,6 @@ export enum FeatureFlag {
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
CssTemplates = 'CSS_TEMPLATES',
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
DashboardRbac = 'DASHBOARD_RBAC',
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
@@ -54,6 +53,8 @@ export enum FeatureFlag {
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
SshTunneling = 'SSH_TUNNELING',
TaggingSystem = 'TAGGING_SYSTEM',
ThemeEnableDarkThemeSwitch = 'THEME_ENABLE_DARK_THEME_SWITCH',
ThemeAllowThemeEditorBeta = 'THEME_ALLOW_THEME_EDITOR_BETA',
Thumbnails = 'THUMBNAILS',
UseAnalogousColors = 'USE_ANALOGOUS_COLORS',
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',

View File

@@ -119,7 +119,7 @@ describe('ChartProps', () => {
});
expect(props1).not.toBe(props2);
});
it('selector returns a new chartProps if some input fields change and returns memoized chart props', () => {
it('selector returns a new chartProps if some input fields change', () => {
const props1 = selector({
width: 800,
height: 600,
@@ -145,7 +145,7 @@ describe('ChartProps', () => {
theme: supersetTheme,
});
expect(props1).not.toBe(props2);
expect(props1).toBe(props3);
expect(props1).not.toBe(props3);
});
});
});

View File

@@ -1385,7 +1385,7 @@ export default function (config) {
p[0] = p[0] - __.margin.left;
p[1] = p[1] - __.margin.top;
((dims = dimensionsForPoint(p)),
(dims = dimensionsForPoint(p)),
(strum = {
p1: p,
dims: dims,
@@ -1393,7 +1393,7 @@ export default function (config) {
maxX: xscale(dims.right),
minY: 0,
maxY: h(),
}));
});
strums[dims.i] = strum;
strums.active = dims.i;
@@ -1942,7 +1942,7 @@ export default function (config) {
p[0] = p[0] - __.margin.left;
p[1] = p[1] - __.margin.top;
((dims = dimensionsForPoint(p)),
(dims = dimensionsForPoint(p)),
(arc = {
p1: p,
dims: dims,
@@ -1953,7 +1953,7 @@ export default function (config) {
startAngle: undefined,
endAngle: undefined,
arc: d3.svg.arc().innerRadius(0),
}));
});
arcs[dims.i] = arc;
arcs.active = dims.i;

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