mirror of
https://github.com/apache/superset.git
synced 2026-04-30 13:34:20 +00:00
Compare commits
112 Commits
metricflow
...
refactor_q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398842a4d8 | ||
|
|
aff7f54b1a | ||
|
|
288da4a050 | ||
|
|
2d8ae42d42 | ||
|
|
990174bb1c | ||
|
|
6f8a79693d | ||
|
|
b8a71e4754 | ||
|
|
13e7ba18ed | ||
|
|
c5887630ab | ||
|
|
c11efecdad | ||
|
|
3dc97b11f8 | ||
|
|
b81487e177 | ||
|
|
72e33ba811 | ||
|
|
b0715bd8bb | ||
|
|
0348b6c313 | ||
|
|
453b3da9f6 | ||
|
|
7c6c0c0451 | ||
|
|
bf43704200 | ||
|
|
fef0676954 | ||
|
|
7485af5e6c | ||
|
|
825b9e784a | ||
|
|
7191ae55c8 | ||
|
|
17725ebc83 | ||
|
|
1a7a381bd5 | ||
|
|
daf207e5c2 | ||
|
|
72294c569f | ||
|
|
792dd08d38 | ||
|
|
1e40e7d02b | ||
|
|
7e98c75f01 | ||
|
|
b18de05ea4 | ||
|
|
9300652277 | ||
|
|
7c2ec4ca5f | ||
|
|
6a83b6fd87 | ||
|
|
659cd33749 | ||
|
|
cb27d5fe8d | ||
|
|
6c9cda758a | ||
|
|
967134f540 | ||
|
|
25bb353f9d | ||
|
|
9cf2472291 | ||
|
|
cf5b976659 | ||
|
|
70394e79ef | ||
|
|
ea64f3122e | ||
|
|
50197fc33e | ||
|
|
c480fa7fcf | ||
|
|
6fc734da51 | ||
|
|
762a11b0bb | ||
|
|
f168dd69a8 | ||
|
|
becd0b8883 | ||
|
|
fd4570625a | ||
|
|
54a5b58e40 | ||
|
|
a611278e04 | ||
|
|
5c2eb0a68c | ||
|
|
0cbf4d5d4d | ||
|
|
6006a21378 | ||
|
|
bf967d6ba4 | ||
|
|
131ae5aa9d | ||
|
|
eca28582b6 | ||
|
|
14e90a0f52 | ||
|
|
a1c39d4906 | ||
|
|
0964a8bb7a | ||
|
|
8de8f95a3c | ||
|
|
16db999067 | ||
|
|
972be15dda | ||
|
|
c9e06714f8 | ||
|
|
32626ab707 | ||
|
|
a9cd58508b | ||
|
|
122bb68e5a | ||
|
|
914ce9aa4f | ||
|
|
bb572983cd | ||
|
|
ff76ab647f | ||
|
|
f554848c9f | ||
|
|
dc0c389488 | ||
|
|
22b3cc0480 | ||
|
|
604d72cc98 | ||
|
|
913e068113 | ||
|
|
1a4e2173f5 | ||
|
|
c49789167b | ||
|
|
1be2287b3a | ||
|
|
e741a3167f | ||
|
|
5f11f9097a | ||
|
|
8783579aa8 | ||
|
|
c25b4221f8 | ||
|
|
9c771fb2ba | ||
|
|
7f44992c4b | ||
|
|
8df5860826 | ||
|
|
b794b192d1 | ||
|
|
3177131d52 | ||
|
|
89bf77b5c9 | ||
|
|
30e5684006 | ||
|
|
3f8472ca7b | ||
|
|
efa8cb6fa4 | ||
|
|
ab59b7e9b0 | ||
|
|
c99843b13a | ||
|
|
da55a6c94a | ||
|
|
7a1c056374 | ||
|
|
1e5a4e9bdc | ||
|
|
9b88527883 | ||
|
|
800c1639ec | ||
|
|
43775e9373 | ||
|
|
9099b0f00d | ||
|
|
77ffe65773 | ||
|
|
32f8f33a4f | ||
|
|
710c277681 | ||
|
|
11324607d0 | ||
|
|
9c6271136d | ||
|
|
c444eed63e | ||
|
|
1df5e59fdf | ||
|
|
77f66e7434 | ||
|
|
2c81eb6c39 | ||
|
|
09c4afc894 | ||
|
|
229d92590a | ||
|
|
f4f516c64c |
125
.cursor/rules/dev-standard.mdc
Normal file
125
.cursor/rules/dev-standard.mdc
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
description: Apache Superset development standards and guidelines for Cursor IDE
|
||||
globs: ["**/*.py", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.sql", "**/*.md"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Apache Superset Development Standards for Cursor IDE
|
||||
|
||||
Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
|
||||
|
||||
## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
|
||||
|
||||
**These migrations are actively happening - avoid deprecated patterns:**
|
||||
|
||||
### Frontend Modernization
|
||||
- **NO `any` types** - Use proper TypeScript types
|
||||
- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
|
||||
- **NO Enzyme** - Use React Testing Library/Jest (Enzyme fully removed)
|
||||
- **Use @superset-ui/core** - Don't import Ant Design directly
|
||||
|
||||
### Testing Strategy Migration
|
||||
- **Prefer unit tests** over integration tests
|
||||
- **Prefer integration tests** over Cypress end-to-end tests
|
||||
- **Cypress is last resort** - Actively moving away from Cypress
|
||||
- **Use Jest + React Testing Library** for component testing
|
||||
|
||||
### Backend Type Safety
|
||||
- **Add type hints** - All new Python code needs proper typing
|
||||
- **MyPy compliance** - Run `pre-commit run mypy` to validate
|
||||
- **SQLAlchemy typing** - Use proper model annotations
|
||||
|
||||
## Code Standards
|
||||
|
||||
### TypeScript Frontend
|
||||
- **NO `any` types** - Use proper TypeScript
|
||||
- **Functional components** with hooks
|
||||
- **@superset-ui/core** for UI components (not direct antd)
|
||||
- **Jest** for testing (NO Enzyme)
|
||||
- **Redux** for global state, hooks for local
|
||||
|
||||
### Python Backend
|
||||
- **Type hints required** for all new code
|
||||
- **MyPy compliant** - run `pre-commit run mypy`
|
||||
- **SQLAlchemy models** with proper typing
|
||||
- **pytest** for testing
|
||||
|
||||
### Apache License Headers
|
||||
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
||||
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
||||
|
||||
## Key Directory Structure
|
||||
|
||||
```
|
||||
superset/
|
||||
├── superset/ # Python backend (Flask, SQLAlchemy)
|
||||
│ ├── views/api/ # REST API endpoints
|
||||
│ ├── models/ # Database models
|
||||
│ └── connectors/ # Database connections
|
||||
├── superset-frontend/src/ # React TypeScript frontend
|
||||
│ ├── components/ # Reusable components
|
||||
│ ├── explore/ # Chart builder
|
||||
│ ├── dashboard/ # Dashboard interface
|
||||
│ └── SqlLab/ # SQL editor
|
||||
├── superset-frontend/packages/
|
||||
│ └── superset-ui-core/ # UI component library (USE THIS)
|
||||
├── tests/ # Python/integration tests
|
||||
├── docs/ # Documentation (UPDATE FOR CHANGES)
|
||||
└── UPDATING.md # Breaking changes log
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Dataset-Centric Approach
|
||||
Charts built from enriched datasets containing:
|
||||
- Dimension columns with labels/descriptions
|
||||
- Predefined metrics as SQL expressions
|
||||
- Self-service analytics within defined contexts
|
||||
|
||||
### Security & Features
|
||||
- **RBAC**: Role-based access via Flask-AppBuilder
|
||||
- **Feature flags**: Control feature rollouts
|
||||
- **Row-level security**: SQL-based data access control
|
||||
|
||||
## Test Utilities
|
||||
|
||||
### Python Test Helpers
|
||||
- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
|
||||
- **`@with_config`** - Config mocking decorator
|
||||
- **`@with_feature_flags`** - Feature flag testing
|
||||
- **`login_as()`, `login_as_admin()`** - Authentication helpers
|
||||
- **`create_dashboard()`, `create_slice()`** - Data setup utilities
|
||||
|
||||
### TypeScript Test Helpers
|
||||
- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
|
||||
- **`createWrapper()`** - Redux/Router/Theme wrapper
|
||||
- **`selectOption()`** - Select component helper
|
||||
- **React Testing Library** - NO Enzyme (removed)
|
||||
|
||||
## Pre-commit Validation
|
||||
|
||||
**Use pre-commit hooks for quality validation:**
|
||||
|
||||
```bash
|
||||
# Install hooks
|
||||
pre-commit install
|
||||
|
||||
# Quick validation (faster than --all-files)
|
||||
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
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- **Documentation**: Update docs/ for any user-facing changes
|
||||
- **Breaking Changes**: Add to UPDATING.md
|
||||
- **Docstrings**: Required for new functions/classes
|
||||
- **Follow existing patterns**: Mimic code style, use existing libraries and utilities
|
||||
- **Type Safety**: This codebase is actively modernizing toward full TypeScript and type safety
|
||||
- **Always run `pre-commit run`** to validate changes before committing
|
||||
|
||||
---
|
||||
|
||||
**Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
|
||||
20
.devcontainer/Dockerfile
Normal file
20
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Keep this in sync with the base image in the main Dockerfile (ARG PY_VER)
|
||||
FROM python:3.11.13-bookworm AS base
|
||||
|
||||
# Install system dependencies that Superset needs
|
||||
# This layer will be cached across Codespace sessions
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libsasl2-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
tmux \
|
||||
gh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv for fast Python package management
|
||||
# This will also be cached in the image
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||
echo 'export PATH="/root/.cargo/bin:$PATH"' >> /etc/bash.bashrc
|
||||
|
||||
# Set the cargo/bin directory in PATH for all users
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
16
.devcontainer/README.md
Normal file
16
.devcontainer/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
62
.devcontainer/bashrc-additions
Normal file
62
.devcontainer/bashrc-additions
Normal file
@@ -0,0 +1,62 @@
|
||||
# Superset Codespaces environment setup
|
||||
# This file is appended to ~/.bashrc during Codespace setup
|
||||
|
||||
# Find the workspace directory (handles both 'superset' and 'superset-2' names)
|
||||
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
|
||||
|
||||
if [ -n "$WORKSPACE_DIR" ]; then
|
||||
# Check if virtual environment exists
|
||||
if [ -d "$WORKSPACE_DIR/.venv" ]; then
|
||||
# Activate the virtual environment
|
||||
source "$WORKSPACE_DIR/.venv/bin/activate"
|
||||
echo "✅ Python virtual environment activated"
|
||||
|
||||
# Verify pre-commit is installed and set up
|
||||
if command -v pre-commit &> /dev/null; then
|
||||
echo "✅ pre-commit is available ($(pre-commit --version))"
|
||||
# Install git hooks if not already installed
|
||||
if [ -d "$WORKSPACE_DIR/.git" ] && [ ! -f "$WORKSPACE_DIR/.git/hooks/pre-commit" ]; then
|
||||
echo "🪝 Installing pre-commit hooks..."
|
||||
cd "$WORKSPACE_DIR" && pre-commit install
|
||||
fi
|
||||
else
|
||||
echo "⚠️ pre-commit not found. Run: pip install pre-commit"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Python virtual environment not found at $WORKSPACE_DIR/.venv"
|
||||
echo " Run: cd $WORKSPACE_DIR && .devcontainer/setup-dev.sh"
|
||||
fi
|
||||
|
||||
# Always cd to the workspace directory for convenience
|
||||
cd "$WORKSPACE_DIR"
|
||||
fi
|
||||
|
||||
# Add helpful aliases for Superset development
|
||||
alias start-superset="$WORKSPACE_DIR/.devcontainer/start-superset.sh"
|
||||
alias setup-dev="$WORKSPACE_DIR/.devcontainer/setup-dev.sh"
|
||||
|
||||
# Show helpful message on login
|
||||
echo ""
|
||||
echo "🚀 Superset Codespaces Environment"
|
||||
echo "=================================="
|
||||
|
||||
# Check if Superset is running
|
||||
if docker ps 2>/dev/null | grep -q "superset"; then
|
||||
echo "✅ Superset is running!"
|
||||
echo " - Check the 'Ports' tab for your live Superset URL"
|
||||
echo " - Initial startup takes 10-20 minutes"
|
||||
echo " - Login: admin/admin"
|
||||
else
|
||||
echo "⚠️ Superset is not running. Use: start-superset"
|
||||
# Check if there's a startup log
|
||||
if [ -f "/tmp/superset-startup.log" ]; then
|
||||
echo " 📋 Startup log found: cat /tmp/superset-startup.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Quick commands:"
|
||||
echo " start-superset - Start Superset with Docker Compose"
|
||||
echo " setup-dev - Set up Python environment (if not already done)"
|
||||
echo " pre-commit run - Run pre-commit checks on staged files"
|
||||
echo ""
|
||||
20
.devcontainer/build-and-push-image.sh
Executable file
20
.devcontainer/build-and-push-image.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Script to build and push the devcontainer image to GitHub Container Registry
|
||||
# This allows caching the image between Codespace sessions
|
||||
|
||||
# You'll need to run this with appropriate GitHub permissions
|
||||
# gh auth login --scopes write:packages
|
||||
|
||||
REGISTRY="ghcr.io"
|
||||
OWNER="apache"
|
||||
REPO="superset"
|
||||
TAG="devcontainer-base"
|
||||
|
||||
echo "Building devcontainer image..."
|
||||
docker build -t $REGISTRY/$OWNER/$REPO:$TAG .devcontainer/
|
||||
|
||||
echo "Pushing to GitHub Container Registry..."
|
||||
docker push $REGISTRY/$OWNER/$REPO:$TAG
|
||||
|
||||
echo "Done! Update .devcontainer/devcontainer.json to use:"
|
||||
echo " \"image\": \"$REGISTRY/$OWNER/$REPO:$TAG\""
|
||||
66
.devcontainer/devcontainer.json
Normal file
66
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
.devcontainer/setup-dev.sh
Executable file
78
.devcontainer/setup-dev.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/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 ""
|
||||
108
.devcontainer/start-superset.sh
Executable file
108
.devcontainer/start-superset.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/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
|
||||
1
.github/copilot-instructions.md
vendored
Symbolic link
1
.github/copilot-instructions.md
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../LLMS.md
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
SUPERSET_TESTENV: true
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov-report= --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
2
.github/workflows/welcome-new-users.yml
vendored
2
.github/workflows/welcome-new-users.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Welcome Message
|
||||
uses: actions/first-interaction@v1
|
||||
uses: actions/first-interaction@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -127,5 +127,7 @@ docker/*local*
|
||||
# Jest test report
|
||||
test-report.html
|
||||
superset/static/stats/statistics.html
|
||||
.aider*
|
||||
|
||||
# LLM-related
|
||||
CLAUDE.local.md
|
||||
.aider*
|
||||
|
||||
@@ -52,14 +52,6 @@ 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
|
||||
@@ -76,7 +68,7 @@ repos:
|
||||
files: ^docs/.*\.(js|jsx|ts|tsx)$
|
||||
- id: type-checking-frontend
|
||||
name: Type-Checking (Frontend)
|
||||
entry: bash -c './scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base'
|
||||
entry: ./scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base
|
||||
language: system
|
||||
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
|
||||
exclude: ^superset-frontend/cypress-base\/
|
||||
@@ -97,9 +89,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
|
||||
@@ -113,9 +105,10 @@ repos:
|
||||
- |
|
||||
TARGET_BRANCH=${GITHUB_BASE_REF:-master}
|
||||
git fetch origin "$TARGET_BRANCH"
|
||||
files=$(git diff --name-only --diff-filter=ACM origin/"$TARGET_BRANCH"..HEAD | grep '^superset/.*\.py$' || true)
|
||||
BASE=$(git merge-base origin/"$TARGET_BRANCH" HEAD)
|
||||
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD | grep '^superset/.*\.py$' || true)
|
||||
if [ -n "$files" ]; then
|
||||
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint $files
|
||||
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint --reports=no $files
|
||||
else
|
||||
echo "No Python files to lint."
|
||||
fi
|
||||
|
||||
@@ -76,3 +76,11 @@ ydb.svg
|
||||
erd.puml
|
||||
erd.svg
|
||||
intro_header.txt
|
||||
|
||||
# for LLMs
|
||||
llm-context.md
|
||||
LLMS.md
|
||||
CLAUDE.md
|
||||
CURSOR.md
|
||||
GEMINI.md
|
||||
GPT.md
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -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's it's not possible selectively COPY of mount using blobs.
|
||||
# Note that it's not possible to selectively COPY or 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 used for compile frontend assets
|
||||
# superset-node is used for compiling 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 the frontend if not in dev mode
|
||||
# Build translations if enabled, then cleanup localization files
|
||||
RUN if [ "$BUILD_TRANSLATIONS" = "true" ]; then \
|
||||
npm run build-translation; \
|
||||
fi; \
|
||||
@@ -167,7 +167,7 @@ RUN mkdir -p \
|
||||
&& touch superset/static/version_info.json
|
||||
|
||||
# Install Playwright and optionally setup headless browsers
|
||||
ARG INCLUDE_CHROMIUM="true"
|
||||
ARG INCLUDE_CHROMIUM="false"
|
||||
ARG INCLUDE_FIREFOX="false"
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
if [ "$INCLUDE_CHROMIUM" = "true" ] || [ "$INCLUDE_FIREFOX" = "true" ]; then \
|
||||
@@ -223,7 +223,7 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/base.txt
|
||||
# Install the superset package
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
uv pip install .
|
||||
uv pip install -e .
|
||||
RUN python -m compileall /app/superset
|
||||
|
||||
USER superset
|
||||
@@ -246,7 +246,7 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/development.txt
|
||||
# Install the superset package
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
uv pip install .
|
||||
uv pip install -e .
|
||||
|
||||
RUN uv pip install .[postgres]
|
||||
RUN python -m compileall /app/superset
|
||||
|
||||
191
LLMS.md
Normal file
191
LLMS.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# LLM Context Guide for Apache Superset
|
||||
|
||||
Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
|
||||
|
||||
## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
|
||||
|
||||
**These migrations are actively happening - avoid deprecated patterns:**
|
||||
|
||||
### Frontend Modernization
|
||||
- **NO `any` types** - Use proper TypeScript types
|
||||
- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
|
||||
- **Use @superset-ui/core** - Don't import Ant Design directly
|
||||
|
||||
### Testing Strategy Migration
|
||||
- **Prefer unit tests** over integration tests
|
||||
- **Prefer integration tests** over Cypress end-to-end tests
|
||||
- **Cypress is last resort** - Actively moving away from Cypress
|
||||
- **Use Jest + React Testing Library** for component testing
|
||||
|
||||
### Backend Type Safety
|
||||
- **Add type hints** - All new Python code needs proper typing
|
||||
- **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
|
||||
|
||||
```
|
||||
superset/
|
||||
├── superset/ # Python backend (Flask, SQLAlchemy)
|
||||
│ ├── views/api/ # REST API endpoints
|
||||
│ ├── models/ # Database models
|
||||
│ └── connectors/ # Database connections
|
||||
├── superset-frontend/src/ # React TypeScript frontend
|
||||
│ ├── components/ # Reusable components
|
||||
│ ├── explore/ # Chart builder
|
||||
│ ├── dashboard/ # Dashboard interface
|
||||
│ └── SqlLab/ # SQL editor
|
||||
├── superset-frontend/packages/
|
||||
│ └── superset-ui-core/ # UI component library (USE THIS)
|
||||
├── tests/ # Python/integration tests
|
||||
├── docs/ # Documentation (UPDATE FOR CHANGES)
|
||||
└── UPDATING.md # Breaking changes log
|
||||
```
|
||||
|
||||
## Code Standards
|
||||
|
||||
### TypeScript Frontend
|
||||
- **Avoid `any` types** - Use proper TypeScript, reuse existing types
|
||||
- **Functional components** with hooks
|
||||
- **@superset-ui/core** for UI components (not direct antd)
|
||||
- **Jest** for testing (NO Enzyme)
|
||||
- **Redux** for global state where it exists, hooks for local
|
||||
|
||||
### Python Backend
|
||||
- **Type hints required** for all new code
|
||||
- **MyPy compliant** - run `pre-commit run mypy`
|
||||
- **SQLAlchemy models** with proper typing
|
||||
- **pytest** for testing
|
||||
|
||||
### Apache License Headers
|
||||
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
||||
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- **docs/**: Update for any user-facing changes
|
||||
- **UPDATING.md**: Add breaking changes here
|
||||
- **Docstrings**: Required for new functions/classes
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Security & Features
|
||||
- **RBAC**: Role-based access via Flask-AppBuilder
|
||||
- **Feature flags**: Control feature rollouts
|
||||
- **Row-level security**: SQL-based data access control
|
||||
|
||||
## Test Utilities
|
||||
|
||||
### Python Test Helpers
|
||||
- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
|
||||
- **`@with_config`** - Config mocking decorator
|
||||
- **`@with_feature_flags`** - Feature flag testing
|
||||
- **`login_as()`, `login_as_admin()`** - Authentication helpers
|
||||
- **`create_dashboard()`, `create_slice()`** - Data setup utilities
|
||||
|
||||
### TypeScript Test Helpers
|
||||
- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
|
||||
- **`createWrapper()`** - Redux/Router/Theme wrapper
|
||||
- **`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
|
||||
npm run test # All tests
|
||||
npm run test -- filename.test.tsx # Single file
|
||||
|
||||
# Backend
|
||||
pytest # All tests
|
||||
pytest tests/unit_tests/specific_test.py # Single file
|
||||
pytest tests/unit_tests/ # Directory
|
||||
|
||||
# If pytest fails with database/setup issues, ask the user to run test environment setup
|
||||
```
|
||||
|
||||
## Environment Validation
|
||||
|
||||
**Quick Setup Check (run this first):**
|
||||
|
||||
```bash
|
||||
# Verify Superset is running
|
||||
curl -f http://localhost:8088/health || echo "❌ Setup required - see https://superset.apache.org/docs/contributing/development#working-with-llms"
|
||||
```
|
||||
|
||||
**If health checks fail:**
|
||||
"It appears you aren't set up properly. Please refer to the [Working with LLMs](https://superset.apache.org/docs/contributing/development#working-with-llms) section in the development docs for setup instructions."
|
||||
|
||||
**Key Project Files:**
|
||||
- `superset-frontend/package.json` - Frontend build scripts (`npm run dev` on port 9000, `npm run test`, `npm run lint`)
|
||||
- `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:**
|
||||
|
||||
```bash
|
||||
# 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 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
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
|
||||
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
|
||||
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
|
||||
|
||||
---
|
||||
|
||||
**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
|
||||
@@ -23,6 +23,9 @@ 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`.
|
||||
There's a migration added that can potentially affect a significant number of existing charts.
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
# 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:
|
||||
|
||||
157
docker-compose-light.yml
Normal file
157
docker-compose-light.yml
Normal file
@@ -0,0 +1,157 @@
|
||||
#
|
||||
# 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
|
||||
@@ -20,6 +20,9 @@
|
||||
# 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
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
# 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
|
||||
|
||||
@@ -53,7 +53,12 @@ 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
|
||||
@@ -66,4 +71,3 @@ 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
|
||||
|
||||
@@ -23,25 +23,57 @@ MIN_MEM_FREE_GB=3
|
||||
MIN_MEM_FREE_KB=$(($MIN_MEM_FREE_GB*1000000))
|
||||
|
||||
echo_mem_warn() {
|
||||
MEM_FREE_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_FREE_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
# Check if running in Codespaces first
|
||||
if [[ -n "${CODESPACES}" ]]; then
|
||||
echo "Memory available: Codespaces managed"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${MEM_FREE_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
|
||||
# Check platform and get memory accordingly
|
||||
if [[ -f /proc/meminfo ]]; then
|
||||
# Linux
|
||||
if grep -q MemAvailable /proc/meminfo; then
|
||||
MEM_AVAIL_KB=$(awk '/MemAvailable/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_AVAIL_GB=$(awk '/MemAvailable/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
else
|
||||
MEM_AVAIL_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_AVAIL_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
fi
|
||||
elif [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS - use vm_stat to get free memory
|
||||
# vm_stat reports in pages, typically 4096 bytes per page
|
||||
PAGE_SIZE=$(pagesize)
|
||||
FREE_PAGES=$(vm_stat | awk '/Pages free:/ {print $3}' | tr -d '.')
|
||||
INACTIVE_PAGES=$(vm_stat | awk '/Pages inactive:/ {print $3}' | tr -d '.')
|
||||
# Free + inactive pages give us available memory (similar to MemAvailable on Linux)
|
||||
AVAIL_PAGES=$((FREE_PAGES + INACTIVE_PAGES))
|
||||
MEM_AVAIL_KB=$((AVAIL_PAGES * PAGE_SIZE / 1024))
|
||||
MEM_AVAIL_GB=$(echo "scale=2; $MEM_AVAIL_KB / 1024 / 1024" | bc)
|
||||
else
|
||||
# Other platforms
|
||||
echo "Memory available: Unable to determine"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${MEM_AVAIL_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
|
||||
cat <<EOF
|
||||
===============================================
|
||||
======== Memory Insufficient Warning =========
|
||||
===============================================
|
||||
|
||||
It looks like you only have ${MEM_FREE_GB}GB of
|
||||
memory free. Please increase your Docker
|
||||
It looks like you only have ${MEM_AVAIL_GB}GB of
|
||||
memory ${MEM_TYPE}. Please increase your Docker
|
||||
resources to at least ${MIN_MEM_FREE_GB}GB
|
||||
|
||||
Note: During builds, available memory may be
|
||||
temporarily low due to caching and compilation.
|
||||
|
||||
===============================================
|
||||
======== Memory Insufficient Warning =========
|
||||
===============================================
|
||||
EOF
|
||||
else
|
||||
echo "Memory check Ok [${MEM_FREE_GB}GB free]"
|
||||
echo "Memory available: ${MEM_AVAIL_GB} GB"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
1
docker/pythonpath_dev/.gitignore
vendored
1
docker/pythonpath_dev/.gitignore
vendored
@@ -20,4 +20,5 @@
|
||||
# DON'T ignore the .gitignore
|
||||
!.gitignore
|
||||
!superset_config.py
|
||||
!superset_config_docker_light.py
|
||||
!superset_config_local.example
|
||||
|
||||
@@ -129,7 +129,7 @@ if os.getenv("CYPRESS_CONFIG") == "true":
|
||||
#
|
||||
try:
|
||||
import superset_config_docker
|
||||
from superset_config_docker import * # noqa
|
||||
from superset_config_docker import * # noqa: F403
|
||||
|
||||
logger.info(
|
||||
f"Loaded your Docker configuration at [{superset_config_docker.__file__}]"
|
||||
|
||||
37
docker/pythonpath_dev/superset_config_docker_light.py
Normal file
37
docker/pythonpath_dev/superset_config_docker_light.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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]
|
||||
@@ -10,44 +10,143 @@ version: 1
|
||||
apache-superset>=6.0
|
||||
:::
|
||||
|
||||
Superset now rides on **Ant Design v5’s token-based theming**.
|
||||
Superset now rides on **Ant Design v5's token-based theming**.
|
||||
Every Antd token works, plus a handful of Superset-specific ones for charts and dashboard chrome.
|
||||
|
||||
## 1 — Create a theme
|
||||
## Managing Themes via CRUD Interface
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
|
||||
|
||||
## 2 — Apply it instance-wide
|
||||
### 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
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
THEME = {
|
||||
# Paste your JSON theme definition here
|
||||
|
||||
# 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
|
||||
}
|
||||
```
|
||||
|
||||
Restart Superset to apply changes
|
||||
### Copying Themes from CRUD Interface
|
||||
|
||||
## 3 — Tweak live in the app (beta)
|
||||
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`:
|
||||
|
||||
Set the feature flag in your `superset_config`
|
||||
```python
|
||||
DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
|
||||
{{ ... }}
|
||||
THEME_ALLOW_THEME_EDITOR_BETA = True,
|
||||
# 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"],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 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
|
||||
### Using Custom Fonts in Themes
|
||||
|
||||
## 4 — Potential Next Steps
|
||||
Once configured, reference the fonts in your theme configuration:
|
||||
|
||||
- CRUD UI for managing multiple themes
|
||||
- Per-dashboard & per-workspace theme assignment
|
||||
- User-selectable theme preferences
|
||||
```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
|
||||
|
||||
@@ -120,6 +120,78 @@ 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
|
||||
@@ -194,6 +266,48 @@ You can also run the pre-commit checks manually in various ways:
|
||||
Replace `<hook_id>` with the ID of the specific hook you want to run. You can find the list
|
||||
of available hooks in the `.pre-commit-config.yaml` file.
|
||||
|
||||
## Working with LLMs
|
||||
|
||||
### Environment Setup
|
||||
Ensure Docker Compose is running before starting LLM sessions:
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Validate your environment:
|
||||
```bash
|
||||
curl -f http://localhost:8088/health && echo "✅ Superset ready"
|
||||
```
|
||||
|
||||
### LLM Session Best Practices
|
||||
- Always validate environment setup first using the health checks above
|
||||
- Use focused validation commands: `pre-commit run` (not `--all-files`)
|
||||
- **Read [LLMS.md](https://github.com/apache/superset/blob/master/LLMS.md) first** - Contains comprehensive development guidelines, coding standards, and critical refactor information
|
||||
- **Check platform-specific files** when available:
|
||||
- `CLAUDE.md` - For Claude/Anthropic tools
|
||||
- `CURSOR.md` - For Cursor editor
|
||||
- `GEMINI.md` - For Google Gemini tools
|
||||
- `GPT.md` - For OpenAI/ChatGPT tools
|
||||
- Follow the TypeScript migration guidelines and avoid deprecated patterns listed in LLMS.md
|
||||
|
||||
### Key Development Commands
|
||||
```bash
|
||||
# Frontend development
|
||||
cd superset-frontend
|
||||
npm run dev # Development server on http://localhost:9000
|
||||
npm run test # Run all tests
|
||||
npm run test -- filename.test.tsx # Run single test file
|
||||
npm run lint # Linting and type checking
|
||||
|
||||
# Backend validation
|
||||
pre-commit run mypy # Type checking
|
||||
pytest # Run all tests
|
||||
pytest tests/unit_tests/specific_test.py # Run single test file
|
||||
pytest tests/unit_tests/ # Run all tests in directory
|
||||
```
|
||||
|
||||
For detailed development context, environment setup, and coding guidelines, see [LLMS.md](https://github.com/apache/superset/blob/master/LLMS.md).
|
||||
|
||||
## Alternatives to `docker compose`
|
||||
|
||||
:::caution
|
||||
@@ -307,14 +421,6 @@ Then make sure you run your WSGI server using the right worker type:
|
||||
gunicorn "superset.app:create_app()" -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -b 127.0.0.1:8088 --reload
|
||||
```
|
||||
|
||||
You can log anything to the browser console, including objects:
|
||||
|
||||
```python
|
||||
from superset import app
|
||||
app.logger.error('An exception occurred!')
|
||||
app.logger.info(form_data)
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in order to properly display the web UI. The `superset-frontend` directory contains all NPM-managed frontend assets. Note that for some legacy pages there are additional frontend assets bundled with Flask-Appbuilder (e.g. jQuery and bootstrap). These are not managed by NPM and may be phased out in the future.
|
||||
|
||||
@@ -26,11 +26,14 @@ 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 3 major ways we support to run `docker compose`:
|
||||
Note that there are 4 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
|
||||
@@ -44,7 +47,7 @@ Note that there are 3 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 two approaches after setting up the requirements for either.
|
||||
More on these approaches after setting up the requirements for either.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -103,13 +106,36 @@ 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 - build a set of immutable images from the local branch
|
||||
### 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
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-non-dev.yml up
|
||||
```
|
||||
|
||||
### Option #3 - boot up an official release
|
||||
### Option #4 - boot up an official release
|
||||
|
||||
```bash
|
||||
# Set the version you want to run
|
||||
|
||||
@@ -28,6 +28,9 @@ const globals = require('globals');
|
||||
const { defineConfig, globalIgnores } = require('eslint/config');
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
@@ -36,7 +39,7 @@ module.exports = defineConfig([
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
@@ -68,5 +71,5 @@ module.exports = defineConfig([
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
}
|
||||
])
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc",
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
"eslint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
@@ -26,33 +26,33 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.26.3",
|
||||
"antd": "^5.26.7",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less": "^4.4.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"swagger-ui-react": "^5.26.0"
|
||||
"swagger-ui-react": "^5.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"webpack": "^5.99.9"
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -65,5 +65,6 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
143
docs/yarn.lock
143
docs/yarn.lock
@@ -2205,11 +2205,16 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
|
||||
"@eslint/js@9.31.0":
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
|
||||
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
|
||||
|
||||
"@eslint/js@^9.32.0":
|
||||
version "9.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
|
||||
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
|
||||
@@ -2512,10 +2517,10 @@
|
||||
classnames "^2.3.2"
|
||||
rc-util "^5.24.4"
|
||||
|
||||
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
|
||||
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
|
||||
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
|
||||
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/portal" "^1.1.0"
|
||||
@@ -3422,10 +3427,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
|
||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
|
||||
version "5.0.6"
|
||||
@@ -3966,6 +3971,11 @@ accepts@~1.3.4, accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-import-phases@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
|
||||
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -3978,12 +3988,7 @@ acorn-walk@^8.0.0:
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
acorn@^8.15.0:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -4107,10 +4112,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.26.3:
|
||||
version "5.26.3"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
|
||||
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
|
||||
antd@^5.26.7:
|
||||
version "5.26.7"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
|
||||
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4123,7 +4128,7 @@ antd@^5.26.3:
|
||||
"@rc-component/mutate-observer" "^1.1.0"
|
||||
"@rc-component/qrcode" "~1.0.0"
|
||||
"@rc-component/tour" "~1.15.1"
|
||||
"@rc-component/trigger" "^2.2.7"
|
||||
"@rc-component/trigger" "^2.3.0"
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
@@ -4153,7 +4158,7 @@ antd@^5.26.3:
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.51.1"
|
||||
rc-tabs "~15.6.1"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-textarea "~1.10.1"
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
@@ -4320,12 +4325,12 @@ available-typed-arrays@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.9.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
|
||||
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
|
||||
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
form-data "^4.0.4"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
babel-loader@^9.2.1:
|
||||
@@ -4508,17 +4513,7 @@ braces@^3.0.3, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
|
||||
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001688"
|
||||
electron-to-chromium "^1.5.73"
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
browserslist@^4.25.0:
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
|
||||
version "4.25.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
|
||||
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
|
||||
@@ -4620,7 +4615,7 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
|
||||
version "1.0.30001714"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
|
||||
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
|
||||
@@ -5903,11 +5898,6 @@ electron-to-chromium@^1.5.160:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
|
||||
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
|
||||
|
||||
electron-to-chromium@^1.5.73:
|
||||
version "1.5.138"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
|
||||
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
@@ -5952,10 +5942,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.17.1:
|
||||
version "5.18.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
|
||||
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
|
||||
enhanced-resolve@^5.17.2:
|
||||
version "5.18.2"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
|
||||
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
@@ -6161,10 +6151,10 @@ escape-string-regexp@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
eslint-config-prettier@^10.1.5:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||
eslint-config-prettier@^10.1.8:
|
||||
version "10.1.8"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
|
||||
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
|
||||
|
||||
eslint-plugin-prettier@^5.5.1:
|
||||
version "5.5.1"
|
||||
@@ -6653,7 +6643,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.0:
|
||||
form-data@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
@@ -8063,10 +8053,10 @@ less-loader@^12.3.0:
|
||||
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
|
||||
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
|
||||
|
||||
less@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
|
||||
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
|
||||
less@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
|
||||
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
@@ -10714,10 +10704,10 @@ rc-tabs@~15.6.1:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-textarea@~1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
||||
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
||||
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
|
||||
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
@@ -12100,10 +12090,10 @@ swagger-client@^3.35.5:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
swagger-ui-react@^5.26.0:
|
||||
version "5.26.0"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
|
||||
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
|
||||
swagger-ui-react@^5.27.1:
|
||||
version "5.27.1"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
|
||||
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
@@ -12525,7 +12515,7 @@ unraw@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
|
||||
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
|
||||
|
||||
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
|
||||
update-browserslist-db@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
|
||||
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
|
||||
@@ -12794,26 +12784,27 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
webpack-sources@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
|
||||
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
|
||||
version "5.99.9"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
|
||||
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
|
||||
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.101.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
|
||||
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.14.0"
|
||||
acorn "^8.15.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.24.0"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.17.1"
|
||||
enhanced-resolve "^5.17.2"
|
||||
es-module-lexer "^1.2.1"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -12827,7 +12818,7 @@ webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.2.3"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
|
||||
@@ -95,7 +95,7 @@ dependencies = [
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=26.1.3, <27",
|
||||
"sqlglot>=27.3.0, <28",
|
||||
# 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.6.1",
|
||||
"sqlalchemy-bigquery>=1.15.0",
|
||||
"google-cloud-bigquery>=3.10.0",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
|
||||
@@ -311,15 +311,16 @@ select = [
|
||||
"Q",
|
||||
"S",
|
||||
"T",
|
||||
"TID",
|
||||
"W",
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"S101",
|
||||
"PT006",
|
||||
"T201",
|
||||
"N999",
|
||||
]
|
||||
|
||||
extend-select = ["I"]
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
@@ -329,6 +330,16 @@ 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
|
||||
@@ -345,6 +356,9 @@ 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"
|
||||
|
||||
@@ -378,7 +378,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.28.1
|
||||
sqlglot==27.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -795,14 +795,14 @@ sqlalchemy==1.4.54
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.12.0
|
||||
sqlalchemy-bigquery==1.15.0
|
||||
# via apache-superset
|
||||
sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.28.1
|
||||
sqlglot==27.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -32,6 +32,10 @@ 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 {
|
||||
@@ -45,27 +49,94 @@ void (async () => {
|
||||
}
|
||||
|
||||
const packageRootDir = await getPackage(packageArg);
|
||||
const updatedArgs = removePackageSegment(remainingArgs, packageRootDir);
|
||||
const argsStr = updatedArgs.join(" ");
|
||||
const changedFiles = removePackageSegment(remainingArgs, packageRootDir);
|
||||
|
||||
const excludedDeclarationDirs = getExcludedDeclarationDirs(
|
||||
excludeDeclarationDirArg
|
||||
// Filter to only TypeScript files
|
||||
const tsFiles = changedFiles.filter(file =>
|
||||
/\.(ts|tsx)$/.test(file) && !DECLARATION_FILE_REGEX.test(file)
|
||||
);
|
||||
|
||||
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(
|
||||
packageRootDir,
|
||||
join(SUPERSET_ROOT, 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}`;
|
||||
|
||||
@@ -77,14 +148,16 @@ void (async () => {
|
||||
console.error(child.stderr);
|
||||
}
|
||||
|
||||
exit(child.exitCode);
|
||||
if (child.exitCode !== 0) {
|
||||
throw new Error(`Type check failed with exit code ${child.exitCode}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to execute type checking:", e);
|
||||
console.error("Package:", packageRootDir);
|
||||
console.error("Failed to execute type checking:", e.message);
|
||||
console.error("Command:", `tscw ${command}`);
|
||||
exit(1);
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -112,7 +185,6 @@ function shouldExcludeDir(fullPath, excludedDirs) {
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
|
||||
async function getFilesRecursively(dir, regex, excludedDirs) {
|
||||
try {
|
||||
const files = await readdir(dir, { withFileTypes: true });
|
||||
@@ -186,7 +258,6 @@ function getExcludedDeclarationDirs(excludeDeclarationDirArg) {
|
||||
* @param {RegExp[]} regexes
|
||||
* @returns {{ matchedArgs: (string | undefined)[], remainingArgs: string[] }}
|
||||
*/
|
||||
|
||||
function extractArgs(args, regexes) {
|
||||
/**
|
||||
* @type {(string | undefined)[]}
|
||||
|
||||
@@ -33,4 +33,4 @@ superset load-test-users
|
||||
|
||||
echo "Running tests"
|
||||
|
||||
pytest --durations-min=2 --maxfail=1 --cov-report= --cov=superset ./tests/integration_tests "$@"
|
||||
pytest --durations-min=2 --cov-report= --cov=superset ./tests/integration_tests "$@"
|
||||
|
||||
@@ -94,67 +94,12 @@ describe('Charts list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('list mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('list');
|
||||
});
|
||||
|
||||
it('should load rows in list mode', () => {
|
||||
cy.getBySel('listview-table').should('be.visible');
|
||||
cy.getBySel('sort-header').eq(1).contains('Name');
|
||||
cy.getBySel('sort-header').eq(2).contains('Type');
|
||||
cy.getBySel('sort-header').eq(3).contains('Dataset');
|
||||
cy.getBySel('sort-header').eq(4).contains('On dashboards');
|
||||
cy.getBySel('sort-header').eq(5).contains('Owners');
|
||||
cy.getBySel('sort-header').eq(6).contains('Last modified');
|
||||
cy.getBySel('sort-header').eq(7).contains('Actions');
|
||||
});
|
||||
|
||||
it('should bulk select in list mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.get('[aria-label="Select all"]').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('card mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('card');
|
||||
});
|
||||
|
||||
it('should load rows in card mode', () => {
|
||||
cy.getBySel('listview-table').should('not.exist');
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
});
|
||||
|
||||
it('should bulk select in card mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.getBySel('styled-card').click({ multiple: true });
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
|
||||
it('should preserve other filters when sorting', () => {
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
setFilter('Type', 'Big Number');
|
||||
|
||||
@@ -54,7 +54,7 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
@@ -65,11 +65,16 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
.then($el => {
|
||||
cy.wrap($el)
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.trigger('keydown', { keyCode: 13, which: 13, force: true });
|
||||
});
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.click();
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).should('not.exist');
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
@@ -240,7 +245,7 @@ describe('Drill by modal', () => {
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('opens the modal from the context menu', () => {
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
@@ -529,7 +534,7 @@ describe('Drill by modal', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Bar Chart', () => {
|
||||
it.skip('Bar Chart', () => {
|
||||
testEchart('echarts_timeseries_bar', 'Bar Chart', [
|
||||
[85, 94],
|
||||
[490, 68],
|
||||
@@ -612,7 +617,7 @@ describe('Drill by modal', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Mixed Chart', () => {
|
||||
it.skip('Mixed Chart', () => {
|
||||
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
|
||||
// click 'boy'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
|
||||
@@ -154,6 +154,7 @@ 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);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import {
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
@@ -160,6 +161,74 @@ describe('Native filters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Dependent filter selects first item based on parent filter selection', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region', datasetId: 2 },
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
|
||||
enterNativeFilterEditModal();
|
||||
|
||||
selectFilter(0);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
// cannot use saveNativeFilterSettings because there is a bug which
|
||||
// sometimes does not allow charts to load when enabling the 'Select first filter value by default'
|
||||
// to be saved when using dependent filters so,
|
||||
// you reload the window.
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
cy.reload();
|
||||
|
||||
applyNativeFilterValueWithIndex(0, 'North America');
|
||||
|
||||
// Check that dependent filter auto-selects the first item
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterContent)
|
||||
.eq(1)
|
||||
.should('contain.text', 'Bermuda');
|
||||
});
|
||||
|
||||
it('User can create filter depend on 2 other filters', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region', datasetId: 2 },
|
||||
|
||||
@@ -68,11 +68,13 @@ function verifyDashboardSearch() {
|
||||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover', {
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
.click({ force: true });
|
||||
cy.wait('@get');
|
||||
}
|
||||
|
||||
|
||||
1644
superset-frontend/package-lock.json
generated
1644
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
|
||||
"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'",
|
||||
"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": "33.1.1",
|
||||
"ag-grid-react": "33.1.1",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "34.0.2",
|
||||
"antd": "^5.24.6",
|
||||
"chrono-node": "^2.7.8",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -145,7 +145,6 @@
|
||||
"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",
|
||||
@@ -317,7 +316,7 @@
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"open-cli": "^8.0.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.5.3",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-packagejson": "^2.5.3",
|
||||
"process": "^0.11.10",
|
||||
"react-resizable": "^3.0.5",
|
||||
@@ -350,7 +349,7 @@
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.16.0",
|
||||
"node": "^20.18.1",
|
||||
"npm": "^10.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.2",
|
||||
"jest": "^30.0.4",
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import { t, css } from '@superset-ui/core';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { InfoTooltip, Tooltip } from '@superset-ui/core/components';
|
||||
import { InfoTooltip, Tooltip, Icons } from '@superset-ui/core/components';
|
||||
|
||||
type ValidationError = string;
|
||||
|
||||
@@ -93,7 +92,7 @@ export function ControlHeader({
|
||||
<div className="ControlHeader" data-test={`${name}-header`}>
|
||||
<div className="pull-left">
|
||||
<label className="control-label" htmlFor={name}>
|
||||
{leftNode && <span>{leftNode}</span>}
|
||||
{leftNode && <>{leftNode}</>}
|
||||
<span
|
||||
role={onClick ? 'button' : undefined}
|
||||
{...(onClick ? { onClick, tabIndex: 0 } : {})}
|
||||
@@ -104,9 +103,9 @@ export function ControlHeader({
|
||||
{warning && (
|
||||
<span>
|
||||
<Tooltip id="error-tooltip" placement="top" title={warning}>
|
||||
<InfoCircleOutlined
|
||||
<Icons.InfoCircleOutlined
|
||||
iconSize="m"
|
||||
css={theme => css`
|
||||
font-size: ${theme.sizeUnit * 3}px;
|
||||
color: ${theme.colorError};
|
||||
`}
|
||||
/>
|
||||
@@ -116,9 +115,9 @@ export function ControlHeader({
|
||||
{danger && (
|
||||
<span>
|
||||
<Tooltip id="error-tooltip" placement="top" title={danger}>
|
||||
<InfoCircleOutlined
|
||||
<Icons.InfoCircleOutlined
|
||||
iconSize="m"
|
||||
css={theme => css`
|
||||
font-size: ${theme.sizeUnit * 3}px;
|
||||
color: ${theme.colorError};
|
||||
`}
|
||||
/>{' '}
|
||||
@@ -132,9 +131,9 @@ export function ControlHeader({
|
||||
placement="top"
|
||||
title={validationErrors.join(' ')}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
<Icons.InfoCircleOutlined
|
||||
iconSize="m"
|
||||
css={theme => css`
|
||||
font-size: ${theme.sizeUnit * 3}px;
|
||||
color: ${theme.colorError};
|
||||
`}
|
||||
/>{' '}
|
||||
|
||||
@@ -86,7 +86,9 @@ 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}>
|
||||
|
||||
@@ -30,7 +30,7 @@ const controlsWithoutXAxis: ControlSetRow[] = [
|
||||
['groupby'],
|
||||
[contributionModeControl],
|
||||
['adhoc_filters'],
|
||||
['limit'],
|
||||
['limit', 'group_others_when_limit_reached'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
['row_limit'],
|
||||
|
||||
@@ -41,6 +41,53 @@ 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: {
|
||||
@@ -69,17 +116,12 @@ export const aggregationControl = {
|
||||
default: 'LAST_VALUE',
|
||||
clearable: false,
|
||||
renderTrigger: false,
|
||||
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')],
|
||||
],
|
||||
choices: Object.entries(aggregationChoices).map(([value, { label }]) => [
|
||||
value,
|
||||
t(label),
|
||||
]),
|
||||
description: t(
|
||||
'Aggregation method used to compute the Big Number from the Trendline.For non-additive metrics like ratios, averages, distinct counts, etc use NONE.',
|
||||
'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.',
|
||||
),
|
||||
provideFormDataToProps: true,
|
||||
mapStateToProps: ({ form_data }: ControlPanelState) => ({
|
||||
|
||||
@@ -177,6 +177,7 @@ const granularity: SharedControlConfig<'SelectControl'> = {
|
||||
'can type and use simple natural language as in `10 seconds`, ' +
|
||||
'`1 day` or `56 weeks`',
|
||||
),
|
||||
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
|
||||
};
|
||||
|
||||
const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
|
||||
@@ -204,6 +205,7 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
|
||||
choices: (datasource as Dataset)?.time_grain_sqla || [],
|
||||
}),
|
||||
visibility: displayTimeRelatedControls,
|
||||
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
|
||||
};
|
||||
|
||||
const time_range: SharedControlConfig<'DateFilterControl'> = {
|
||||
@@ -283,6 +285,19 @@ 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',
|
||||
@@ -446,6 +461,7 @@ 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,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"d3-format": "^1.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"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": "^4.0.0",
|
||||
"reselect": "^5.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
|
||||
@@ -17,11 +17,8 @@
|
||||
* 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 } from 'reselect';
|
||||
import { createSelector, lruMemoize } from 'reselect';
|
||||
import {
|
||||
AppSection,
|
||||
Behavior,
|
||||
@@ -37,7 +34,7 @@ import {
|
||||
SetDataMaskHook,
|
||||
} from '../types/Base';
|
||||
import { QueryData, DataRecordFilters } from '..';
|
||||
import { SupersetTheme } from '../../theme';
|
||||
import { supersetTheme, SupersetTheme } from '../../theme';
|
||||
|
||||
// TODO: more specific typing for these fields of ChartProps
|
||||
type AnnotationData = PlainObject;
|
||||
@@ -109,6 +106,8 @@ export interface ChartPropsConfig {
|
||||
theme: SupersetTheme;
|
||||
/* legend index */
|
||||
legendIndex?: number;
|
||||
inContextMenu?: boolean;
|
||||
emitCrossFilters?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_WIDTH = 800;
|
||||
@@ -161,7 +160,11 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
||||
|
||||
theme: SupersetTheme;
|
||||
|
||||
constructor(config: ChartPropsConfig & { formData?: FormData } = {}) {
|
||||
constructor(
|
||||
config: ChartPropsConfig & { formData?: FormData } = {
|
||||
theme: supersetTheme,
|
||||
},
|
||||
) {
|
||||
const {
|
||||
annotationData = {},
|
||||
datasource = {},
|
||||
@@ -276,5 +279,16 @@ 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,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,6 +35,11 @@ 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;
|
||||
@@ -202,7 +207,7 @@ export function AsyncAceEditor(
|
||||
|
||||
/* Basic editor styles with dark mode support */
|
||||
.ace_editor.ace-github,
|
||||
.ace_editor.ace-textmate {
|
||||
.ace_editor.ace-tm {
|
||||
background-color: ${token.colorBgContainer} !important;
|
||||
color: ${token.colorText} !important;
|
||||
}
|
||||
@@ -265,7 +270,7 @@ export function AsyncAceEditor(
|
||||
/* Adjust tooltip styles */
|
||||
.ace_tooltip {
|
||||
margin-left: ${token.margin}px;
|
||||
padding: 0px;
|
||||
padding: ${token.sizeUnit * 2}px;
|
||||
background-color: ${token.colorBgElevated} !important;
|
||||
color: ${token.colorText} !important;
|
||||
border: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { 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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
* 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';
|
||||
@@ -70,7 +69,6 @@ export function Button(props: ButtonProps) {
|
||||
if (!buttonStyle || buttonStyle === 'primary') {
|
||||
variant = 'solid';
|
||||
antdType = 'primary';
|
||||
color = 'primary';
|
||||
} else if (buttonStyle === 'secondary') {
|
||||
variant = 'filled';
|
||||
color = 'primary';
|
||||
@@ -78,7 +76,6 @@ export function Button(props: ButtonProps) {
|
||||
variant = 'outlined';
|
||||
color = 'default';
|
||||
} else if (buttonStyle === 'dashed') {
|
||||
color = 'primary';
|
||||
variant = 'dashed';
|
||||
antdType = 'dashed';
|
||||
} else if (buttonStyle === 'danger') {
|
||||
@@ -90,6 +87,7 @@ export function Button(props: ButtonProps) {
|
||||
const element = children as ReactElement;
|
||||
|
||||
let renderedChildren = [];
|
||||
|
||||
if (element && element.type === Fragment) {
|
||||
renderedChildren = Children.toArray(element.props.children);
|
||||
} else {
|
||||
@@ -120,7 +118,7 @@ export function Button(props: ButtonProps) {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 1.5715,
|
||||
lineHeight: 1,
|
||||
fontSize: fontSizeSM,
|
||||
fontWeight: fontWeightStrong,
|
||||
height,
|
||||
@@ -134,6 +132,11 @@ export function Button(props: ButtonProps) {
|
||||
'& > span > :first-of-type': {
|
||||
marginRight: firstChildMargin,
|
||||
},
|
||||
':not(:hover)': effectiveButtonStyle === 'secondary' && {
|
||||
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
|
||||
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
|
||||
color: `${theme.colorPrimaryTextHover} !important`,
|
||||
},
|
||||
}}
|
||||
icon={icon}
|
||||
{...restProps}
|
||||
|
||||
@@ -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 { themeObject } from '@superset-ui/core';
|
||||
import { useTheme, isThemeDark } from '@superset-ui/core';
|
||||
|
||||
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
|
||||
|
||||
@@ -77,6 +77,7 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
wrapLines = true,
|
||||
style: overrideStyle,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [isLanguageReady, setIsLanguageReady] = useState(
|
||||
registeredLanguages.has(language),
|
||||
);
|
||||
@@ -92,14 +93,14 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
loadLanguage();
|
||||
}, [language]);
|
||||
|
||||
const isDark = themeObject.isThemeDark();
|
||||
const isDark = isThemeDark(theme);
|
||||
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
|
||||
|
||||
const defaultCustomStyle: React.CSSProperties = {
|
||||
background: themeObject.theme.colorBgElevated,
|
||||
padding: themeObject.theme.sizeUnit * 4,
|
||||
background: theme.colorBgElevated,
|
||||
padding: theme.sizeUnit * 4,
|
||||
border: 0,
|
||||
borderRadius: themeObject.theme.borderRadius,
|
||||
borderRadius: theme.borderRadius,
|
||||
...customStyle,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useTheme } from '@superset-ui/core';
|
||||
import { useTheme, css } 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 = false,
|
||||
validateCheckStatus,
|
||||
testId,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -37,36 +37,38 @@ export const CollapseLabelInModal: React.FC<CollapseLabelInModalProps> = ({
|
||||
return (
|
||||
<div data-test={testId}>
|
||||
<Typography.Title
|
||||
style={{
|
||||
marginTop: 0,
|
||||
marginBottom: theme.sizeUnit / 2,
|
||||
fontSize: theme.fontSizeLG,
|
||||
}}
|
||||
css={css`
|
||||
&& {
|
||||
margin-top: 0;
|
||||
margin-bottom: ${theme.sizeUnit / 2}px;
|
||||
font-size: ${theme.fontSizeLG}px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{title}{' '}
|
||||
{validateCheckStatus ? (
|
||||
<Icons.CheckCircleOutlined
|
||||
style={{ color: theme.colorSuccess }}
|
||||
aria-label="check-circle"
|
||||
role="img"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
color: theme.colorErrorText,
|
||||
fontSize: theme.fontSizeLG,
|
||||
}}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{validateCheckStatus !== undefined &&
|
||||
(validateCheckStatus ? (
|
||||
<Icons.CheckCircleOutlined
|
||||
iconColor={theme.colorSuccess}
|
||||
aria-label="check-circle"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
css={css`
|
||||
color: ${theme.colorErrorText};
|
||||
font-size: ${theme.fontSizeLG}px;
|
||||
`}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
))}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: theme.fontSizeSM,
|
||||
color: theme.colorTextDescription,
|
||||
}}
|
||||
css={css`
|
||||
margin: 0;
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
color: ${theme.colorTextDescription};
|
||||
`}
|
||||
>
|
||||
{subtitle}
|
||||
</Typography.Paragraph>
|
||||
|
||||
@@ -68,6 +68,7 @@ export function ConfirmStatusChange({
|
||||
onConfirm={confirm}
|
||||
onHide={hide}
|
||||
open={open}
|
||||
name="please confirm"
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -31,17 +31,13 @@ const StyledDiv = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const DescriptionContainer = styled.div`
|
||||
line-height: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
padding-top: 16px;
|
||||
`;
|
||||
|
||||
export function DeleteModal({
|
||||
description,
|
||||
onConfirm,
|
||||
onHide,
|
||||
open,
|
||||
title,
|
||||
name,
|
||||
}: DeleteModalProps) {
|
||||
const [disableChange, setDisableChange] = useState(true);
|
||||
const [confirmation, setConfirmation] = useState<string>('');
|
||||
@@ -81,12 +77,13 @@ export function DeleteModal({
|
||||
onHide={hide}
|
||||
onHandledPrimaryAction={confirm}
|
||||
primaryButtonName={t('Delete')}
|
||||
primaryButtonType="danger"
|
||||
primaryButtonStyle="danger"
|
||||
show={open}
|
||||
name={name}
|
||||
title={title}
|
||||
centered
|
||||
>
|
||||
<DescriptionContainer>{description}</DescriptionContainer>
|
||||
{description}
|
||||
<StyledDiv>
|
||||
<FormLabel htmlFor="delete">
|
||||
{t('Type "%s" to confirm', t('DELETE'))}
|
||||
|
||||
@@ -25,4 +25,5 @@ export interface DeleteModalProps {
|
||||
onHide: () => void;
|
||||
open: boolean;
|
||||
title: ReactNode;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ const ImageContainer = ({
|
||||
<Empty
|
||||
description={false}
|
||||
image={mappedImage}
|
||||
imageStyle={getImageHeight(size)}
|
||||
styles={{ image: getImageHeight(size) }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -144,6 +144,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
description = t('There is currently no information to display.'),
|
||||
image = 'empty.svg',
|
||||
buttonText,
|
||||
buttonIcon,
|
||||
buttonAction,
|
||||
size = 'medium',
|
||||
children,
|
||||
@@ -165,6 +166,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
)}
|
||||
{buttonText && buttonAction && (
|
||||
<Button
|
||||
icon={buttonIcon}
|
||||
buttonStyle="primary"
|
||||
onClick={buttonAction}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactNode, SyntheticEvent } from 'react';
|
||||
import type { IconType } from '@superset-ui/core/components';
|
||||
|
||||
export type EmptyStateSize = 'small' | 'medium' | 'large';
|
||||
|
||||
@@ -25,6 +26,7 @@ export type EmptyStateProps = {
|
||||
description?: ReactNode;
|
||||
image?: ReactNode | string;
|
||||
buttonText?: ReactNode;
|
||||
buttonIcon?: IconType;
|
||||
buttonAction?: (event: SyntheticEvent) => void;
|
||||
size?: EmptyStateSize;
|
||||
children?: ReactNode;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { css, useTheme, themeObject } from '../..';
|
||||
import { css, useTheme, getFontSize } 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
|
||||
? `${themeObject.getFontSize(iconSize)}px`
|
||||
? `${getFontSize(theme, iconSize)}px`
|
||||
: `${theme.fontSize}px`,
|
||||
cursor: rest?.onClick ? 'pointer' : undefined,
|
||||
};
|
||||
@@ -76,12 +76,12 @@ export const BaseIconComponent: React.FC<
|
||||
style={style}
|
||||
width={
|
||||
iconSize
|
||||
? `${themeObject.getFontSize(iconSize) || theme.fontSize}px`
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
height={
|
||||
iconSize
|
||||
? `${themeObject.getFontSize(iconSize) || theme.fontSize}px`
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
{...(rest as CustomIconType)}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { KeyboardEvent, useMemo } from 'react';
|
||||
import { SerializedStyles, CSSObject } from '@emotion/react';
|
||||
import { kebabCase } from 'lodash';
|
||||
import { css, t, useTheme, themeObject } from '@superset-ui/core';
|
||||
import { css, t, useTheme, getFontSize } from '@superset-ui/core';
|
||||
import {
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
@@ -68,7 +68,7 @@ export const InfoTooltip = ({
|
||||
|
||||
const iconCss = css`
|
||||
color: ${variant?.color ?? theme.colorIcon};
|
||||
font-size: ${themeObject.getFontSize(iconSize)}px;
|
||||
font-size: ${getFontSize(theme, iconSize)}px;
|
||||
`;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { Tag } from '@superset-ui/core/components/Tag';
|
||||
import { css } from '@emotion/react';
|
||||
import { useTheme, themeObject } from '@superset-ui/core';
|
||||
import { useTheme, getColorVariants } 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 = themeObject.getColorVariants(type);
|
||||
const baseColor = getColorVariants(theme, type);
|
||||
const color = baseColor.active;
|
||||
const borderColor = baseColor.border;
|
||||
const backgroundColor = baseColor.bg;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { useEffect, useState, FunctionComponent } from 'react';
|
||||
|
||||
import { t, styled, css } from '@superset-ui/core';
|
||||
import { t, styled, css, useTheme } from '@superset-ui/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { extendedDayjs } from '../../utils/dates';
|
||||
import { Icons } from '../Icons';
|
||||
@@ -38,24 +38,14 @@ extendedDayjs.updateLocale('en', {
|
||||
});
|
||||
|
||||
const TextStyles = styled.span`
|
||||
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;
|
||||
`};
|
||||
color: ${({ theme }) => theme.colorText};
|
||||
`;
|
||||
|
||||
export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
|
||||
updatedAt,
|
||||
update,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [timeSince, setTimeSince] = useState<dayjs.Dayjs>(
|
||||
extendedDayjs(updatedAt),
|
||||
);
|
||||
@@ -75,10 +65,9 @@ export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
|
||||
<TextStyles>
|
||||
{t('Last Updated %s', timeSince.isValid() ? timeSince.calendar() : '--')}
|
||||
{update && (
|
||||
<RefreshIcon
|
||||
iconSize="l"
|
||||
<Icons.SyncOutlined
|
||||
css={css`
|
||||
vertical-align: text-bottom;
|
||||
margin-left: ${theme.sizeUnit * 2}px;
|
||||
`}
|
||||
onClick={update}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ export function FormModal({
|
||||
formSubmitHandler,
|
||||
bodyStyle = {},
|
||||
requiredFields = [],
|
||||
name,
|
||||
}: FormModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -78,6 +79,7 @@ export function FormModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
name={name}
|
||||
show={show}
|
||||
title={title}
|
||||
onHide={handleClose}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const InteractiveModal = (props: ModalProps) => (
|
||||
InteractiveModal.args = {
|
||||
disablePrimaryButton: false,
|
||||
primaryButtonName: 'Danger',
|
||||
primaryButtonType: 'danger',
|
||||
primaryButtonStyle: 'danger',
|
||||
show: true,
|
||||
title: "I'm a modal!",
|
||||
resizable: false,
|
||||
|
||||
@@ -77,7 +77,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
.ant-modal-header {
|
||||
flex: 0 0 auto;
|
||||
border-radius: ${theme.borderRadius}px ${theme.borderRadius}px 0 0;
|
||||
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
|
||||
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
@@ -121,7 +121,8 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 0 1 auto;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
|
||||
|
||||
overflow: auto;
|
||||
${!resizable && height && `height: ${height};`}
|
||||
}
|
||||
@@ -208,7 +209,7 @@ const CustomModal = ({
|
||||
onHide,
|
||||
onHandledPrimaryAction,
|
||||
primaryButtonName = t('OK'),
|
||||
primaryButtonType = 'primary',
|
||||
primaryButtonStyle = 'primary',
|
||||
show,
|
||||
name,
|
||||
title,
|
||||
@@ -261,7 +262,7 @@ const CustomModal = ({
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
buttonStyle={primaryButtonType}
|
||||
buttonStyle={primaryButtonStyle}
|
||||
disabled={disablePrimaryButton}
|
||||
tooltip={primaryTooltipMessage}
|
||||
loading={primaryButtonLoading}
|
||||
@@ -332,7 +333,7 @@ const CustomModal = ({
|
||||
}
|
||||
footer={!hideFooter ? modalFooter : null}
|
||||
hideFooter={hideFooter}
|
||||
wrapProps={{ 'data-test': `${name || title}-modal`, ...wrapProps }}
|
||||
wrapProps={{ 'data-test': `${name || 'antd'}-modal`, ...wrapProps }}
|
||||
modalRender={modal =>
|
||||
resizable || draggable ? (
|
||||
<Draggable
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { CSSProperties, ReactNode } from 'react';
|
||||
import type { ModalFuncProps } from 'antd';
|
||||
import type { ResizableProps } from 're-resizable';
|
||||
import type { DraggableProps } from 'react-draggable';
|
||||
import { ButtonStyle } from '../Button/types';
|
||||
|
||||
export interface ModalProps {
|
||||
className?: string;
|
||||
@@ -30,7 +31,7 @@ export interface ModalProps {
|
||||
onHide: () => void;
|
||||
onHandledPrimaryAction?: () => void;
|
||||
primaryButtonName?: string;
|
||||
primaryButtonType?: 'primary' | 'danger';
|
||||
primaryButtonStyle?: ButtonStyle;
|
||||
show: boolean;
|
||||
name?: string;
|
||||
title: ReactNode;
|
||||
|
||||
@@ -110,6 +110,7 @@ export const ModalTrigger = forwardRef(
|
||||
className={className}
|
||||
show={showModal}
|
||||
onHide={close}
|
||||
name={modalTitle}
|
||||
title={modalTitle}
|
||||
footer={modalFooter}
|
||||
hideFooter={!modalFooter}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ interface TelemetryPixelProps {
|
||||
|
||||
const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0';
|
||||
|
||||
const TelemetryPixel = ({
|
||||
export const TelemetryPixel = ({
|
||||
version = 'unknownVersion',
|
||||
sha = 'unknownSHA',
|
||||
build = 'unknownBuild',
|
||||
@@ -56,4 +56,3 @@ const TelemetryPixel = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default TelemetryPixel;
|
||||
|
||||
@@ -1,150 +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 { 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;
|
||||
@@ -1,79 +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 { 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;
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,7 @@
|
||||
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)`
|
||||
|
||||
@@ -16,52 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
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 { t } from '@superset-ui/core';
|
||||
import { Icons, Modal, Typography, Button } from '@superset-ui/core/components';
|
||||
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;
|
||||
@@ -86,44 +44,22 @@ export const UnsavedChangesModal: FC<UnsavedChangesModalProps> = ({
|
||||
show={showModal}
|
||||
width="444px"
|
||||
title={
|
||||
<div
|
||||
css={css`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<StyledWarningIcon iconSize="xl" />
|
||||
<StyledModalTitle type="secondary" level={5}>
|
||||
{title}
|
||||
</StyledModalTitle>
|
||||
</div>
|
||||
<>
|
||||
<Icons.WarningOutlined iconSize="m" style={{ marginRight: 8 }} />
|
||||
{title}
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<StyledDiscardBtn
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
onClick={onConfirmNavigation}
|
||||
>
|
||||
<>
|
||||
<Button buttonStyle="secondary" onClick={onConfirmNavigation}>
|
||||
{t('Discard')}
|
||||
</StyledDiscardBtn>
|
||||
<StyledSaveBtn
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
</Button>
|
||||
<Button buttonStyle="primary" onClick={handleSave}>
|
||||
{t('Save')}
|
||||
</StyledSaveBtn>
|
||||
</div>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StyledModalBody type="secondary">{body}</StyledModalBody>
|
||||
<Typography.Text>{body}</Typography.Text>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -148,6 +148,7 @@ export {
|
||||
Typography,
|
||||
type TypographyProps,
|
||||
type ParagraphProps,
|
||||
type TitleProps,
|
||||
} from './Typography';
|
||||
|
||||
export { Image, type ImageProps } from './Image';
|
||||
@@ -163,6 +164,8 @@ 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';
|
||||
|
||||
@@ -32,7 +32,7 @@ export const CACHE_KEY = '@SUPERSET-UI/CONNECTION';
|
||||
export const DEFAULT_FETCH_RETRY_OPTIONS: FetchRetryOptions = {
|
||||
retries: 3,
|
||||
retryDelay: 1000,
|
||||
retryOn: [503],
|
||||
retryOn: [502, 503, 504],
|
||||
};
|
||||
|
||||
export const COMMON_ERR_MESSAGES = {
|
||||
|
||||
@@ -63,6 +63,7 @@ export default function buildQueryObject<T extends QueryFormData>(
|
||||
series_columns,
|
||||
series_limit,
|
||||
series_limit_metric,
|
||||
group_others_when_limit_reached,
|
||||
...residualFormData
|
||||
} = formData;
|
||||
const {
|
||||
@@ -128,6 +129,7 @@ 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,
|
||||
|
||||
@@ -189,24 +189,6 @@ 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({
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
// 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 */
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
} from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { noop } from 'lodash';
|
||||
import { isThemeDark } from './utils/themeUtils';
|
||||
import { GlobalStyles } from './GlobalStyles';
|
||||
|
||||
import {
|
||||
@@ -53,9 +53,7 @@ import {
|
||||
SupersetTheme,
|
||||
allowedAntdTokens,
|
||||
SharedAntdTokens,
|
||||
ColorVariants,
|
||||
DeprecatedThemeColors,
|
||||
FontSizeKey,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
@@ -102,15 +100,6 @@ 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);
|
||||
|
||||
@@ -183,7 +172,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 = this.isThemeDark(); // Now we can safely call this
|
||||
const isDark = isThemeDark(this.theme); // Use utility function with theme
|
||||
this.theme.colors = getDeprecatedColors(systemColors, isDark);
|
||||
|
||||
// Update the providers with the fully formed theme
|
||||
@@ -201,22 +190,6 @@ 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 };
|
||||
@@ -250,45 +223,6 @@ 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,
|
||||
|
||||
@@ -26,7 +26,9 @@ import {
|
||||
type ThemeStorage,
|
||||
type ThemeControllerOptions,
|
||||
type ThemeContextType,
|
||||
type SupersetThemeConfig,
|
||||
ThemeAlgorithm,
|
||||
ThemeMode,
|
||||
} from './types';
|
||||
|
||||
export {
|
||||
@@ -66,7 +68,16 @@ const themeObject: Theme = Theme.fromConfig({
|
||||
const { theme } = themeObject;
|
||||
const supersetTheme = theme;
|
||||
|
||||
export { Theme, themeObject, styled, theme, supersetTheme };
|
||||
export {
|
||||
Theme,
|
||||
ThemeAlgorithm,
|
||||
ThemeMode,
|
||||
themeObject,
|
||||
styled,
|
||||
theme,
|
||||
supersetTheme,
|
||||
};
|
||||
|
||||
export type {
|
||||
SupersetTheme,
|
||||
SerializableThemeConfig,
|
||||
@@ -74,4 +85,8 @@ export type {
|
||||
ThemeStorage,
|
||||
ThemeControllerOptions,
|
||||
ThemeContextType,
|
||||
SupersetThemeConfig,
|
||||
};
|
||||
|
||||
// Export theme utility functions
|
||||
export * from './utils/themeUtils';
|
||||
|
||||
@@ -411,6 +411,7 @@ export interface ThemeControllerOptions {
|
||||
onChange?: (theme: Theme) => void;
|
||||
canUpdateTheme?: () => boolean;
|
||||
canUpdateMode?: () => boolean;
|
||||
isGlobalContext?: boolean;
|
||||
}
|
||||
|
||||
export interface ThemeContextType {
|
||||
@@ -419,4 +420,25 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@@ -30,6 +30,7 @@ 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',
|
||||
@@ -53,8 +54,6 @@ 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',
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('ChartProps', () => {
|
||||
});
|
||||
expect(props1).not.toBe(props2);
|
||||
});
|
||||
it('selector returns a new chartProps if some input fields change', () => {
|
||||
it('selector returns a new chartProps if some input fields change and returns memoized chart props', () => {
|
||||
const props1 = selector({
|
||||
width: 800,
|
||||
height: 600,
|
||||
@@ -145,7 +145,7 @@ describe('ChartProps', () => {
|
||||
theme: supersetTheme,
|
||||
});
|
||||
expect(props1).not.toBe(props2);
|
||||
expect(props1).not.toBe(props3);
|
||||
expect(props1).toBe(props3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
getSequentialSchemeRegistry,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
import Datamap from 'datamaps/dist/datamaps.world.min';
|
||||
import Datamap from 'datamaps/dist/datamaps.all.min';
|
||||
import { ColorBy } from './utils';
|
||||
|
||||
const propTypes = {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
spatial,
|
||||
viewport,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -53,7 +54,10 @@ const config: ControlPanelConfig = {
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
[mapboxStyle],
|
||||
...generateDeckGLColorSchemeControls({}),
|
||||
...generateDeckGLColorSchemeControls({
|
||||
defaultSchemeType: COLOR_SCHEME_TYPES.categorical_palette,
|
||||
disableCategoricalColumn: true,
|
||||
}),
|
||||
[viewport],
|
||||
[autozoom],
|
||||
[gridSize],
|
||||
|
||||
@@ -37,4 +37,4 @@ export function formatSelectOptions(options: (string | number)[]) {
|
||||
export const isColorSchemeTypeVisible = (
|
||||
controls: ControlStateMapping,
|
||||
colorSchemeType: ColorSchemeType,
|
||||
) => controls.color_scheme_type.value === colorSchemeType;
|
||||
) => controls.color_scheme_type?.value === colorSchemeType;
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@types/d3-array": "^2.9.0",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"ag-grid-community": "^33.1.1",
|
||||
"ag-grid-react": "^33.1.1",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "^34.0.2",
|
||||
"classnames": "^2.5.1",
|
||||
"d3-array": "^2.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user