mirror of
https://github.com/apache/superset.git
synced 2026-05-05 16:04:19 +00:00
Compare commits
150 Commits
query-iden
...
move-contr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e85ee6d9 | ||
|
|
361a7f0f94 | ||
|
|
184f800ec1 | ||
|
|
a5bc492a95 | ||
|
|
544236ff20 | ||
|
|
2670c3e951 | ||
|
|
0f60a8d57b | ||
|
|
51c40dc971 | ||
|
|
5c90fca556 | ||
|
|
859e627c30 | ||
|
|
980c06e7d7 | ||
|
|
204b32e4a0 | ||
|
|
3603775df1 | ||
|
|
cf3b93b7bc | ||
|
|
df772a9afa | ||
|
|
d01c038471 | ||
|
|
7f4a3a3d0f | ||
|
|
c198b990a3 | ||
|
|
286b4d81e9 | ||
|
|
446beb4d2e | ||
|
|
0ea89c1c57 | ||
|
|
26f0556bef | ||
|
|
1137185842 | ||
|
|
97913203e1 | ||
|
|
06b98c1095 | ||
|
|
fe0ea69280 | ||
|
|
761daec53d | ||
|
|
407fb67f1e | ||
|
|
49689eec6c | ||
|
|
791ea9860d | ||
|
|
2f8939d229 | ||
|
|
ccf6290120 | ||
|
|
96a1aa60e8 | ||
|
|
2ea0368c2d | ||
|
|
9e407e4e80 | ||
|
|
360e58c181 | ||
|
|
22d5eb7835 | ||
|
|
7c4a77a909 | ||
|
|
4e209e51d0 | ||
|
|
7191ae55c8 | ||
|
|
17725ebc83 | ||
|
|
1a7a381bd5 | ||
|
|
daf207e5c2 | ||
|
|
72294c569f | ||
|
|
792dd08d38 | ||
|
|
1e40e7d02b | ||
|
|
7e98c75f01 | ||
|
|
b18de05ea4 | ||
|
|
9300652277 | ||
|
|
7c2ec4ca5f | ||
|
|
6a83b6fd87 | ||
|
|
659cd33749 | ||
|
|
cb27d5fe8d | ||
|
|
6c9cda758a | ||
|
|
967134f540 | ||
|
|
25bb353f9d | ||
|
|
9cf2472291 | ||
|
|
cf5b976659 | ||
|
|
70394e79ef | ||
|
|
ea64f3122e | ||
|
|
50197fc33e | ||
|
|
c480fa7fcf | ||
|
|
6fc734da51 | ||
|
|
762a11b0bb | ||
|
|
f168dd69a8 | ||
|
|
becd0b8883 | ||
|
|
fd4570625a | ||
|
|
54a5b58e40 | ||
|
|
a611278e04 | ||
|
|
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 | ||
|
|
fe1fddde05 | ||
|
|
7e67deead7 | ||
|
|
6e02603098 | ||
|
|
4518f6999c | ||
|
|
aff847b3af | ||
|
|
b24aca0304 | ||
|
|
2c453035e4 | ||
|
|
4fe11869fc | ||
|
|
a0a49f9300 | ||
|
|
29d2fac485 | ||
|
|
0c5da6cb5d | ||
|
|
da6947d295 | ||
|
|
2db8f809ba | ||
|
|
5912fad745 | ||
|
|
dc41c45bec | ||
|
|
88ee90c579 | ||
|
|
bbb2279644 | ||
|
|
1958df6b83 | ||
|
|
58bd3bfcf0 | ||
|
|
d6eb6e08d0 |
40
.claude_rc
Normal file
40
.claude_rc
Normal file
@@ -0,0 +1,40 @@
|
||||
# Claude Code RC for move-controls
|
||||
|
||||
This is a claudette-managed Apache Superset development environment.
|
||||
|
||||
## Project: move-controls
|
||||
- Worktree Path: /Users/evan_1/.claudette/worktrees/move-controls
|
||||
- Frontend Port: 9004
|
||||
- Frontend URL: http://localhost:9004
|
||||
|
||||
## Quick Commands
|
||||
|
||||
Start services:
|
||||
```bash
|
||||
claudette docker up
|
||||
```
|
||||
|
||||
Access frontend:
|
||||
```bash
|
||||
open http://localhost:9004
|
||||
```
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
# Backend
|
||||
pytest tests/unit_tests/
|
||||
|
||||
# Frontend
|
||||
cd superset-frontend && npm test
|
||||
```
|
||||
|
||||
## Environment Details
|
||||
- Python venv: `.venv/` (auto-activated in claudette shell)
|
||||
- Node modules: `superset-frontend/node_modules/`
|
||||
- Docker prefix: `move-controls_`
|
||||
|
||||
## Development Tips
|
||||
- Always use `claudette shell` to work in this project
|
||||
- Run `pre-commit run --all-files` before committing
|
||||
- Use `claudette docker` instead of docker-compose directly
|
||||
- The frontend dev server runs on port 9004 to avoid conflicts
|
||||
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
|
||||
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -42,7 +42,7 @@ body:
|
||||
options:
|
||||
- master / latest-dev
|
||||
- "5.0.0"
|
||||
- "4.1.2"
|
||||
- "4.1.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
1
.github/copilot-instructions.md
vendored
Symbolic link
1
.github/copilot-instructions.md
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../LLMS.md
|
||||
82
.github/workflows/claude.yml
vendored
Normal file
82
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Claude PR Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
allowed: ${{ steps.check.outputs.allowed }}
|
||||
steps:
|
||||
- name: Check if user is allowed
|
||||
id: check
|
||||
run: |
|
||||
# List of allowed users
|
||||
ALLOWED_USERS="mistercrunch,rusackas"
|
||||
|
||||
# Get the commenter's username
|
||||
COMMENTER="${{ github.event.comment.user.login }}"
|
||||
|
||||
echo "Checking permissions for user: $COMMENTER"
|
||||
|
||||
# Check if user is in allowed list
|
||||
if [[ ",$ALLOWED_USERS," == *",$COMMENTER,"* ]]; then
|
||||
echo "allowed=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ User $COMMENTER is allowed to use Claude"
|
||||
else
|
||||
echo "allowed=false" >> $GITHUB_OUTPUT
|
||||
echo "❌ User $COMMENTER is not allowed to use Claude"
|
||||
fi
|
||||
|
||||
deny-access:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
|
||||
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
|
||||
|
||||
If you believe you should have access, please contact a project maintainer.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: message
|
||||
});
|
||||
|
||||
claude-code-action:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
@@ -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 }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -26,6 +26,7 @@ __pycache__
|
||||
.cache
|
||||
.bento*
|
||||
.cache-loader
|
||||
.claude_rc
|
||||
.coverage
|
||||
cover
|
||||
.DS_Store
|
||||
@@ -92,6 +93,7 @@ scripts/*.zip
|
||||
# IntelliJ
|
||||
*.iml
|
||||
venv
|
||||
.venv
|
||||
@eaDir/
|
||||
|
||||
# PyCharm
|
||||
@@ -126,4 +128,7 @@ docker/*local*
|
||||
# Jest test report
|
||||
test-report.html
|
||||
superset/static/stats/statistics.html
|
||||
|
||||
# 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
|
||||
|
||||
58
CHANGELOG/4.1.3.md
Normal file
58
CHANGELOG/4.1.3.md
Normal file
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.3 (Thu May 29 02:31:07 2025 -0500)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#33522](https://github.com/apache/superset/pull/33522) fix(Sqllab): Autocomplete got stuck in UI when open it too fast (@rebenitez1802)
|
||||
- [#33425](https://github.com/apache/superset/pull/33425) fix(table-chart): time shift is not working (@justinpark)
|
||||
- [#32414](https://github.com/apache/superset/pull/32414) fix(api): Added uuid to list api calls (@withnale)
|
||||
- [#33354](https://github.com/apache/superset/pull/33354) fix: loading examples from raw.githubusercontent.com fails with 429 errors (@mistercrunch)
|
||||
- [#32382](https://github.com/apache/superset/pull/32382) fix(pinot): revert join and subquery flags (@yuribogomolov)
|
||||
- [#32473](https://github.com/apache/superset/pull/32473) fix(plugin-chart-echarts): remove erroneous upper bound value (@villebro)
|
||||
- [#33048](https://github.com/apache/superset/pull/33048) fix: improve error type on parse error (@justinpark)
|
||||
- [#32968](https://github.com/apache/superset/pull/32968) fix(pivot-table): Revert "fix(Pivot Table): Fix column width to respect currency config (#31414)" (@justinpark)
|
||||
- [#32795](https://github.com/apache/superset/pull/32795) fix(log): store navigation path to get correct logging path (@justinpark)
|
||||
- [#33216](https://github.com/apache/superset/pull/33216) fix: Downgrade to marshmallow<4 (@amotl)
|
||||
- [#32866](https://github.com/apache/superset/pull/32866) fix: make packages PEP 625 compliant (@sadpandajoe)
|
||||
- [#32035](https://github.com/apache/superset/pull/32035) fix(fe/dashboard-list): display modifier info for `Last modified` data (@hainenber)
|
||||
- [#32708](https://github.com/apache/superset/pull/32708) fix(logging): missing path in event data (@justinpark)
|
||||
- [#32699](https://github.com/apache/superset/pull/32699) fix: Signature of Celery pruner jobs (@michael-s-molina)
|
||||
- [#32681](https://github.com/apache/superset/pull/32681) fix(log): Update recent_activity by event name (@justinpark)
|
||||
- [#32608](https://github.com/apache/superset/pull/32608) fix(welcome): perf on distinct recent activities (@justinpark)
|
||||
- [#32572](https://github.com/apache/superset/pull/32572) fix: Log table retention policy (@michael-s-molina)
|
||||
- [#32406](https://github.com/apache/superset/pull/32406) fix(model/helper): represent RLS filter clause in proper textual SQL string (@hainenber)
|
||||
- [#32240](https://github.com/apache/superset/pull/32240) fix: upgrade to 3.11.11-slim-bookworm to address critical vulnerabilities (@gpchandran)
|
||||
- [#30858](https://github.com/apache/superset/pull/30858) fix(chart data): removing query from /chart/data payload when accessing as guest user (@fisjac)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#33612](https://github.com/apache/superset/pull/33612) chore: update Dockerfile - Upgrade to 3.11.12 (@gpchandran)
|
||||
- [#33435](https://github.com/apache/superset/pull/33435) docs: CVEs fixed on 4.1.2 (@sha174n)
|
||||
- [#33339](https://github.com/apache/superset/pull/33339) chore(🦾): bump python h11 0.14.0 -> 0.16.0 (@github-actions[bot])
|
||||
- [#32745](https://github.com/apache/superset/pull/32745) chore(🦾): bump python sqlglot 26.1.3 -> 26.11.1 (@github-actions[bot])
|
||||
- [#32782](https://github.com/apache/superset/pull/32782) chore: Revert "chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm`" (@sadpandajoe)
|
||||
- [#32780](https://github.com/apache/superset/pull/32780) chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm` (@gpchandran)
|
||||
802
CONTROL_PANEL_MIGRATION_AGENT.md
Normal file
802
CONTROL_PANEL_MIGRATION_AGENT.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# Control Panel Migration Agent
|
||||
|
||||
A comprehensive guide for migrating Apache Superset control panels from the legacy config-based approach to the new React-based approach.
|
||||
|
||||
## Overview
|
||||
|
||||
This migration transforms control panels from complex string-referenced configurations (`controlPanelSections`/`controlSetRows`) to pure React components. The new approach provides:
|
||||
|
||||
- **Direct React components** instead of config objects
|
||||
- **Full TypeScript support** with proper type safety
|
||||
- **Simplified architecture** with no JSON intermediary
|
||||
- **Better developer experience** with IDE autocomplete and refactoring support
|
||||
|
||||
## Architecture Comparison
|
||||
|
||||
### Legacy Architecture
|
||||
```typescript
|
||||
// Old approach: Config-based with string references and config objects
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[GroupByControl()], // String reference
|
||||
[MetricControl()], // String reference
|
||||
[
|
||||
{
|
||||
name: 'show_labels',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Labels'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
], // Config object
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### New React-Based Architecture
|
||||
```typescript
|
||||
// New approach: Pure React components with direct JSX
|
||||
export const PieControlPanel: FC<PieControlPanelProps> = ({ ... }) => {
|
||||
return (
|
||||
<div>
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
// ... other props
|
||||
/>
|
||||
<CheckboxControl
|
||||
label={t('Show Labels')}
|
||||
value={formValues.show_labels ?? true}
|
||||
onChange={handleChange('show_labels')}
|
||||
renderTrigger
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Mark as modern panel
|
||||
(PieControlPanel as any).isModernPanel = true;
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. File Structure
|
||||
|
||||
Keep the existing file location but change from config to React component:
|
||||
- `controlPanel.ts` → `[ChartName]ControlPanelSimple.tsx`
|
||||
- The file remains in the same directory as the original
|
||||
|
||||
### 2. Update Imports
|
||||
|
||||
**From (Legacy):**
|
||||
```typescript
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
sharedControls,
|
||||
GroupByControl,
|
||||
MetricControl
|
||||
} from '@superset-ui/chart-controls';
|
||||
```
|
||||
|
||||
**To (Modern):**
|
||||
```typescript
|
||||
import { FC, useState } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Tabs } from 'antd';
|
||||
import {
|
||||
ColorSchemeControl,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
// Direct component imports
|
||||
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
|
||||
import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
|
||||
import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
|
||||
import TextControl from '../../../../src/explore/components/controls/TextControl';
|
||||
import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
|
||||
import SliderControl from '../../../../src/explore/components/controls/SliderControl';
|
||||
import SelectControl from '../../../../src/explore/components/controls/SelectControl';
|
||||
import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
|
||||
import ControlHeader from '../../../../src/explore/components/ControlHeader';
|
||||
import Control from '../../../../src/explore/components/Control';
|
||||
```
|
||||
|
||||
### 3. Component Structure Template
|
||||
|
||||
Create a React component with this structure:
|
||||
|
||||
```typescript
|
||||
interface [ChartName]ControlPanelProps {
|
||||
onChange?: (field: string, value: any) => void;
|
||||
value?: Record<string, any>;
|
||||
datasource?: any;
|
||||
actions?: any;
|
||||
controls?: any;
|
||||
form_data?: any;
|
||||
}
|
||||
|
||||
export const [ChartName]ControlPanel: FC<[ChartName]ControlPanelProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
datasource,
|
||||
form_data,
|
||||
actions,
|
||||
controls,
|
||||
}) => {
|
||||
// Safety checks for datasource
|
||||
if (!datasource || !form_data) {
|
||||
return <div>Loading control panel...</div>;
|
||||
}
|
||||
|
||||
// Ensure safe data structures
|
||||
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
|
||||
const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
|
||||
|
||||
// Helper for control changes
|
||||
const handleChange = (field: string) => (val: any) => {
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(field, val);
|
||||
} else if (onChange) {
|
||||
onChange(field, val);
|
||||
}
|
||||
};
|
||||
|
||||
// Get form values
|
||||
const formValues = form_data || value || {};
|
||||
|
||||
// Tab state (if using tabs)
|
||||
const [activeTab, setActiveTab] = useState('data');
|
||||
|
||||
// Component implementation here...
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* Your controls here */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// CRITICAL: Mark as modern panel
|
||||
([ChartName]ControlPanel as any).isModernPanel = true;
|
||||
|
||||
// Export wrapper config for compatibility
|
||||
const config = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: null,
|
||||
expanded: true,
|
||||
controlSetRows: [[[ChartName]ControlPanel as any]],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
// Move all defaults here
|
||||
field_name: {
|
||||
default: defaultValue,
|
||||
label: t('Field Label'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### 4. Control Mapping Reference
|
||||
|
||||
#### String References → React Components
|
||||
|
||||
| Legacy String | Modern React Component | Notes |
|
||||
|---------------|----------------------|--------|
|
||||
| `['groupby']` | `<DndColumnSelect ... />` | Multi-select columns |
|
||||
| `['metric']` | `<DndMetricSelect ... />` | Single metric select |
|
||||
| `['metrics']` | `<DndMetricSelect ... />` | Multi metric select |
|
||||
| `['adhoc_filters']` | `<DndFilterSelect ... />` | Advanced filters |
|
||||
| `['row_limit']` | `<TextControl isInt ... />` | Numeric input |
|
||||
| `['color_scheme']` | `ColorSchemeControl()` with `Control` wrapper | Special handling needed |
|
||||
|
||||
#### Config Objects → React Components
|
||||
|
||||
| Legacy Config | Modern Component | Example |
|
||||
|---------------|------------------|---------|
|
||||
| `{ type: 'TextControl', ... }` | `<TextControl ... />` | `<TextControl value={val} onChange={fn} />` |
|
||||
| `{ type: 'CheckboxControl', ... }` | `<CheckboxControl ... />` | `<CheckboxControl label="..." value={val} />` |
|
||||
| `{ type: 'SelectControl', ... }` | `<SelectControl ... />` | `<SelectControl choices={[...]} value={val} />` |
|
||||
| `{ type: 'SliderControl', ... }` | `<SliderControl ... />` | `<SliderControl {...{min: 0, max: 100}} />` |
|
||||
|
||||
### 5. Props Mapping Guide
|
||||
|
||||
| Legacy Config Property | Modern React Prop | Notes |
|
||||
|----------------------|------------------|-------|
|
||||
| `label` | `label` prop OR `<ControlHeader>` | Use ControlHeader for tooltips |
|
||||
| `description` | `description` prop OR `<ControlHeader>` | ControlHeader for complex descriptions |
|
||||
| `default` | Move to `controlOverrides` | Don't set as component prop |
|
||||
| `renderTrigger: true` | `renderTrigger` prop | Controls instant chart updates |
|
||||
| `visibility` | Conditional rendering | `{condition && <Control />}` |
|
||||
| `choices` | `choices` prop | For SelectControl |
|
||||
| `min/max/step` | Spread object | `{...{ min: 10, max: 100, step: 1 }}` |
|
||||
|
||||
### 6. Implementing Tabbed Layout
|
||||
|
||||
Most modern control panels use a Data/Customize tab structure:
|
||||
|
||||
```typescript
|
||||
const [activeTab, setActiveTab] = useState('data');
|
||||
|
||||
const dataTabContent = (
|
||||
<div>
|
||||
{/* Query-related controls: columns, metrics, filters, row limit */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<ControlHeader
|
||||
label={t('Group by')}
|
||||
description={t('Columns to group by')}
|
||||
hovered
|
||||
/>
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
options={safeColumns}
|
||||
name="groupby"
|
||||
label="" // Avoid duplicate labels
|
||||
multi
|
||||
canDelete
|
||||
ghostButtonText={t('Add dimension')}
|
||||
type="DndColumnSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
{/* More data controls... */}
|
||||
</div>
|
||||
);
|
||||
|
||||
const customizeTabContent = (
|
||||
<div>
|
||||
{/* Styling and display controls */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Chart Options')}</h4>
|
||||
{/* Styling controls... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabItems = [
|
||||
{ key: 'data', label: t('Data'), children: dataTabContent },
|
||||
{ key: 'customize', label: t('Customize'), children: customizeTabContent },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 7. Common Control Patterns
|
||||
|
||||
#### Drag-and-Drop Controls
|
||||
```typescript
|
||||
{/* Group By - Column Selection */}
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
options={safeColumns}
|
||||
name="groupby"
|
||||
label="" // Empty to avoid duplicate with ControlHeader
|
||||
multi
|
||||
canDelete
|
||||
ghostButtonText={t('Add dimension')}
|
||||
type="DndColumnSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
{/* Metric Selection */}
|
||||
<DndMetricSelect
|
||||
value={formValues.metric}
|
||||
onChange={handleChange('metric')}
|
||||
datasource={safeDataSource}
|
||||
name="metric"
|
||||
label=""
|
||||
multi={false}
|
||||
savedMetrics={safeMetrics}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<DndFilterSelect
|
||||
value={formValues.adhoc_filters || []}
|
||||
onChange={handleChange('adhoc_filters')}
|
||||
datasource={safeDataSource}
|
||||
columns={safeColumns}
|
||||
formData={formValues}
|
||||
name="adhoc_filters"
|
||||
savedMetrics={safeMetrics}
|
||||
selectedMetrics={formValues.metric ? [formValues.metric] : []}
|
||||
type="DndFilterSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Color Scheme Control (Special Case)
|
||||
```typescript
|
||||
{/* Color Scheme requires special Control wrapper */}
|
||||
{(() => {
|
||||
const colorSchemeControl = ColorSchemeControl();
|
||||
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
|
||||
return (
|
||||
<Control
|
||||
{...cleanConfig}
|
||||
name="color_scheme"
|
||||
value={formValues.color_scheme}
|
||||
actions={{
|
||||
...actions,
|
||||
setControlValue: (field: string, val: any) => {
|
||||
handleChange('color_scheme')(val);
|
||||
},
|
||||
}}
|
||||
renderTrigger
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
```
|
||||
|
||||
#### Basic Controls
|
||||
```typescript
|
||||
{/* Text Input */}
|
||||
<TextControl
|
||||
value={formValues.row_limit}
|
||||
onChange={handleChange('row_limit')}
|
||||
isInt
|
||||
placeholder="100"
|
||||
controlId="row_limit"
|
||||
/>
|
||||
|
||||
{/* Checkbox */}
|
||||
<CheckboxControl
|
||||
label={t('Show Labels')}
|
||||
description={t('Whether to display the labels.')}
|
||||
value={formValues.show_labels ?? true}
|
||||
onChange={handleChange('show_labels')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
|
||||
{/* Select Dropdown */}
|
||||
<SelectControl
|
||||
label={t('Label Type')}
|
||||
description={t('What should be shown on the label?')}
|
||||
value={formValues.label_type || 'key'}
|
||||
onChange={handleChange('label_type')}
|
||||
choices={[
|
||||
['key', t('Category Name')],
|
||||
['value', t('Value')],
|
||||
['percent', t('Percentage')],
|
||||
]}
|
||||
clearable={false}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
|
||||
{/* Slider */}
|
||||
<SliderControl
|
||||
value={formValues.outerRadius || 70}
|
||||
onChange={handleChange('outerRadius')}
|
||||
{...{ min: 10, max: 100, step: 1 }}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Control Headers with Tooltips
|
||||
```typescript
|
||||
<ControlHeader
|
||||
label={t('Percentage threshold')}
|
||||
description={t('Minimum threshold in percentage points for showing labels.')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
```
|
||||
|
||||
#### Conditional Rendering
|
||||
```typescript
|
||||
{/* Show control only when condition is met */}
|
||||
{formValues.show_labels && (
|
||||
<CheckboxControl
|
||||
label={t('Put labels outside')}
|
||||
description={t('Put the labels outside of the pie?')}
|
||||
value={formValues.labels_outside ?? true}
|
||||
onChange={handleChange('labels_outside')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Nested conditional rendering */}
|
||||
{formValues.label_type === 'template' && (
|
||||
<TextControl
|
||||
value={formValues.label_template || ''}
|
||||
onChange={handleChange('label_template')}
|
||||
placeholder="{name}: {value}"
|
||||
controlId="label_template"
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
### 8. Section Organization
|
||||
|
||||
Use HTML headers and spacing for logical groupings:
|
||||
|
||||
```typescript
|
||||
{/* Chart Options Section */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Chart Options')}</h4>
|
||||
|
||||
{/* Controls for this section */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{/* Individual control */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels Section */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Labels')}</h4>
|
||||
|
||||
{/* Label-related controls */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 9. Control Defaults in controlOverrides
|
||||
|
||||
Move all default values to the `controlOverrides` section:
|
||||
|
||||
```typescript
|
||||
const config = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: null,
|
||||
expanded: true,
|
||||
controlSetRows: [[PieControlPanel as any]],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
default: [],
|
||||
label: t('Group by'),
|
||||
},
|
||||
metric: {
|
||||
default: null,
|
||||
label: t('Metric'),
|
||||
},
|
||||
show_labels: {
|
||||
default: true,
|
||||
label: t('Show labels'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
color_scheme: {
|
||||
default: 'supersetColors',
|
||||
label: t('Color scheme'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
// ... all other defaults
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 10. Chart Plugin Integration
|
||||
|
||||
Update the chart plugin to use the new control panel:
|
||||
|
||||
```typescript
|
||||
// In your chart's index.ts file
|
||||
import controlPanel from './PieControlPanelSimple'; // New React-based panel
|
||||
|
||||
export default class EchartsPieChartPlugin extends EchartsChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
controlPanel,
|
||||
// ... other config
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Your Migration
|
||||
|
||||
### 1. Visual Validation
|
||||
- [ ] All controls render properly in the UI
|
||||
- [ ] Tab navigation works (if using tabs)
|
||||
- [ ] Control layout matches the original
|
||||
- [ ] Conditional controls show/hide correctly
|
||||
|
||||
### 2. Functional Testing
|
||||
- [ ] Control changes update the chart immediately (if `renderTrigger: true`)
|
||||
- [ ] Form values persist when switching between tabs
|
||||
- [ ] Drag-and-drop controls work with datasource
|
||||
- [ ] Error states display appropriately
|
||||
- [ ] Default values apply correctly
|
||||
|
||||
### 3. Integration Testing
|
||||
- [ ] Control panel works in Explore view
|
||||
- [ ] Values save correctly when creating charts
|
||||
- [ ] Dashboard filters work with the controls
|
||||
- [ ] Chart reloading preserves control values
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: Double Labels on Controls
|
||||
**Problem:** Control shows both ControlHeader label and control's built-in label
|
||||
**Solution:** Set `label=""` on the control when using ControlHeader:
|
||||
```typescript
|
||||
<ControlHeader label={t('Group by')} />
|
||||
<DndColumnSelect
|
||||
label="" // Empty to prevent duplicate
|
||||
// ... other props
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: Slider Min/Max Not Working
|
||||
**Problem:** Slider doesn't respect min/max values
|
||||
**Solution:** Use spread operator with object literal:
|
||||
```typescript
|
||||
<SliderControl
|
||||
value={formValues.outerRadius || 70}
|
||||
onChange={handleChange('outerRadius')}
|
||||
{...{ min: 10, max: 100, step: 1 }} // Use spread with object
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: Controls Not Triggering Chart Updates
|
||||
**Problem:** Chart doesn't refresh when controls change
|
||||
**Solution:** Ensure `renderTrigger` is set where needed:
|
||||
```typescript
|
||||
<CheckboxControl
|
||||
// ... other props
|
||||
renderTrigger // Add this for instant updates
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: "Cannot read properties of undefined"
|
||||
**Problem:** Attempting to access undefined datasource or form_data
|
||||
**Solution:** Add safety checks and fallbacks:
|
||||
```typescript
|
||||
// Safety checks at component start
|
||||
if (!datasource || !form_data) {
|
||||
return <div>Loading control panel...</div>;
|
||||
}
|
||||
|
||||
// Safe array access
|
||||
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
|
||||
```
|
||||
|
||||
### Issue: Color Scheme Control Not Working
|
||||
**Problem:** ColorSchemeControl doesn't integrate properly
|
||||
**Solution:** Use the special Control wrapper pattern:
|
||||
```typescript
|
||||
{(() => {
|
||||
const colorSchemeControl = ColorSchemeControl();
|
||||
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
|
||||
return (
|
||||
<Control
|
||||
{...cleanConfig}
|
||||
name="color_scheme"
|
||||
value={formValues.color_scheme}
|
||||
actions={{
|
||||
...actions,
|
||||
setControlValue: (field: string, val: any) => {
|
||||
handleChange('color_scheme')(val);
|
||||
},
|
||||
}}
|
||||
renderTrigger
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### Pre-Migration
|
||||
- [ ] Identify all controls in the legacy control panel
|
||||
- [ ] Note any conditional control visibility rules
|
||||
- [ ] Check for custom control configurations
|
||||
- [ ] Understand the chart's specific requirements
|
||||
|
||||
### During Migration
|
||||
- [ ] Create new `[ChartName]ControlPanelSimple.tsx` file
|
||||
- [ ] Implement component structure with proper interface
|
||||
- [ ] Map all legacy controls to React components
|
||||
- [ ] Add safety checks for datasource/form_data
|
||||
- [ ] Implement tab structure (Data/Customize)
|
||||
- [ ] Add all control defaults to `controlOverrides`
|
||||
- [ ] Mark component as modern with `isModernPanel = true`
|
||||
- [ ] Update chart plugin to import new control panel
|
||||
|
||||
### Post-Migration Testing
|
||||
- [ ] Test all control interactions
|
||||
- [ ] Verify chart updates on control changes
|
||||
- [ ] Check conditional control visibility
|
||||
- [ ] Validate default values
|
||||
- [ ] Test with different datasources
|
||||
- [ ] Run pre-commit hooks: `pre-commit run`
|
||||
- [ ] Test in Explore and Dashboard contexts
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Dynamic Control Visibility
|
||||
```typescript
|
||||
// Show additional controls based on current selection
|
||||
{formValues.chart_type === 'pie' && (
|
||||
<div>
|
||||
{/* Pie-specific controls */}
|
||||
<CheckboxControl
|
||||
label={t('Show as Donut')}
|
||||
value={formValues.donut ?? false}
|
||||
onChange={handleChange('donut')}
|
||||
/>
|
||||
|
||||
{formValues.donut && (
|
||||
<SliderControl
|
||||
value={formValues.innerRadius || 30}
|
||||
onChange={handleChange('innerRadius')}
|
||||
{...{ min: 0, max: 100, step: 1 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### Complex Control Groups
|
||||
```typescript
|
||||
{/* Side-by-side controls */}
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ControlHeader label={t('Min Value')} />
|
||||
<TextControl
|
||||
value={formValues.min_value}
|
||||
onChange={handleChange('min_value')}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ControlHeader label={t('Max Value')} />
|
||||
<TextControl
|
||||
value={formValues.max_value}
|
||||
onChange={handleChange('max_value')}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Custom Validation
|
||||
```typescript
|
||||
// Add validation logic to handleChange
|
||||
const handleChange = (field: string) => (val: any) => {
|
||||
// Custom validation
|
||||
if (field === 'row_limit' && val && val < 1) {
|
||||
console.warn('Row limit must be positive');
|
||||
return;
|
||||
}
|
||||
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(field, val);
|
||||
} else if (onChange) {
|
||||
onChange(field, val);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Additional Migration Patterns
|
||||
|
||||
### Single Column Selection
|
||||
When a control expects a single column value (not an array):
|
||||
```typescript
|
||||
// For Sankey source/target columns
|
||||
const handleSingleColumnChange = (field: string) => (val: any) => {
|
||||
const singleValue = Array.isArray(val) ? val[0] : val;
|
||||
actions.setControlValue(field, singleValue);
|
||||
};
|
||||
|
||||
// Usage
|
||||
<DndColumnSelect
|
||||
value={formValues.source ? [formValues.source] : []}
|
||||
onChange={handleSingleColumnChange('source')}
|
||||
options={safeColumns}
|
||||
multi={false}
|
||||
/>
|
||||
```
|
||||
|
||||
### Required Field Validation
|
||||
For controls that must have values:
|
||||
```typescript
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
|
||||
// In controlOverrides
|
||||
source: {
|
||||
validators: [validateNonEmpty],
|
||||
label: t('Source Column'),
|
||||
},
|
||||
```
|
||||
|
||||
### Chart Type Descriptions
|
||||
Add helpful descriptions at the top of control panels:
|
||||
```typescript
|
||||
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
|
||||
{t('Sankey Diagram')}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.65 }}>
|
||||
{t('Visualize flow between different entities')}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Migration Issues & Solutions
|
||||
|
||||
### Issue 1: "Cannot read properties of undefined (reading 'map')"
|
||||
**Problem:** DndColumnSelect crashes when datasource is undefined
|
||||
**Solution:** Pass `options={datasource?.columns || []}` instead of `datasource={datasource}`
|
||||
|
||||
### Issue 2: Tabs import error ("Element type is invalid")
|
||||
**Problem:** Runtime error when loading control panel
|
||||
**Solution:** Import from 'antd' directly: `import { Tabs } from 'antd';` (NOT from '@superset-ui/core')
|
||||
|
||||
### Issue 3: React hooks error
|
||||
**Problem:** "React Hook 'useState' is called conditionally"
|
||||
**Solution:** Always declare state hooks before any conditional returns:
|
||||
```typescript
|
||||
const [activeTab, setActiveTab] = useState('data'); // FIRST
|
||||
if (!datasource) return <div>Loading...</div>; // THEN conditions
|
||||
```
|
||||
|
||||
### Issue 4: ESLint color literal warnings
|
||||
**Problem:** theme-colors/no-literal-colors ESLint rule
|
||||
**Solution:** Use opacity instead of color literals:
|
||||
```typescript
|
||||
// Bad: style={{ color: '#666' }}
|
||||
// Good: style={{ opacity: 0.65 }}
|
||||
```
|
||||
|
||||
### Issue 5: Single value vs array handling
|
||||
**Problem:** Some controls expect single values but DndColumnSelect returns arrays
|
||||
**Solution:** See "Single Column Selection" pattern above
|
||||
|
||||
### Issue 6: antd import warnings
|
||||
**Problem:** "'antd' should be listed in the project's dependencies"
|
||||
**Solution:** Use `SKIP=eslint-frontend` when committing if antd is already available
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The Pie chart control panel migration (`PieControlPanelSimple.tsx`) serves as the definitive reference implementation showing:
|
||||
|
||||
- Complete tab-based layout (Data/Customize)
|
||||
- All major control types (DndColumnSelect, CheckboxControl, SelectControl, SliderControl, etc.)
|
||||
- Conditional control rendering
|
||||
- Proper safety checks and error handling
|
||||
- Color scheme integration
|
||||
- Control grouping and organization
|
||||
- Modern React patterns and TypeScript usage
|
||||
|
||||
Study this implementation for best practices and patterns that can be applied to any chart control panel migration.
|
||||
|
||||
## Summary
|
||||
|
||||
The new React-based control panel approach provides:
|
||||
|
||||
1. **Better Developer Experience** - Direct React components with TypeScript
|
||||
2. **Improved Maintainability** - Clear component structure and patterns
|
||||
3. **Enhanced Flexibility** - Easy conditional rendering and dynamic controls
|
||||
4. **Type Safety** - Full TypeScript support with proper interfaces
|
||||
5. **Simplified Architecture** - No complex config intermediaries
|
||||
|
||||
The migration process involves converting string references and config objects to direct React components, implementing proper safety checks, and organizing controls in a logical tab-based structure. The key is to maintain compatibility with the existing Superset infrastructure while providing a more modern and maintainable development experience.
|
||||
153
CONTROL_PANEL_MODERNIZATION.md
Normal file
153
CONTROL_PANEL_MODERNIZATION.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Control Panel Modernization Guide
|
||||
|
||||
## Current State
|
||||
|
||||
Apache Superset's control panels currently use a legacy `controlSetRows` structure that relies on nested arrays to define layout. This approach has several limitations:
|
||||
|
||||
1. **Rigid Layout**: The nested array structure makes it difficult to create responsive or complex layouts
|
||||
2. **Poor Type Safety**: Arrays of arrays don't provide good TypeScript support
|
||||
3. **Mixed Paradigms**: String references, configuration objects, and React components are mixed together
|
||||
4. **Limited Reusability**: Layout logic is embedded in the structure rather than using composable components
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Component Modernization ✅ COMPLETED
|
||||
- Replaced string-based control references with React components
|
||||
- Updated individual control components to use Ant Design
|
||||
- Modernized the `ControlRow` component to use Ant Design's Grid
|
||||
|
||||
### Phase 2: Layout Utilities ✅ COMPLETED
|
||||
- Created `ControlPanelLayout.tsx` with reusable layout components
|
||||
- Implemented `ControlSection`, `SingleControlRow`, `TwoColumnRow`, `ThreeColumnRow`
|
||||
- Updated control group components to use Ant Design Row/Col
|
||||
|
||||
### Phase 3: React-Based Control Panels 🚧 IN PROGRESS
|
||||
- Create `ReactControlPanel` component for rendering modern panels
|
||||
- Support both legacy and modern formats during transition
|
||||
- Provide migration helpers and examples
|
||||
|
||||
### Phase 4: Gradual Migration 📋 TODO
|
||||
- Migrate chart control panels one by one
|
||||
- Start with simpler charts (Pie, Bar) before complex ones
|
||||
- Maintain backward compatibility throughout
|
||||
|
||||
## Modern Control Panel Structure
|
||||
|
||||
### Legacy Structure (controlSetRows)
|
||||
```typescript
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[GroupByControl()],
|
||||
[MetricControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[RowLimitControl()],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Modern Structure (React Components)
|
||||
```typescript
|
||||
const modernConfig: ReactControlPanelConfig = {
|
||||
sections: [
|
||||
{
|
||||
key: 'query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
render: ({ values, onChange }) => (
|
||||
<>
|
||||
<SingleControlRow>
|
||||
<GroupByControl value={values.groupby} onChange={onChange} />
|
||||
</SingleControlRow>
|
||||
<SingleControlRow>
|
||||
<MetricControl value={values.metrics} onChange={onChange} />
|
||||
</SingleControlRow>
|
||||
<TwoColumnRow
|
||||
left={<AdhocFiltersControl value={values.adhoc_filters} onChange={onChange} />}
|
||||
right={<RowLimitControl value={values.row_limit} onChange={onChange} />}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Benefits of Modernization
|
||||
|
||||
1. **Better Type Safety**: Full TypeScript support with proper interfaces
|
||||
2. **Flexible Layouts**: Use Ant Design's Grid system for responsive layouts
|
||||
3. **Cleaner Code**: React components instead of nested arrays
|
||||
4. **Improved DX**: Better IDE support and autocomplete
|
||||
5. **Easier Testing**: Component-based architecture is easier to test
|
||||
6. **Consistent Styling**: Leverage Ant Design's theme system
|
||||
|
||||
## Migration Example
|
||||
|
||||
To migrate a control panel:
|
||||
|
||||
1. **Create a modern version** alongside the existing one:
|
||||
```typescript
|
||||
// controlPanelModern.tsx
|
||||
export const modernConfig: ReactControlPanelConfig = {
|
||||
sections: [/* ... */]
|
||||
};
|
||||
```
|
||||
|
||||
2. **Use the compatibility wrapper** for backward compatibility:
|
||||
```typescript
|
||||
export default createReactControlPanel(modernConfig);
|
||||
```
|
||||
|
||||
3. **Update the chart plugin** to use the new control panel:
|
||||
```typescript
|
||||
import controlPanel from './controlPanelModern';
|
||||
```
|
||||
|
||||
## Layout Components Available
|
||||
|
||||
- `ControlSection`: Collapsible section container
|
||||
- `SingleControlRow`: Full-width single control
|
||||
- `TwoColumnRow`: Two controls side by side (50/50)
|
||||
- `ThreeColumnRow`: Three controls in a row (33/33/33)
|
||||
- `Row` and `Col` from Ant Design for custom layouts
|
||||
|
||||
## Files Created
|
||||
|
||||
1. `packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx`
|
||||
- Layout utility components
|
||||
|
||||
2. `packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx`
|
||||
- Example of modern control panel structure
|
||||
|
||||
3. `plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx`
|
||||
- Modern version of Pie chart control panel
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Complete the ReactControlPanel integration** with ControlPanelsContainer
|
||||
2. **Create migration tooling** to help convert existing panels
|
||||
3. **Document best practices** for control panel design
|
||||
4. **Update chart plugin template** to use modern structure
|
||||
5. **Gradually migrate all 90+ control panels** in the codebase
|
||||
|
||||
## Technical Debt Addressed
|
||||
|
||||
- Eliminates nested array layout structure
|
||||
- Removes string-based control references
|
||||
- Reduces coupling between layout and configuration
|
||||
- Improves maintainability and testability
|
||||
- Enables better code splitting and lazy loading
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The migration maintains full backward compatibility:
|
||||
- Existing control panels continue to work
|
||||
- Both formats can coexist during migration
|
||||
- No breaking changes to the public API
|
||||
- Charts can be migrated incrementally
|
||||
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
|
||||
|
||||
265
LLMS.md
Normal file
265
LLMS.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
|
||||
## Control Panel Migration Guide
|
||||
|
||||
### New React-Based Control Panel Architecture
|
||||
|
||||
We've successfully migrated from the complex string-referenced control panel system to a simpler React-based approach. Here's how to migrate existing control panels:
|
||||
|
||||
#### Key Changes
|
||||
1. **No more string references** - Use React component functions instead of strings
|
||||
2. **Redux integration** - Controls connect directly to Redux via useFormData hook
|
||||
3. **Type safety** - Full TypeScript support with proper types
|
||||
4. **Simplified architecture** - Direct React components, no JSON intermediary
|
||||
|
||||
#### Migration Steps
|
||||
|
||||
##### 1. Update imports
|
||||
Replace string references with component imports:
|
||||
```typescript
|
||||
// OLD
|
||||
controlSetRows: [
|
||||
['series'],
|
||||
['metric'],
|
||||
]
|
||||
|
||||
// NEW
|
||||
import { SeriesControl, MetricControl } from '@superset-ui/chart-controls';
|
||||
controlSetRows: [
|
||||
[SeriesControl()],
|
||||
[MetricControl()],
|
||||
]
|
||||
```
|
||||
|
||||
##### 2. Custom controls
|
||||
Use InlineTextControl or InlineSelectControl for custom controls:
|
||||
```typescript
|
||||
// OLD
|
||||
{
|
||||
name: 'size_from',
|
||||
config: { type: 'TextControl', ... }
|
||||
}
|
||||
|
||||
// NEW
|
||||
InlineTextControl('sizeFrom', {
|
||||
label: t('Minimum Font Size'),
|
||||
default: 10,
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
##### 3. Control naming
|
||||
Use camelCase for new controls, but handle both in transformProps:
|
||||
```typescript
|
||||
// In transformProps.ts
|
||||
const finalSizeFrom = sizeFrom ?? size_from ?? 10;
|
||||
```
|
||||
|
||||
#### Available Control Components
|
||||
All these return CustomControlItem and can be used in controlSetRows:
|
||||
- `SeriesControl()` - Column selection
|
||||
- `MetricControl()` - Metric selection
|
||||
- `AdhocFiltersControl()` - Filter configuration
|
||||
- `RowLimitControl()` - Row limit
|
||||
- `ColorSchemeControl()` - Color scheme picker
|
||||
- `InlineTextControl(name, config)` - Custom text input
|
||||
- `InlineSelectControl(name, config)` - Custom select
|
||||
|
||||
#### Example Migration
|
||||
See `plugins/plugin-chart-word-cloud/src/plugin/controlPanelFixed.ts` for a complete example.
|
||||
|
||||
# important-instruction-reminders
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
NEVER create files unless they're absolutely necessary for achieving your goal.
|
||||
ALWAYS prefer editing an existing file to creating a new one.
|
||||
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
257
PIE_CHART_MIGRATION_PLAN.md
Normal file
257
PIE_CHART_MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Pie Chart Control Panel Migration - Phased Approach
|
||||
|
||||
## Phase 1: Parallel Implementation ✅ COMPLETED
|
||||
|
||||
We've created a modern control panel alongside the legacy one:
|
||||
|
||||
### Files Created:
|
||||
1. **`controlPanelModern.tsx`** - Modern React-based control panel
|
||||
2. **`ModernControlPanelRenderer.tsx`** - Bridge component for compatibility
|
||||
3. **Updated `ControlPanelsContainer.tsx`** - Support for modern panels
|
||||
|
||||
### Key Features:
|
||||
- Full React component structure (no `controlSetRows`)
|
||||
- Uses Ant Design Grid directly
|
||||
- Type-safe with TypeScript interfaces
|
||||
- Conditional rendering based on form values
|
||||
- Organized into logical sections
|
||||
|
||||
## Phase 2: Integration Testing 🚧 NEXT STEP
|
||||
|
||||
### 2.1 Update the Pie Chart Plugin
|
||||
|
||||
```typescript
|
||||
// In plugins/plugin-chart-echarts/src/Pie/index.ts
|
||||
import controlPanel from './controlPanel'; // Legacy
|
||||
import controlPanelModern from './controlPanelModern'; // Modern
|
||||
|
||||
// Feature flag to toggle between old and new
|
||||
const useModernPanel = window.featureFlags?.MODERN_CONTROL_PANELS;
|
||||
|
||||
export default class EchartsPieChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
// ... other config
|
||||
controlPanel: useModernPanel ? controlPanelModern : controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Test the Modern Panel
|
||||
|
||||
Create test file to verify both panels produce same output:
|
||||
|
||||
```typescript
|
||||
// controlPanel.test.tsx
|
||||
describe('Pie Control Panel Migration', () => {
|
||||
it('modern panel handles all legacy controls', () => {
|
||||
// Test that all controls from legacy panel exist in modern
|
||||
});
|
||||
|
||||
it('produces same form_data structure', () => {
|
||||
// Verify form_data compatibility
|
||||
});
|
||||
|
||||
it('visibility conditions work correctly', () => {
|
||||
// Test conditional rendering
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 3: Feature Flag Rollout
|
||||
|
||||
### 3.1 Add Feature Flag
|
||||
|
||||
```python
|
||||
# In superset/config.py
|
||||
FEATURE_FLAGS = {
|
||||
"MODERN_CONTROL_PANELS": False, # Start disabled
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Gradual Rollout
|
||||
|
||||
1. **Internal Testing**: Enable for development environment
|
||||
2. **Beta Users**: Enable for select users (5%)
|
||||
3. **Wider Rollout**: Increase to 50%
|
||||
4. **Full Migration**: Enable for all users
|
||||
5. **Cleanup**: Remove legacy code
|
||||
|
||||
## Phase 4: Migration Utilities
|
||||
|
||||
### 4.1 Control Panel Converter
|
||||
|
||||
```typescript
|
||||
// convertLegacyPanel.ts
|
||||
export function convertControlSetRows(rows: ControlSetRow[]): ReactElement {
|
||||
return rows.map(row => {
|
||||
if (row.length === 1) {
|
||||
return <SingleControlRow>{convertControl(row[0])}</SingleControlRow>;
|
||||
}
|
||||
if (row.length === 2) {
|
||||
return (
|
||||
<TwoColumnRow
|
||||
left={convertControl(row[0])}
|
||||
right={convertControl(row[1])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// ... handle other cases
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Common Patterns Library
|
||||
|
||||
```typescript
|
||||
// commonPanelPatterns.tsx
|
||||
export const QuerySection = ({ values, onChange }) => (
|
||||
<>
|
||||
<GroupByControl />
|
||||
<MetricControl />
|
||||
<AdhocFiltersControl />
|
||||
<RowLimitControl />
|
||||
</>
|
||||
);
|
||||
|
||||
export const AppearanceSection = ({ values, onChange }) => (
|
||||
<>
|
||||
<ColorSchemeControl />
|
||||
<OpacityControl />
|
||||
<LegendControls />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
## Phase 5: Migrate Other Charts
|
||||
|
||||
### Priority Order (Simple to Complex):
|
||||
|
||||
1. **Simple Charts** (1-2 weeks each)
|
||||
- Bar Chart
|
||||
- Line Chart
|
||||
- Area Chart
|
||||
- Scatter Plot
|
||||
|
||||
2. **Medium Complexity** (2-3 weeks each)
|
||||
- Table
|
||||
- Pivot Table
|
||||
- Heatmap
|
||||
- Treemap
|
||||
|
||||
3. **Complex Charts** (3-4 weeks each)
|
||||
- Mixed Time Series
|
||||
- Box Plot
|
||||
- Sankey
|
||||
- Graph/Network
|
||||
|
||||
### Migration Checklist per Chart:
|
||||
|
||||
- [ ] Create `controlPanelModern.tsx`
|
||||
- [ ] Update plugin index to support both
|
||||
- [ ] Write migration tests
|
||||
- [ ] Test with feature flag
|
||||
- [ ] Document any chart-specific patterns
|
||||
- [ ] Update TypeScript types if needed
|
||||
|
||||
## Phase 6: System-Wide Updates
|
||||
|
||||
### 6.1 Update Control Panel Registry
|
||||
|
||||
```typescript
|
||||
// getChartControlPanelRegistry.ts
|
||||
export interface ModernControlPanelRegistry {
|
||||
get(key: string): ControlPanelConfig | ReactControlPanelConfig;
|
||||
registerModern(key: string, config: ReactControlPanelConfig): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Update Explore Components
|
||||
|
||||
- `ControlPanelsContainer` - Full support for modern panels ✅
|
||||
- `Control` - Ensure all control types work
|
||||
- `ControlRow` - Already modernized ✅
|
||||
- `getSectionsToRender` - Update to handle React components
|
||||
|
||||
### 6.3 Update Types
|
||||
|
||||
```typescript
|
||||
// types.ts
|
||||
export type ControlPanelConfig = LegacyControlPanelConfig | ModernControlPanelConfig;
|
||||
|
||||
export interface ModernControlPanelConfig {
|
||||
type: 'modern';
|
||||
sections: ReactControlPanelSection[];
|
||||
controlOverrides?: ControlOverrides;
|
||||
formDataOverrides?: FormDataOverrides;
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits Tracking
|
||||
|
||||
### Metrics to Monitor:
|
||||
1. **Developer Velocity**: Time to add new controls
|
||||
2. **Bug Rate**: Control panel-related issues
|
||||
3. **Performance**: Rendering time for control panels
|
||||
4. **Type Safety**: TypeScript coverage percentage
|
||||
5. **Code Maintainability**: Lines of code, complexity metrics
|
||||
|
||||
### Expected Improvements:
|
||||
- 50% reduction in control panel code
|
||||
- 80% reduction in control panel bugs
|
||||
- 100% TypeScript coverage
|
||||
- 30% faster control panel rendering
|
||||
- Easier onboarding for new developers
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
|
||||
1. **Feature Flag**: Immediately disable `MODERN_CONTROL_PANELS`
|
||||
2. **Hotfix**: Revert to legacy panel for affected charts
|
||||
3. **Investigation**: Debug issues in staging environment
|
||||
4. **Fix Forward**: Address issues and re-enable gradually
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Phase 1**: ✅ Completed
|
||||
- **Phase 2**: 1 week (testing and integration)
|
||||
- **Phase 3**: 2 weeks (feature flag and rollout)
|
||||
- **Phase 4**: 1 week (utilities and patterns)
|
||||
- **Phase 5**: 3-6 months (all charts migration)
|
||||
- **Phase 6**: 2 weeks (system updates)
|
||||
- **Cleanup**: 1 week (remove legacy code)
|
||||
|
||||
**Total: 4-7 months for complete migration**
|
||||
|
||||
## Next Immediate Steps
|
||||
|
||||
1. Test the modern Pie control panel in development
|
||||
2. Fix any issues with value binding and onChange handlers
|
||||
3. Create feature flag in Python backend
|
||||
4. Write comprehensive tests
|
||||
5. Get team buy-in on approach
|
||||
6. Start incremental migration
|
||||
|
||||
## Code Snippets for Testing
|
||||
|
||||
```bash
|
||||
# Test the modern panel
|
||||
cd superset-frontend
|
||||
npm run dev
|
||||
|
||||
# In browser console
|
||||
window.featureFlags = { MODERN_CONTROL_PANELS: true };
|
||||
|
||||
# Create a new Pie chart and verify controls work
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All control panels migrated to modern format
|
||||
- [ ] No regression in functionality
|
||||
- [ ] Improved developer experience
|
||||
- [ ] Better performance metrics
|
||||
- [ ] Reduced maintenance burden
|
||||
- [ ] Full TypeScript coverage
|
||||
@@ -28,6 +28,7 @@ These features are considered **unfinished** and should only be used on developm
|
||||
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
|
||||
|
||||
- ALERT_REPORT_TABS
|
||||
- DATE_RANGE_TIMESHIFTS_ENABLED
|
||||
- ENABLE_ADVANCED_DATA_TYPES
|
||||
- PRESTO_EXPAND_DATA
|
||||
- SHARE_QUERIES_VIA_KV_STORE
|
||||
|
||||
@@ -23,12 +23,16 @@ 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.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
|
||||
@@ -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]
|
||||
78
docs/docs/configuration/map-tiles.mdx
Normal file
78
docs/docs/configuration/map-tiles.mdx
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Map Tiles
|
||||
sidebar_position: 12
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Map tiles
|
||||
|
||||
Superset uses OSM and Mapbox tiles by default. OSM is free but you still need setting your MAPBOX_API_KEY if you want to use mapbox maps.
|
||||
|
||||
## Setting map tiles
|
||||
|
||||
Map tiles can be set with `DECKGL_BASE_MAP` in your `superset_config.py` or `superset_config_docker.py`
|
||||
For adding your own map tiles, you can use the following format.
|
||||
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['tile://https://your_personal_url/{z}/{x}/{y}.png', 'MyTile']
|
||||
]
|
||||
```
|
||||
Openstreetmap tiles url can be added without prefix.
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 'OpenStreetMap']
|
||||
]
|
||||
```
|
||||
|
||||
Default values are:
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
|
||||
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light'],
|
||||
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets'],
|
||||
['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
|
||||
]
|
||||
```
|
||||
|
||||
It is possible to set only mapbox by removing osm tiles and other way around.
|
||||
|
||||
:::warning
|
||||
Setting `DECKGL_BASE_MAP` overwrite default values
|
||||
:::
|
||||
|
||||
After defining your map tiles, set them in these variables:
|
||||
- `CORS_OPTIONS`
|
||||
- `connect-src` of `TALISMAN_CONFIG` and `TALISMAN_CONFIG_DEV` variables.
|
||||
|
||||
```python
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS: dict[Any, Any] = {
|
||||
"origins": [
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
]
|
||||
}
|
||||
|
||||
.
|
||||
.
|
||||
|
||||
TALISMAN_CONFIG = {
|
||||
"content_security_policy": {
|
||||
...
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://api.mapbox.com",
|
||||
"https://events.mapbox.com",
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
75
docs/eslint.config.js
Normal file
75
docs/eslint.config.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-env node */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const typescriptEslintParser = require('@typescript-eslint/parser');
|
||||
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
|
||||
const eslintConfigPrettier = require('eslint-config-prettier');
|
||||
const prettierEslintPlugin = require('eslint-plugin-prettier');
|
||||
const js = require('@eslint/js');
|
||||
const ts = require('typescript-eslint');
|
||||
const react = require('eslint-plugin-react');
|
||||
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,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptEslintParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
typescript: typescriptEslintPlugin,
|
||||
react,
|
||||
prettier: prettierEslintPlugin,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
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,30 +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.32.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"prettier": "^2.0.0",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"webpack": "^5.99.9"
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -62,5 +65,6 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ const StyledTitleContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Link as React.ComponentType<any>)`
|
||||
const StyledButton = styled(Link)`
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable no-undef */
|
||||
import { useEffect } from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
@@ -50,7 +49,9 @@ export default function Root({ children }) {
|
||||
|
||||
// Handle route changes for SPA
|
||||
const handleRouteChange = () => {
|
||||
devMode && console.log('Route changed to:', window.location.pathname);
|
||||
if (devMode) {
|
||||
console.log('Route changed to:', window.location.pathname);
|
||||
}
|
||||
|
||||
// Short timeout to ensure the page has fully rendered
|
||||
setTimeout(() => {
|
||||
@@ -58,11 +59,9 @@ export default function Root({ children }) {
|
||||
const currentTitle = document.title;
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
devMode &&
|
||||
console.log('Tracking page view:', currentPath, currentTitle);
|
||||
|
||||
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
|
||||
if (devMode) {
|
||||
console.log('Tracking page view:', currentPath, currentTitle);
|
||||
window._paq.push(['setDomains', ['superset.apache.org']]);
|
||||
window._paq.push([
|
||||
'setCustomUrl',
|
||||
@@ -85,17 +84,22 @@ export default function Root({ children }) {
|
||||
'routeDidUpdate',
|
||||
];
|
||||
|
||||
devMode && console.log('Setting up Docusaurus route listeners');
|
||||
if (devMode) {
|
||||
console.log('Setting up Docusaurus route listeners');
|
||||
}
|
||||
possibleEvents.forEach(eventName => {
|
||||
document.addEventListener(eventName, () => {
|
||||
devMode &&
|
||||
if (devMode) {
|
||||
console.log(`Docusaurus route update detected via ${eventName}`);
|
||||
}
|
||||
handleRouteChange();
|
||||
});
|
||||
});
|
||||
|
||||
// Also set up manual history tracking as fallback
|
||||
devMode && console.log('Setting up manual history tracking as fallback');
|
||||
if (devMode) {
|
||||
console.log('Setting up manual history tracking as fallback');
|
||||
}
|
||||
const originalPushState = window.history.pushState;
|
||||
window.history.pushState = function () {
|
||||
originalPushState.apply(this, arguments);
|
||||
|
||||
617
docs/yarn.lock
617
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.14.2
|
||||
version: 0.14.3
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 13.4.4
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ metadata:
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.init.jobAnnotations }}
|
||||
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -44,10 +44,10 @@ spec:
|
||||
{{- if or .Values.extraLabels .Values.init.podLabels }}
|
||||
labels:
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.init.podLabels }}
|
||||
{{- toYaml .Values.init.podLabels | nindent 8 }}
|
||||
{{- toYaml .Values.init.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
spec:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetCeleryBeat.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetCeleryBeat.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetCeleryFlower.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetCeleryFlower.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetWorker.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetWorker.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetWebsockets.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetWebsockets.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{- with .Values.supersetNode.podDisruptionBudget }}
|
||||
{{- if .enabled -}}
|
||||
{{- if and .minAvailable .maxUnavailable }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
|
||||
{{- end}}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
@@ -35,12 +35,12 @@ metadata:
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- if .maxUnavailable }}
|
||||
maxUnavailable: {{ .maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "supersetNode.selectorLabels" $ | nindent 6 }}
|
||||
|
||||
486
package-lock.json
generated
Normal file
486
package-lock.json
generated
Normal file
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"name": "move-controls",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
}
|
||||
}
|
||||
@@ -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 "$@"
|
||||
|
||||
360
superset-embedded-sdk/package-lock.json
generated
360
superset-embedded-sdk/package-lock.json
generated
@@ -3251,20 +3251,6 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -3658,6 +3644,19 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4064,6 +4063,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -4124,12 +4137,57 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -4590,6 +4648,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -4617,10 +4691,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
@@ -4640,6 +4717,30 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
@@ -4649,6 +4750,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -4711,6 +4825,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -4739,6 +4865,45 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -6927,6 +7092,15 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -10565,19 +10739,6 @@
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
@@ -10862,6 +11023,16 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -11140,6 +11311,17 @@
|
||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||
"dev": true
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -11183,12 +11365,45 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -11503,6 +11718,19 @@
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -11523,9 +11751,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true
|
||||
},
|
||||
"gensync": {
|
||||
@@ -11540,12 +11768,40 @@
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -11588,6 +11844,12 @@
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -11609,6 +11871,30 @@
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -13219,6 +13505,12 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
||||
@@ -94,67 +94,12 @@ describe('Charts list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('list mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('list');
|
||||
});
|
||||
|
||||
it('should load rows in list mode', () => {
|
||||
cy.getBySel('listview-table').should('be.visible');
|
||||
cy.getBySel('sort-header').eq(1).contains('Name');
|
||||
cy.getBySel('sort-header').eq(2).contains('Type');
|
||||
cy.getBySel('sort-header').eq(3).contains('Dataset');
|
||||
cy.getBySel('sort-header').eq(4).contains('On dashboards');
|
||||
cy.getBySel('sort-header').eq(5).contains('Owners');
|
||||
cy.getBySel('sort-header').eq(6).contains('Last modified');
|
||||
cy.getBySel('sort-header').eq(7).contains('Actions');
|
||||
});
|
||||
|
||||
it('should bulk select in list mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.get('[aria-label="Select all"]').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('card mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('card');
|
||||
});
|
||||
|
||||
it('should load rows in card mode', () => {
|
||||
cy.getBySel('listview-table').should('not.exist');
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
});
|
||||
|
||||
it('should bulk select in card mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.getBySel('styled-card').click({ multiple: true });
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
|
||||
it('should preserve other filters when sorting', () => {
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
setFilter('Type', 'Big Number');
|
||||
|
||||
@@ -31,6 +31,52 @@ import {
|
||||
interceptFormDataKey,
|
||||
} from '../explore/utils';
|
||||
|
||||
const interceptDrillInfo = () => {
|
||||
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
result: {
|
||||
id: 1,
|
||||
changed_on_humanized: '2 days ago',
|
||||
created_on_humanized: 'a week ago',
|
||||
table_name: 'birth_names',
|
||||
changed_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
created_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
column_name: 'gender',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'state',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'name',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'ds',
|
||||
verbose_name: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).as('drillInfo');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="close-drill-by-modal"]').length) {
|
||||
@@ -54,7 +100,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$/)
|
||||
@@ -62,14 +108,20 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
.then($el => {
|
||||
cy.wrap($el)
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.trigger('keydown', { keyCode: 13, which: 13, force: true });
|
||||
});
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.click();
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).should('not.exist');
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
@@ -230,17 +282,19 @@ describe('Drill by modal', () => {
|
||||
closeModal();
|
||||
});
|
||||
before(() => {
|
||||
interceptDrillInfo();
|
||||
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
|
||||
});
|
||||
|
||||
describe('Modal actions + Table', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('opens the modal from the context menu', () => {
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
@@ -384,6 +438,7 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -529,7 +584,7 @@ describe('Drill by modal', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('Bar Chart', () => {
|
||||
it.skip('Bar Chart', () => {
|
||||
testEchart('echarts_timeseries_bar', 'Bar Chart', [
|
||||
[85, 94],
|
||||
[490, 68],
|
||||
@@ -547,6 +602,7 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -612,7 +668,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');
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
'node_modules/(?!d3-(interpolate|color|time|scale)|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
3084
superset-frontend/package-lock.json
generated
3084
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": [
|
||||
@@ -104,12 +104,12 @@
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
|
||||
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
|
||||
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
|
||||
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
|
||||
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
@@ -121,15 +121,15 @@
|
||||
"@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",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
@@ -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",
|
||||
@@ -203,12 +202,13 @@
|
||||
"use-event-callback": "^0.1.0",
|
||||
"use-immer": "^0.9.0",
|
||||
"use-query-params": "^1.1.9",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
@@ -243,7 +243,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -316,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",
|
||||
@@ -331,7 +331,7 @@
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.19.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.99.9",
|
||||
@@ -349,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};
|
||||
`}
|
||||
/>{' '}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Collapse } from '@superset-ui/core/components';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const StyledCollapse = styled(Collapse)`
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: 1px solid ${({ theme }: any) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }: any) => theme.borderRadius}px;
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 2}px;
|
||||
|
||||
.ant-collapse-header {
|
||||
font-weight: ${({ theme }: any) => theme.typography.weights.bold};
|
||||
background: ${({ theme }: any) => theme.colors.grayscale.light5};
|
||||
border-radius: ${({ theme }: any) => theme.borderRadius}px
|
||||
${({ theme }: any) => theme.borderRadius}px 0 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background: white;
|
||||
padding: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ControlPanelSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export const ControlPanelSection: React.FC<ControlPanelSectionProps> = ({
|
||||
title,
|
||||
children,
|
||||
expanded = false,
|
||||
}) => {
|
||||
const [activeKey, setActiveKey] = useState(expanded ? ['1'] : []);
|
||||
|
||||
return (
|
||||
<StyledCollapse
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
>
|
||||
<Panel header={title} key="1">
|
||||
{children}
|
||||
</Panel>
|
||||
</StyledCollapse>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.control-wrapper {
|
||||
min-width: 0; // Allow flex items to shrink
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ControlRowProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ControlRow: React.FC<ControlRowProps> = ({ children }) => (
|
||||
<StyledRow>{children}</StyledRow>
|
||||
);
|
||||
@@ -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}>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import AdhocFilterControlOriginal from 'src/explore/components/controls/FilterControl/AdhocFilterControl/index';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface AdhocFilterControlProps {
|
||||
name: string;
|
||||
value?: any[];
|
||||
onChange: (value: any[]) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing AdhocFilterControl that simplifies its API
|
||||
*/
|
||||
export const AdhocFilterControl: React.FC<AdhocFilterControlProps> = ({
|
||||
name,
|
||||
value = [],
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
renderTrigger,
|
||||
}) => {
|
||||
// Get datasource from Redux state
|
||||
const datasource = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource,
|
||||
) as any;
|
||||
|
||||
const columns = datasource?.columns || [];
|
||||
const savedMetrics = datasource?.metrics || [];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<AdhocFilterControlOriginal
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
columns={columns}
|
||||
savedMetrics={savedMetrics}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import CheckboxControlComponent from '../../../../../../../src/explore/components/controls/CheckboxControl';
|
||||
|
||||
export interface CheckboxControlProps {
|
||||
value?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
hovered?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox control component
|
||||
*/
|
||||
export const CheckboxControl: React.FC<CheckboxControlProps> = (
|
||||
props,
|
||||
): ReactElement => <CheckboxControlComponent {...props} />;
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/core';
|
||||
import { SelectControl } from './SimpleSelectControl';
|
||||
|
||||
export interface ColorSchemeControlProps {
|
||||
name: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color scheme selector control
|
||||
*/
|
||||
export const ColorSchemeControl: React.FC<ColorSchemeControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
renderTrigger,
|
||||
clearable = false,
|
||||
}) => {
|
||||
// Get available color schemes from the categorical scheme registry
|
||||
const colorSchemes = getCategoricalSchemeRegistry().keys();
|
||||
const choices: [string, string][] = colorSchemes.map((scheme: string) => [
|
||||
scheme,
|
||||
scheme,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange as (value: string | string[]) => void}
|
||||
label={label}
|
||||
description={description}
|
||||
required={required}
|
||||
renderTrigger={renderTrigger}
|
||||
choices={choices}
|
||||
clearable={clearable}
|
||||
multiple={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import ControlComponent from '../../../../../../../src/explore/components/Control';
|
||||
|
||||
export interface ControlProps {
|
||||
type?: string;
|
||||
name: string;
|
||||
value?: any;
|
||||
actions?: any;
|
||||
formData?: any;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic control wrapper component
|
||||
*/
|
||||
export const Control: React.FC<ControlProps> = (props): ReactElement => (
|
||||
<ControlComponent {...props} />
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import { DndColumnSelect as DndColumnSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface DndColumnSelectProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
isTemporal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndColumnSelect that simplifies its API
|
||||
*/
|
||||
export const DndColumnSelect: React.FC<DndColumnSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
multi = false,
|
||||
required,
|
||||
renderTrigger,
|
||||
canDelete = true,
|
||||
ghostButtonText,
|
||||
isTemporal,
|
||||
}) => {
|
||||
// Get columns from Redux state
|
||||
const columns = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource?.columns || [],
|
||||
) as any[];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<DndColumnSelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={columns}
|
||||
multi={multi}
|
||||
canDelete={canDelete}
|
||||
ghostButtonText={
|
||||
ghostButtonText || (multi ? 'Drop columns here' : 'Drop column here')
|
||||
}
|
||||
isTemporal={isTemporal}
|
||||
type="DndColumnSelect"
|
||||
actions={
|
||||
{
|
||||
setControlValue: (controlName: string, value: any) => ({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
validationErrors: undefined,
|
||||
}),
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import { DndFilterSelect as DndFilterSelectControl } from '../../../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
|
||||
import { Datasource } from '../../types';
|
||||
|
||||
export interface DndFilterSelectProps {
|
||||
value?: any[];
|
||||
onChange: (value: any[]) => void;
|
||||
datasource?: Datasource;
|
||||
columns?: any[];
|
||||
formData?: any;
|
||||
savedMetrics?: any[];
|
||||
selectedMetrics?: any[];
|
||||
name?: string;
|
||||
actions?: any;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndFilterSelect that simplifies its API
|
||||
*/
|
||||
export const DndFilterSelect: React.FC<DndFilterSelectProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
datasource,
|
||||
columns = [],
|
||||
formData = {},
|
||||
savedMetrics = [],
|
||||
selectedMetrics = [],
|
||||
name = 'adhoc_filters',
|
||||
actions,
|
||||
type = 'DndFilterSelect',
|
||||
...restProps
|
||||
}): ReactElement => {
|
||||
// Handle the case where onChange needs to be wrapped for actions.setControlValue
|
||||
const handleChange = (val: any) => {
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(name, val);
|
||||
} else if (onChange) {
|
||||
onChange(val);
|
||||
}
|
||||
};
|
||||
|
||||
// For compatibility with the original component
|
||||
const componentProps = {
|
||||
value,
|
||||
onChange: handleChange,
|
||||
datasource,
|
||||
columns,
|
||||
formData,
|
||||
name,
|
||||
savedMetrics,
|
||||
selectedMetrics,
|
||||
type,
|
||||
actions,
|
||||
...restProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter-select-wrapper">
|
||||
<DndFilterSelectControl {...componentProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import { DndMetricSelect as DndMetricSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface DndMetricSelectProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndMetricSelect that simplifies its API
|
||||
*/
|
||||
export const DndMetricSelect: React.FC<DndMetricSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
multi = false,
|
||||
required,
|
||||
renderTrigger,
|
||||
canDelete = true,
|
||||
ghostButtonText,
|
||||
}) => {
|
||||
// Get datasource from Redux state
|
||||
const datasource = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource,
|
||||
) as any;
|
||||
|
||||
const columns = datasource?.columns || [];
|
||||
const savedMetrics = datasource?.metrics || [];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<DndMetricSelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
columns={columns}
|
||||
savedMetrics={savedMetrics}
|
||||
multi={multi}
|
||||
canDelete={canDelete}
|
||||
ghostButtonText={
|
||||
ghostButtonText || (multi ? 'Drop metrics here' : 'Drop metric here')
|
||||
}
|
||||
type="DndMetricSelect"
|
||||
actions={
|
||||
{
|
||||
setControlValue: (controlName: string, value: any) => ({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
validationErrors: undefined,
|
||||
}),
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import SelectControlComponent from '../../../../../../../src/explore/components/controls/SelectControl';
|
||||
|
||||
export interface SelectControlProps {
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
choices?: Array<[string | number, string]>;
|
||||
clearable?: boolean;
|
||||
multi?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
hovered?: boolean;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select control component
|
||||
*/
|
||||
export const SelectControl: React.FC<SelectControlProps> = (
|
||||
props,
|
||||
): ReactElement => <SelectControlComponent {...props} />;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Select } from '@superset-ui/core/components';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface SelectControlProps {
|
||||
name: string;
|
||||
value?: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
choices: Array<[string, string]>;
|
||||
clearable?: boolean;
|
||||
multiple?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const SelectControl: React.FC<SelectControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
disabled,
|
||||
choices,
|
||||
clearable = true,
|
||||
multiple = false,
|
||||
renderTrigger,
|
||||
required,
|
||||
}) => {
|
||||
const options = choices.map(([val, label]) => ({
|
||||
value: val,
|
||||
label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
allowClear={clearable}
|
||||
mode={multiple ? 'multiple' : undefined}
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { Input } from '@superset-ui/core/components';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface TextControlProps {
|
||||
name: string;
|
||||
value?: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
renderTrigger?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const TextControl: React.FC<TextControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
disabled,
|
||||
isInt,
|
||||
isFloat,
|
||||
min,
|
||||
max,
|
||||
renderTrigger,
|
||||
required,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue: string | number = e.target.value;
|
||||
|
||||
if (isInt) {
|
||||
newValue = parseInt(newValue, 10) || 0;
|
||||
} else if (isFloat) {
|
||||
newValue = parseFloat(newValue) || 0;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange, isInt, isFloat],
|
||||
);
|
||||
|
||||
const inputType = isInt || isFloat ? 'number' : 'text';
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
name={name}
|
||||
type={inputType}
|
||||
value={value ?? ''}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
min={min}
|
||||
max={max}
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import SliderControlComponent from '../../../../../../../src/explore/components/controls/SliderControl';
|
||||
|
||||
export interface SliderControlProps {
|
||||
value?: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider control component
|
||||
*/
|
||||
export const SliderControl: React.FC<SliderControlProps> = (
|
||||
props,
|
||||
): ReactElement => <SliderControlComponent {...props} />;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import TextControlComponent from '../../../../../../../src/explore/components/controls/TextControl';
|
||||
|
||||
export interface TextControlProps {
|
||||
value?: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
placeholder?: string;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
disabled?: boolean;
|
||||
controlId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text input control component
|
||||
*/
|
||||
export const TextControl: React.FC<TextControlProps> = (
|
||||
props,
|
||||
): ReactElement => <TextControlComponent {...props} />;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Layout components
|
||||
export { ControlPanelSection } from './ControlPanelSection';
|
||||
export { ControlRow } from './ControlRow';
|
||||
export { ControlHeader } from './ControlHeader';
|
||||
|
||||
// Control components
|
||||
export { TextControl as SimpleTextControl } from './controls/SimpleTextControl';
|
||||
export { SelectControl as SimpleSelectControl } from './controls/SimpleSelectControl';
|
||||
|
||||
// Wrapper controls for new simplified API
|
||||
export { DndColumnSelect } from './controls/DndColumnSelectWrapper';
|
||||
export { DndMetricSelect } from './controls/DndMetricSelectWrapper';
|
||||
export { DndFilterSelect } from './controls/DndFilterSelectWrapper';
|
||||
export { AdhocFilterControl } from './controls/AdhocFilterControlWrapper';
|
||||
export { ColorSchemeControl as SimpleColorSchemeControl } from './controls/ColorSchemeControlWrapper';
|
||||
export { TextControl } from './controls/TextControlWrapper';
|
||||
export { CheckboxControl } from './controls/CheckboxControlWrapper';
|
||||
export { SelectControl } from './controls/SelectControlWrapper';
|
||||
export { SliderControl } from './controls/SliderControlWrapper';
|
||||
export { Control } from './controls/ControlWrapper';
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
|
||||
// Define minimal types to avoid circular dependencies
|
||||
interface ExploreState {
|
||||
form_data: QueryFormData;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RootState {
|
||||
explore: ExploreState;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access and update form data from Redux store
|
||||
* Provides a simple interface for control components
|
||||
*
|
||||
* NOTE: This hook assumes the Redux store structure used by Superset's explore view.
|
||||
* The actual setControlValue action should be provided by the app via context or props.
|
||||
*/
|
||||
export function useFormData() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get form data from Redux
|
||||
const formData = useSelector<RootState, QueryFormData>(state => {
|
||||
console.log('useFormData - state.explore:', state.explore);
|
||||
return state.explore?.form_data || {};
|
||||
});
|
||||
|
||||
// Update a single control value
|
||||
// This is a placeholder - the actual action should be injected
|
||||
const updateControl = useCallback(
|
||||
(controlName: string, value: any) => {
|
||||
console.log('updateControl:', controlName, value);
|
||||
// In production, this would dispatch the actual setControlValue action
|
||||
// For now, we'll dispatch a generic action
|
||||
dispatch({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Update multiple controls at once
|
||||
const updateControls = useCallback(
|
||||
(updates: Partial<QueryFormData>) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
updateControl(key, value);
|
||||
});
|
||||
},
|
||||
[updateControl],
|
||||
);
|
||||
|
||||
return {
|
||||
formData,
|
||||
updateControl,
|
||||
updateControls,
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import * as sectionsModule from './sections';
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
export * from './operators';
|
||||
export * from './hooks/useFormData';
|
||||
|
||||
// can't do `export * as sections from './sections'`, babel-transformer will fail
|
||||
export const sections = sectionsModule;
|
||||
@@ -32,7 +33,26 @@ export * from './components/Dropdown';
|
||||
export * from './components/Menu';
|
||||
export * from './components/MetricOption';
|
||||
export * from './components/ControlHeader';
|
||||
export * from './components';
|
||||
// Export individual control components for easier access
|
||||
export {
|
||||
DndColumnSelect,
|
||||
DndMetricSelect,
|
||||
DndFilterSelect,
|
||||
TextControl,
|
||||
CheckboxControl,
|
||||
SelectControl,
|
||||
SliderControl,
|
||||
Control,
|
||||
} from './components';
|
||||
|
||||
export * from './shared-controls';
|
||||
export {
|
||||
GranularityControl,
|
||||
RadioButtonControl,
|
||||
ReactControlPanel,
|
||||
} from './shared-controls/components';
|
||||
// Export all from shared-controls/components which includes inline control functions
|
||||
export * from './shared-controls/components';
|
||||
export * from './types';
|
||||
export * from './fixtures';
|
||||
|
||||
@@ -18,6 +18,20 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { ControlPanelSectionConfig, ControlSetRow } from '../types';
|
||||
import {
|
||||
AdhocFiltersControl,
|
||||
GroupByControl,
|
||||
GroupOthersWhenLimitReachedControl,
|
||||
LimitControl,
|
||||
MetricsControl,
|
||||
OrderDescControl,
|
||||
RowLimitControl,
|
||||
ShowEmptyColumnsControl,
|
||||
TimeGrainSqlaControl,
|
||||
TimeLimitMetricControl,
|
||||
TruncateMetricControl,
|
||||
XAxisControl,
|
||||
} from '../shared-controls/components/SharedControlComponents';
|
||||
import {
|
||||
contributionModeControl,
|
||||
xAxisForceCategoricalControl,
|
||||
@@ -26,30 +40,34 @@ import {
|
||||
} from '../shared-controls';
|
||||
|
||||
const controlsWithoutXAxis: ControlSetRow[] = [
|
||||
['metrics'],
|
||||
['groupby'],
|
||||
[MetricsControl()],
|
||||
[GroupByControl()],
|
||||
[contributionModeControl],
|
||||
['adhoc_filters'],
|
||||
['limit'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
['row_limit'],
|
||||
['truncate_metric'],
|
||||
['show_empty_columns'],
|
||||
[AdhocFiltersControl()],
|
||||
[LimitControl(), GroupOthersWhenLimitReachedControl()],
|
||||
[TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
[RowLimitControl()],
|
||||
[TruncateMetricControl()],
|
||||
[ShowEmptyColumnsControl()],
|
||||
];
|
||||
|
||||
export const echartsTimeSeriesQuery: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [['x_axis'], ['time_grain_sqla'], ...controlsWithoutXAxis],
|
||||
controlSetRows: [
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
...controlsWithoutXAxis,
|
||||
],
|
||||
};
|
||||
|
||||
export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['x_axis'],
|
||||
['time_grain_sqla'],
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
[xAxisForceCategoricalControl],
|
||||
[xAxisSortControl],
|
||||
[xAxisSortAscControl],
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { ControlPanelSectionConfig } from '../types';
|
||||
import {
|
||||
GranularityControl,
|
||||
GranularitySqlaControl,
|
||||
TimeGrainSqlaControl,
|
||||
TimeRangeControl,
|
||||
DatasourceControl,
|
||||
VizTypeControl,
|
||||
ColorSchemeControl,
|
||||
} from '../shared-controls/components/SharedControlComponents';
|
||||
|
||||
// A few standard controls sections that are used internally.
|
||||
// Not recommended for use in third-party plugins.
|
||||
@@ -31,10 +40,10 @@ const baseTimeSection = {
|
||||
export const legacyTimeseriesTime: ControlPanelSectionConfig = {
|
||||
...baseTimeSection,
|
||||
controlSetRows: [
|
||||
['granularity'],
|
||||
['granularity_sqla'],
|
||||
['time_grain_sqla'],
|
||||
['time_range'],
|
||||
[GranularityControl()],
|
||||
[GranularitySqlaControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
[TimeRangeControl()],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -42,8 +51,8 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
label: t('Datasource & Chart Type'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['datasource'],
|
||||
['viz_type'],
|
||||
[DatasourceControl()],
|
||||
[VizTypeControl()],
|
||||
[
|
||||
{
|
||||
name: 'slice_id',
|
||||
@@ -91,7 +100,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
|
||||
export const colorScheme: ControlPanelSectionConfig = {
|
||||
label: t('Color Scheme'),
|
||||
controlSetRows: [['color_scheme']],
|
||||
controlSetRows: [[ColorSchemeControl()]],
|
||||
};
|
||||
|
||||
export const annotations: ControlPanelSectionConfig = {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Input, Select, Switch, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface AxisControlSectionProps {
|
||||
axis: 'x' | 'y';
|
||||
showTitle?: boolean;
|
||||
showFormat?: boolean;
|
||||
showRotation?: boolean;
|
||||
showBounds?: boolean;
|
||||
showLogarithmic?: boolean;
|
||||
showMinorTicks?: boolean;
|
||||
showTruncate?: boolean;
|
||||
timeFormat?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const D3_FORMAT_OPTIONS = [
|
||||
['SMART_NUMBER', t('Adaptive formatting')],
|
||||
['~g', t('Original value')],
|
||||
['d', t('Signed integer')],
|
||||
['.1f', t('1 decimal place')],
|
||||
['.2f', t('2 decimal places')],
|
||||
['.3f', t('3 decimal places')],
|
||||
['+,', t('Positive integer')],
|
||||
['$,.2f', t('Currency (2 decimals)')],
|
||||
[',.0%', t('Percentage')],
|
||||
['.1%', t('Percentage (1 decimal)')],
|
||||
];
|
||||
|
||||
const D3_TIME_FORMAT_OPTIONS = [
|
||||
['smart_date', t('Adaptive formatting')],
|
||||
['%Y-%m-%d', t('2023-01-01')],
|
||||
['%Y-%m-%d %H:%M', t('2023-01-01 10:30')],
|
||||
['%m/%d/%Y', t('01/01/2023')],
|
||||
['%d/%m/%Y', t('01/01/2023')],
|
||||
['%Y', t('2023')],
|
||||
['%B %Y', t('January 2023')],
|
||||
['%b %Y', t('Jan 2023')],
|
||||
['%B %-d, %Y', t('January 1, 2023')],
|
||||
];
|
||||
|
||||
const ROTATION_OPTIONS = [
|
||||
[0, '0°'],
|
||||
[45, '45°'],
|
||||
[90, '90°'],
|
||||
[-45, '-45°'],
|
||||
[-90, '-90°'],
|
||||
];
|
||||
|
||||
export const AxisControlSection: FC<AxisControlSectionProps> = ({
|
||||
axis,
|
||||
showTitle = true,
|
||||
showFormat = true,
|
||||
showRotation = false,
|
||||
showBounds = false,
|
||||
showLogarithmic = false,
|
||||
showMinorTicks = false,
|
||||
showTruncate = false,
|
||||
timeFormat = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isXAxis = axis === 'x';
|
||||
const axisUpper = axis.toUpperCase();
|
||||
const titleKey = `${axis}_axis_title`;
|
||||
const formatKey = timeFormat
|
||||
? `${axis}_axis_time_format`
|
||||
: `${axis}_axis_format`;
|
||||
const rotationKey = `${axis}_axis_label_rotation`;
|
||||
const boundsMinKey = `${axis}_axis_bounds_min`;
|
||||
const boundsMaxKey = `${axis}_axis_bounds_max`;
|
||||
const logScaleKey = `log_scale`;
|
||||
const minorTicksKey = `${axis}_axis_minor_ticks`;
|
||||
const truncateKey = `truncate_${axis}axis`;
|
||||
const truncateLabelsKey = `${axis}_axis_truncate_labels`;
|
||||
|
||||
return (
|
||||
<div className="axis-control-section">
|
||||
{showTitle && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Title`)}</label>
|
||||
<Input
|
||||
value={values[titleKey] || ''}
|
||||
onChange={e => onChange(titleKey, e.target.value)}
|
||||
placeholder={t(`Enter ${axis} axis title`)}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'Overrides the axis title derived from the metric or column name',
|
||||
)}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showFormat && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Format`)}</label>
|
||||
<Select
|
||||
value={
|
||||
values[formatKey] ||
|
||||
(timeFormat ? 'smart_date' : 'SMART_NUMBER')
|
||||
}
|
||||
onChange={value => onChange(formatKey, value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a format')}
|
||||
options={(timeFormat
|
||||
? D3_TIME_FORMAT_OPTIONS
|
||||
: D3_FORMAT_OPTIONS
|
||||
).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{timeFormat
|
||||
? t('D3 time format for x axis')
|
||||
: t('D3 format for axis values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showRotation && isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Rotation')}</label>
|
||||
<Select
|
||||
value={values[rotationKey] || 0}
|
||||
onChange={value => onChange(rotationKey, value)}
|
||||
style={{ width: '100%' }}
|
||||
options={ROTATION_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Rotation angle for axis labels')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showBounds && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Bounds`)}</label>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<InputNumber
|
||||
value={values[boundsMinKey]}
|
||||
onChange={value => onChange(boundsMinKey, value)}
|
||||
placeholder={t('Min')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<InputNumber
|
||||
value={values[boundsMaxKey]}
|
||||
onChange={value => onChange(boundsMaxKey, value)}
|
||||
placeholder={t('Max')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
<small className="text-muted">
|
||||
{t('Bounds for axis values. Leave empty for automatic scaling.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showLogarithmic && !isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values[logScaleKey] || false}
|
||||
onChange={checked => onChange(logScaleKey, checked)}
|
||||
/>
|
||||
{t('Logarithmic Scale')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Use a logarithmic scale for the Y-axis')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showMinorTicks && !isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values[minorTicksKey] || false}
|
||||
onChange={checked => onChange(minorTicksKey, checked)}
|
||||
/>
|
||||
{t('Show Minor Ticks')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show minor grid lines on the axis')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showTruncate && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={
|
||||
values[truncateKey] || values[truncateLabelsKey] || false
|
||||
}
|
||||
onChange={checked => {
|
||||
onChange(truncateKey, checked);
|
||||
onChange(truncateLabelsKey, checked);
|
||||
}}
|
||||
/>
|
||||
{t(`Truncate ${axisUpper} Axis Labels`)}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Truncate long axis labels to prevent overlap')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AxisControlSection;
|
||||
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
import type { CustomControlItem, ControlValueValidator } from '../../types';
|
||||
|
||||
// Base control props that all controls share
|
||||
interface BaseControlProps {
|
||||
name: string;
|
||||
label?: ReactElement | string;
|
||||
description?: string;
|
||||
default?: any;
|
||||
renderTrigger?: boolean;
|
||||
validators?: ControlValueValidator[];
|
||||
warning?: string;
|
||||
error?: string;
|
||||
mapStateToProps?: (state: any, control: any) => any;
|
||||
visibility?: (props: any) => boolean;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
|
||||
// Use the existing CustomControlItem type instead of creating a duplicate
|
||||
// This ensures type compatibility with the rest of the codebase
|
||||
export type ControlComponentConfig = CustomControlItem;
|
||||
|
||||
// CheckboxControl Component
|
||||
interface CheckboxControlProps extends BaseControlProps {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export const CheckboxControl = (
|
||||
props: CheckboxControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? false,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SelectControl Component
|
||||
interface SelectControlProps extends BaseControlProps {
|
||||
choices?: Array<[string, string]> | (() => Array<[string, string]>);
|
||||
clearable?: boolean;
|
||||
freeForm?: boolean;
|
||||
multi?: boolean;
|
||||
placeholder?: string;
|
||||
optionRenderer?: (option: any) => ReactElement;
|
||||
valueRenderer?: (value: any) => ReactElement;
|
||||
valueKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
|
||||
export const SelectControl = (
|
||||
props: SelectControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
choices: props.choices ?? [],
|
||||
clearable: props.clearable ?? true,
|
||||
freeForm: props.freeForm ?? false,
|
||||
multi: props.multi ?? false,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
placeholder: props.placeholder,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
optionRenderer: props.optionRenderer,
|
||||
valueRenderer: props.valueRenderer,
|
||||
valueKey: props.valueKey,
|
||||
labelKey: props.labelKey,
|
||||
},
|
||||
});
|
||||
|
||||
// TextControl Component
|
||||
interface TextControlProps extends BaseControlProps {
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
}
|
||||
|
||||
export const TextControl = (
|
||||
props: TextControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
default: props.default ?? '',
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
disabled: props.disabled ?? false,
|
||||
isInt: props.isInt ?? false,
|
||||
isFloat: props.isFloat ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// TextAreaControl Component
|
||||
interface TextAreaControlProps extends BaseControlProps {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
language?: 'json' | 'html' | 'sql' | 'markdown' | 'javascript';
|
||||
offerEditInModal?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TextAreaControl = (
|
||||
props: TextAreaControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextAreaControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
rows: props.rows ?? 3,
|
||||
language: props.language,
|
||||
offerEditInModal: props.offerEditInModal ?? true,
|
||||
default: props.default ?? '',
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
disabled: props.disabled ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SliderControl Component
|
||||
interface SliderControlProps extends BaseControlProps {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default?: number;
|
||||
}
|
||||
|
||||
export const SliderControl = (
|
||||
props: SliderControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SliderControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
min: props.min ?? 0,
|
||||
max: props.max ?? 100,
|
||||
step: props.step ?? 1,
|
||||
default: props.default ?? 0,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// RadioButtonControl Component
|
||||
interface RadioButtonControlProps extends BaseControlProps {
|
||||
options?: Array<[string, string | ReactElement]>;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export const RadioButtonControl = (
|
||||
props: RadioButtonControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'RadioButtonControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
options: props.options ?? [],
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// NumberControl Component
|
||||
interface NumberControlProps extends BaseControlProps {
|
||||
min?: number;
|
||||
max?: number;
|
||||
default?: number;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const NumberControl = (
|
||||
props: NumberControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
isFloat: true,
|
||||
controlHeader: {
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
},
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
},
|
||||
});
|
||||
|
||||
// ColorPickerControl Component
|
||||
interface ColorPickerControlProps extends BaseControlProps {
|
||||
default?: { r: number; g: number; b: number; a?: number };
|
||||
}
|
||||
|
||||
export const ColorPickerControl = (
|
||||
props: ColorPickerControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColorPickerControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? { r: 0, g: 122, b: 135, a: 1 },
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// DateFilterControl Component
|
||||
interface DateFilterControlProps extends BaseControlProps {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export const DateFilterControl = (
|
||||
props: DateFilterControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'DateFilterControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
renderTrigger: props.renderTrigger,
|
||||
},
|
||||
});
|
||||
|
||||
// BoundsControl Component
|
||||
interface BoundsControlProps extends BaseControlProps {
|
||||
default?: [number | null, number | null];
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export const BoundsControl = (
|
||||
props: BoundsControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'BoundsControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? [null, null],
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SwitchControl Component
|
||||
interface SwitchControlProps extends BaseControlProps {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export const SwitchControl = (
|
||||
props: SwitchControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? false,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// HiddenControl Component (for hidden fields)
|
||||
interface HiddenControlProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
export const HiddenControl = (
|
||||
props: HiddenControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'HiddenControl',
|
||||
default: props.default,
|
||||
value: props.value,
|
||||
renderTrigger: false,
|
||||
visible: false,
|
||||
},
|
||||
});
|
||||
|
||||
// MetricsControl Component
|
||||
interface MetricsControlProps extends BaseControlProps {
|
||||
multi?: boolean;
|
||||
clearable?: boolean;
|
||||
savedMetrics?: any[];
|
||||
columns?: any[];
|
||||
datasourceType?: string;
|
||||
}
|
||||
|
||||
export const MetricsControl = (
|
||||
props: MetricsControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'MetricsControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
multi: props.multi ?? true,
|
||||
clearable: props.clearable ?? true,
|
||||
validators: props.validators ?? [],
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
columns: state.datasource?.columns || [],
|
||||
savedMetrics: state.datasource?.metrics || [],
|
||||
datasourceType: state.datasource?.type,
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
savedMetrics: props.savedMetrics,
|
||||
columns: props.columns,
|
||||
datasourceType: props.datasourceType,
|
||||
},
|
||||
});
|
||||
|
||||
// GroupByControl Component
|
||||
interface GroupByControlProps extends BaseControlProps {
|
||||
multi?: boolean;
|
||||
clearable?: boolean;
|
||||
columns?: any[];
|
||||
}
|
||||
|
||||
export const GroupByControl = (
|
||||
props: GroupByControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
multi: props.multi ?? true,
|
||||
clearable: props.clearable ?? true,
|
||||
validators: props.validators ?? [],
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
choices: state.datasource?.columns || [],
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
columns: props.columns,
|
||||
},
|
||||
});
|
||||
|
||||
// AdhocFilterControl Component
|
||||
interface AdhocFilterControlProps extends BaseControlProps {
|
||||
columns?: any[];
|
||||
savedMetrics?: any[];
|
||||
datasourceType?: string;
|
||||
}
|
||||
|
||||
export const AdhocFilterControl = (
|
||||
props: AdhocFilterControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'AdhocFilterControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
columns: state.datasource?.columns || [],
|
||||
savedMetrics: state.datasource?.metrics || [],
|
||||
datasourceType: state.datasource?.type,
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
columns: props.columns,
|
||||
savedMetrics: props.savedMetrics,
|
||||
datasourceType: props.datasourceType,
|
||||
},
|
||||
});
|
||||
|
||||
// SpatialControl Component
|
||||
interface SpatialControlProps extends BaseControlProps {
|
||||
choices?: any[];
|
||||
}
|
||||
|
||||
export const SpatialControl = (
|
||||
props: SpatialControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SpatialControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
validators: props.validators,
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
choices: state.datasource?.columns || [],
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// ColorSchemeControl Component
|
||||
interface ColorSchemeControlProps extends BaseControlProps {
|
||||
choices?: (() => Array<[string, string]>) | Array<[string, string]>;
|
||||
schemes?: () => any;
|
||||
isLinear?: boolean;
|
||||
}
|
||||
|
||||
export const ColorSchemeControl = (
|
||||
props: ColorSchemeControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColorSchemeControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
choices: props.choices,
|
||||
schemes: props.schemes,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
isLinear: props.isLinear,
|
||||
},
|
||||
});
|
||||
|
||||
// SelectAsyncControl Component
|
||||
interface SelectAsyncControlProps extends BaseControlProps {
|
||||
dataEndpoint?: string;
|
||||
multi?: boolean;
|
||||
mutator?: (data: any) => any;
|
||||
placeholder?: string;
|
||||
onAsyncErrorMessage?: string;
|
||||
cacheOptions?: boolean;
|
||||
}
|
||||
|
||||
export const SelectAsyncControl = (
|
||||
props: SelectAsyncControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectAsyncControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
dataEndpoint: props.dataEndpoint,
|
||||
multi: props.multi ?? false,
|
||||
mutator: props.mutator,
|
||||
placeholder: props.placeholder,
|
||||
onAsyncErrorMessage: props.onAsyncErrorMessage,
|
||||
cacheOptions: props.cacheOptions ?? true,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
renderTrigger: props.renderTrigger,
|
||||
},
|
||||
});
|
||||
|
||||
// ContourControl Component
|
||||
interface ContourControlProps extends BaseControlProps {
|
||||
renderTrigger?: boolean;
|
||||
choices?: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export const ContourControl = (
|
||||
props: ContourControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ContourControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
choices: props.choices,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// ColumnConfigControl Component
|
||||
interface ColumnConfigControlProps extends BaseControlProps {
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
export const ColumnConfigControl = (
|
||||
props: ColumnConfigControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColumnConfigControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// Export all components
|
||||
export default {
|
||||
CheckboxControl,
|
||||
SelectControl,
|
||||
TextControl,
|
||||
TextAreaControl,
|
||||
SliderControl,
|
||||
RadioButtonControl,
|
||||
NumberControl,
|
||||
ColorPickerControl,
|
||||
DateFilterControl,
|
||||
BoundsControl,
|
||||
SwitchControl,
|
||||
HiddenControl,
|
||||
MetricsControl,
|
||||
GroupByControl,
|
||||
AdhocFilterControl,
|
||||
SpatialControl,
|
||||
ColorSchemeControl,
|
||||
SelectAsyncControl,
|
||||
ContourControl,
|
||||
ColumnConfigControl,
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, FC } from 'react';
|
||||
import { Row, Col, Collapse } from '@superset-ui/core/components';
|
||||
|
||||
/**
|
||||
* Props for control panel sections
|
||||
*/
|
||||
export interface ControlSectionProps {
|
||||
label?: ReactNode;
|
||||
description?: ReactNode;
|
||||
expanded?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A collapsible section in the control panel
|
||||
*/
|
||||
export const ControlSection: FC<ControlSectionProps> = ({
|
||||
label,
|
||||
description,
|
||||
expanded = true,
|
||||
children,
|
||||
}) => {
|
||||
if (!label) {
|
||||
// No label means no collapsible wrapper
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse defaultActiveKey={expanded ? ['1'] : []} ghost>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<span>
|
||||
{label}
|
||||
{description && (
|
||||
<span style={{ marginLeft: 8, fontSize: '0.85em', opacity: 0.7 }}>
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
{children}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for control row - uses Ant Design grid
|
||||
*/
|
||||
export interface ControlRowProps {
|
||||
children: ReactNode;
|
||||
gutter?: number | [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* A row of controls using Ant Design's grid system
|
||||
* Automatically distributes controls evenly across columns
|
||||
*/
|
||||
export const ControlPanelRow: FC<ControlRowProps> = ({
|
||||
children,
|
||||
gutter = [16, 16],
|
||||
}) => {
|
||||
const childArray = Array.isArray(children) ? children : [children];
|
||||
const validChildren = childArray.filter(
|
||||
child => child !== null && child !== undefined,
|
||||
);
|
||||
const colSpan =
|
||||
validChildren.length > 0 ? Math.floor(24 / validChildren.length) : 24;
|
||||
|
||||
return (
|
||||
<Row gutter={gutter} style={{ marginBottom: 16 }}>
|
||||
{validChildren.map((child, index) => (
|
||||
<Col key={index} span={colSpan}>
|
||||
{child}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the main control panel layout
|
||||
*/
|
||||
export interface ControlPanelLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main control panel layout container
|
||||
*/
|
||||
export const ControlPanelLayout: FC<ControlPanelLayoutProps> = ({
|
||||
children,
|
||||
}) => (
|
||||
<div className="control-panel-layout" style={{ padding: '16px 0' }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a full-width single control row
|
||||
*/
|
||||
export const SingleControlRow: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>{children}</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a two-column control row
|
||||
*/
|
||||
export const TwoColumnRow: FC<{ left: ReactNode; right: ReactNode }> = ({
|
||||
left,
|
||||
right,
|
||||
}) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={12}>{left}</Col>
|
||||
<Col span={12}>{right}</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a three-column control row
|
||||
*/
|
||||
export const ThreeColumnRow: FC<{
|
||||
left: ReactNode;
|
||||
center: ReactNode;
|
||||
right: ReactNode;
|
||||
}> = ({ left, center, right }) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={8}>{left}</Col>
|
||||
<Col span={8}>{center}</Col>
|
||||
<Col span={8}>{right}</Col>
|
||||
</Row>
|
||||
);
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Select, Input, Slider } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface DeckGLControlsSectionProps {
|
||||
layerType?:
|
||||
| 'scatter'
|
||||
| 'polygon'
|
||||
| 'path'
|
||||
| 'heatmap'
|
||||
| 'hex'
|
||||
| 'grid'
|
||||
| 'screengrid'
|
||||
| 'contour'
|
||||
| 'geojson'
|
||||
| 'arc';
|
||||
showViewport?: boolean;
|
||||
showMapStyle?: boolean;
|
||||
showColorScheme?: boolean;
|
||||
showLegend?: boolean;
|
||||
showTooltip?: boolean;
|
||||
showFilters?: boolean;
|
||||
showAnimation?: boolean;
|
||||
show3D?: boolean;
|
||||
showMultiplier?: boolean;
|
||||
showPointRadius?: boolean;
|
||||
showLineWidth?: boolean;
|
||||
showFillColor?: boolean;
|
||||
showStrokeColor?: boolean;
|
||||
showOpacity?: boolean;
|
||||
showCoverage?: boolean;
|
||||
showElevation?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const DeckGLControlsSection: FC<DeckGLControlsSectionProps> = ({
|
||||
layerType = 'scatter',
|
||||
showViewport = true,
|
||||
showMapStyle = true,
|
||||
showColorScheme = true,
|
||||
showLegend = true,
|
||||
showTooltip = true,
|
||||
showFilters = true,
|
||||
showAnimation = false,
|
||||
show3D = false,
|
||||
showMultiplier = false,
|
||||
showPointRadius = false,
|
||||
showLineWidth = false,
|
||||
showFillColor = false,
|
||||
showStrokeColor = false,
|
||||
showOpacity = true,
|
||||
showCoverage = false,
|
||||
showElevation = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => (
|
||||
<div className="deckgl-controls-section">
|
||||
{/* Map Style */}
|
||||
{showMapStyle && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Map Style')}</label>
|
||||
<Select
|
||||
value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'}
|
||||
onChange={value => onChange('mapbox_style', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/streets-v11',
|
||||
label: t('Streets'),
|
||||
},
|
||||
{ value: 'mapbox://styles/mapbox/light-v9', label: t('Light') },
|
||||
{ value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') },
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/satellite-v9',
|
||||
label: t('Satellite'),
|
||||
},
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/outdoors-v11',
|
||||
label: t('Outdoors'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Base map style for the visualization')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Viewport */}
|
||||
{showViewport && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Zoom')}</label>
|
||||
<Slider
|
||||
value={values.zoom || 11}
|
||||
onChange={value => onChange('zoom', value)}
|
||||
min={0}
|
||||
max={22}
|
||||
step={0.1}
|
||||
marks={{ 0: '0', 11: '11', 22: '22' }}
|
||||
/>
|
||||
<small className="text-muted">{t('Map zoom level')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.autozoom || true}
|
||||
onChange={checked => onChange('autozoom', checked)}
|
||||
/>
|
||||
{t('Auto Zoom')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Automatically zoom to fit data bounds')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Point/Shape Size Controls */}
|
||||
{showPointRadius && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Point Radius')}</label>
|
||||
<Slider
|
||||
value={values.point_radius_fixed?.value || 1000}
|
||||
onChange={value =>
|
||||
onChange('point_radius_fixed', { type: 'fix', value })
|
||||
}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={10}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Fixed radius for points in meters')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showLineWidth && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Line Width')}</label>
|
||||
<Slider
|
||||
value={values.line_width || 1}
|
||||
onChange={value => onChange('line_width', value)}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
/>
|
||||
<small className="text-muted">{t('Width of lines in pixels')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* 3D Controls */}
|
||||
{show3D && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.extruded || false}
|
||||
onChange={checked => onChange('extruded', checked)}
|
||||
/>
|
||||
{t('3D')}
|
||||
</label>
|
||||
<small className="text-muted">{t('Show data in 3D')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
{values.extruded && showElevation && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Elevation')}</label>
|
||||
<Slider
|
||||
value={values.elevation || 0.1}
|
||||
onChange={value => onChange('elevation', value)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Elevation multiplier for 3D rendering')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Opacity */}
|
||||
{showOpacity && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Opacity')}</label>
|
||||
<Slider
|
||||
value={values.opacity || 80}
|
||||
onChange={value => onChange('opacity', value)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
marks={{ 0: '0%', 50: '50%', 100: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">{t('Layer opacity')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Coverage (for hex, grid) */}
|
||||
{showCoverage && (layerType === 'hex' || layerType === 'grid') && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Coverage')}</label>
|
||||
<Slider
|
||||
value={values.coverage || 1}
|
||||
onChange={value => onChange('coverage', value)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">{t('Cell coverage radius')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
{showLegend && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Legend Position')}</label>
|
||||
<Select
|
||||
value={values.legend_position || 'top_right'}
|
||||
onChange={value => onChange('legend_position', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'top_left', label: t('Top left') },
|
||||
{ value: 'top_right', label: t('Top right') },
|
||||
{ value: 'bottom_left', label: t('Bottom left') },
|
||||
{ value: 'bottom_right', label: t('Bottom right') },
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Legend Format')}</label>
|
||||
<Input
|
||||
value={values.legend_format || ''}
|
||||
onChange={e => onChange('legend_format', e.target.value)}
|
||||
placeholder=".3s"
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 number format for legend')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
{showFilters && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.filter_nulls || true}
|
||||
onChange={checked => onChange('filter_nulls', checked)}
|
||||
/>
|
||||
{t('Filter Nulls')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Filter out null values from data')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Tooltip */}
|
||||
{showTooltip && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Tooltip')}</label>
|
||||
<Input.TextArea
|
||||
value={values.js_tooltip || ''}
|
||||
onChange={e => onChange('js_tooltip', e.target.value)}
|
||||
placeholder={t('JavaScript tooltip generator')}
|
||||
rows={3}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('JavaScript code for custom tooltip')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Animation */}
|
||||
{showAnimation && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.animation || false}
|
||||
onChange={checked => onChange('animation', checked)}
|
||||
/>
|
||||
{t('Animate')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Animate visualization over time')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Multiplier for some visualizations */}
|
||||
{showMultiplier && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Multiplier')}</label>
|
||||
<Slider
|
||||
value={values.multiplier || 1}
|
||||
onChange={value => onChange('multiplier', value)}
|
||||
min={0.01}
|
||||
max={10}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">{t('Value multiplier')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DeckGLControlsSection;
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Select, Input, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface FilterControlsSectionProps {
|
||||
filterType: 'select' | 'range' | 'time' | 'time_column' | 'time_grain';
|
||||
showMultiple?: boolean;
|
||||
showSearch?: boolean;
|
||||
showParentFilter?: boolean;
|
||||
showDefaultValue?: boolean;
|
||||
showInverseSelection?: boolean;
|
||||
showDateFilter?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const FilterControlsSection: FC<FilterControlsSectionProps> = ({
|
||||
filterType,
|
||||
showMultiple = true,
|
||||
showSearch = true,
|
||||
showParentFilter = true,
|
||||
showDefaultValue = true,
|
||||
showInverseSelection = false,
|
||||
showDateFilter = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isSelect = filterType === 'select';
|
||||
const isRange = filterType === 'range';
|
||||
const isTime = filterType === 'time';
|
||||
const isTimeColumn = filterType === 'time_column';
|
||||
const isTimeGrain = filterType === 'time_grain';
|
||||
|
||||
return (
|
||||
<div className="filter-controls-section">
|
||||
{/* Multiple Selection */}
|
||||
{showMultiple && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.multiSelect || false}
|
||||
onChange={checked => onChange('multiSelect', checked)}
|
||||
/>
|
||||
{t('Multiple Select')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Allow selecting multiple values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
{showSearch && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.enableEmptyFilter || false}
|
||||
onChange={checked => onChange('enableEmptyFilter', checked)}
|
||||
/>
|
||||
{t('Enable Empty Filter')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Allow empty filter values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Inverse Selection */}
|
||||
{showInverseSelection && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.inverseSelection || false}
|
||||
onChange={checked => onChange('inverseSelection', checked)}
|
||||
/>
|
||||
{t('Inverse Selection')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Exclude selected values instead of including them')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Parent Filter */}
|
||||
{showParentFilter && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.parentFilter || false}
|
||||
onChange={checked => onChange('parentFilter', checked)}
|
||||
/>
|
||||
{t('Parent Filter')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Filter is dependent on another filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Default Value */}
|
||||
{showDefaultValue && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Default Value')}</label>
|
||||
{isSelect ? (
|
||||
<Input
|
||||
value={values.defaultValue || ''}
|
||||
onChange={e => onChange('defaultValue', e.target.value)}
|
||||
placeholder={t('Enter default value')}
|
||||
/>
|
||||
) : isRange ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<InputNumber
|
||||
value={values.defaultValueMin}
|
||||
onChange={value => onChange('defaultValueMin', value)}
|
||||
placeholder={t('Min')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<InputNumber
|
||||
value={values.defaultValueMax}
|
||||
onChange={value => onChange('defaultValueMax', value)}
|
||||
placeholder={t('Max')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={values.defaultValue || ''}
|
||||
onChange={e => onChange('defaultValue', e.target.value)}
|
||||
placeholder={t('Enter default value')}
|
||||
/>
|
||||
)}
|
||||
<small className="text-muted">
|
||||
{t('Default value to use when filter is first loaded')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Sort Options for Select */}
|
||||
{isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Sort Filter Values')}</label>
|
||||
<Select
|
||||
value={values.sortFilter || false}
|
||||
onChange={value => onChange('sortFilter', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: false, label: t('No Sort') },
|
||||
{ value: true, label: t('Sort Ascending') },
|
||||
{ value: 'desc', label: t('Sort Descending') },
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Sort filter values alphabetically')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Search for Select Filter */}
|
||||
{isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.searchAllOptions || false}
|
||||
onChange={checked => onChange('searchAllOptions', checked)}
|
||||
/>
|
||||
{t('Search All Options')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Search all filter options, not just displayed ones')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Range Options */}
|
||||
{isRange && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Min Value')}</label>
|
||||
<InputNumber
|
||||
value={values.rangeMin}
|
||||
onChange={value => onChange('rangeMin', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Minimum value for the range')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Max Value')}</label>
|
||||
<InputNumber
|
||||
value={values.rangeMax}
|
||||
onChange={value => onChange('rangeMax', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Maximum value for the range')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Time Options */}
|
||||
{(isTime || isTimeColumn) && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.defaultToFirstValue || false}
|
||||
onChange={checked => onChange('defaultToFirstValue', checked)}
|
||||
/>
|
||||
{t('Default to First Value')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Default to the first available time value')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Time Grain Options */}
|
||||
{isTimeGrain && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Default Time Grain')}</label>
|
||||
<Select
|
||||
value={values.defaultTimeGrain || 'day'}
|
||||
onChange={value => onChange('defaultTimeGrain', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'minute', label: t('Minute') },
|
||||
{ value: 'hour', label: t('Hour') },
|
||||
{ value: 'day', label: t('Day') },
|
||||
{ value: 'week', label: t('Week') },
|
||||
{ value: 'month', label: t('Month') },
|
||||
{ value: 'quarter', label: t('Quarter') },
|
||||
{ value: 'year', label: t('Year') },
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Default time granularity')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* UI Configuration */}
|
||||
<h4 style={{ marginTop: 24, marginBottom: 16 }}>
|
||||
{t('UI Configuration')}
|
||||
</h4>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.instant_filtering || true}
|
||||
onChange={checked => onChange('instant_filtering', checked)}
|
||||
/>
|
||||
{t('Instant Filtering')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Apply filters instantly as they change')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_apply || false}
|
||||
onChange={checked => onChange('show_apply', checked)}
|
||||
/>
|
||||
{t('Show Apply Button')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show an apply button for the filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_clear || true}
|
||||
onChange={checked => onChange('show_clear', checked)}
|
||||
/>
|
||||
{t('Show Clear Button')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show a clear button for the filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterControlsSection;
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface FormatControlGroupProps {
|
||||
showNumber?: boolean;
|
||||
showCurrency?: boolean;
|
||||
showDate?: boolean;
|
||||
showPercentage?: boolean;
|
||||
numberFormatLabel?: string;
|
||||
currencyFormatLabel?: string;
|
||||
dateFormatLabel?: string;
|
||||
percentageFormatLabel?: string;
|
||||
customFormatOptions?: Array<[string, string]>;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
export const D3_FORMAT_OPTIONS = [
|
||||
['SMART_NUMBER', t('Adaptive formatting')],
|
||||
['~g', t('Original value')],
|
||||
['d', t('Signed integer')],
|
||||
['.0f', t('Integer')],
|
||||
['.1f', t('1 decimal place')],
|
||||
['.2f', t('2 decimal places')],
|
||||
['.3f', t('3 decimal places')],
|
||||
['.4f', t('4 decimal places')],
|
||||
['.5f', t('5 decimal places')],
|
||||
['+,', t('Positive integer')],
|
||||
['+,.0f', t('Positive number')],
|
||||
['+,.1f', t('Positive (1 decimal)')],
|
||||
['+,.2f', t('Positive (2 decimals)')],
|
||||
[',.0f', t('Number (no decimals)')],
|
||||
[',.1f', t('Number (1 decimal)')],
|
||||
[',.2f', t('Number (2 decimals)')],
|
||||
[',.3f', t('Number (3 decimals)')],
|
||||
['.0%', t('Percentage')],
|
||||
['.1%', t('Percentage (1 decimal)')],
|
||||
['.2%', t('Percentage (2 decimals)')],
|
||||
['.3%', t('Percentage (3 decimals)')],
|
||||
[',.0%', t('Percentage with thousands')],
|
||||
['.1s', t('SI notation')],
|
||||
['.2s', t('SI notation (2 decimals)')],
|
||||
['.3s', t('SI notation (3 decimals)')],
|
||||
['$,.0f', t('Currency (no decimals)')],
|
||||
['$,.1f', t('Currency (1 decimal)')],
|
||||
['$,.2f', t('Currency (2 decimals)')],
|
||||
['$,.3f', t('Currency (3 decimals)')],
|
||||
];
|
||||
|
||||
export const D3_TIME_FORMAT_OPTIONS = [
|
||||
['smart_date', t('Adaptive formatting')],
|
||||
['%Y-%m-%d', t('YYYY-MM-DD')],
|
||||
['%Y-%m-%d %H:%M', t('YYYY-MM-DD HH:MM')],
|
||||
['%Y-%m-%d %H:%M:%S', t('YYYY-MM-DD HH:MM:SS')],
|
||||
['%Y/%m/%d', t('YYYY/MM/DD')],
|
||||
['%m/%d/%Y', t('MM/DD/YYYY')],
|
||||
['%d/%m/%Y', t('DD/MM/YYYY')],
|
||||
['%d.%m.%Y', t('DD.MM.YYYY')],
|
||||
['%Y', t('Year (YYYY)')],
|
||||
['%B %Y', t('Month Year (January 2023)')],
|
||||
['%b %Y', t('Month Year (Jan 2023)')],
|
||||
['%B', t('Month (January)')],
|
||||
['%b', t('Month (Jan)')],
|
||||
['%B %-d, %Y', t('Month Day, Year')],
|
||||
['%b %-d, %Y', t('Mon Day, Year')],
|
||||
['%a', t('Day of week (short)')],
|
||||
['%A', t('Day of week (full)')],
|
||||
['%H:%M', t('Time (24-hour)')],
|
||||
['%I:%M %p', t('Time (12-hour)')],
|
||||
['%H:%M:%S', t('Time with seconds')],
|
||||
];
|
||||
|
||||
const CURRENCY_OPTIONS = [
|
||||
{ value: 'USD', label: 'USD ($)' },
|
||||
{ value: 'EUR', label: 'EUR (€)' },
|
||||
{ value: 'GBP', label: 'GBP (£)' },
|
||||
{ value: 'JPY', label: 'JPY (¥)' },
|
||||
{ value: 'CNY', label: 'CNY (¥)' },
|
||||
{ value: 'INR', label: 'INR (₹)' },
|
||||
{ value: 'CAD', label: 'CAD ($)' },
|
||||
{ value: 'AUD', label: 'AUD ($)' },
|
||||
{ value: 'CHF', label: 'CHF (Fr)' },
|
||||
{ value: 'SEK', label: 'SEK (kr)' },
|
||||
{ value: 'NOK', label: 'NOK (kr)' },
|
||||
{ value: 'DKK', label: 'DKK (kr)' },
|
||||
{ value: 'KRW', label: 'KRW (₩)' },
|
||||
{ value: 'BRL', label: 'BRL (R$)' },
|
||||
{ value: 'MXN', label: 'MXN ($)' },
|
||||
{ value: 'RUB', label: 'RUB (₽)' },
|
||||
];
|
||||
|
||||
const FormatControlGroup: FC<FormatControlGroupProps> = ({
|
||||
showNumber = true,
|
||||
showCurrency = false,
|
||||
showDate = false,
|
||||
showPercentage = false,
|
||||
numberFormatLabel = t('Number format'),
|
||||
currencyFormatLabel = t('Currency'),
|
||||
dateFormatLabel = t('Date format'),
|
||||
percentageFormatLabel = t('Percentage format'),
|
||||
customFormatOptions = [],
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const formatOptions =
|
||||
customFormatOptions.length > 0 ? customFormatOptions : D3_FORMAT_OPTIONS;
|
||||
|
||||
return (
|
||||
<div className="format-control-group">
|
||||
{showNumber && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{numberFormatLabel}</label>
|
||||
<Select
|
||||
value={values.number_format || 'SMART_NUMBER'}
|
||||
onChange={value => onChange('number_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={formatOptions.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 format string for numbers. See ')}
|
||||
<a
|
||||
href="https://github.com/d3/d3-format/blob/main/README.md#format"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('D3 format docs')}
|
||||
</a>
|
||||
{t(' for details.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showCurrency && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{currencyFormatLabel}</label>
|
||||
<Select
|
||||
value={values.currency_format || 'USD'}
|
||||
onChange={value => onChange('currency_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select currency')}
|
||||
options={CURRENCY_OPTIONS}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Currency to use for formatting')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showDate && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{dateFormatLabel}</label>
|
||||
<Select
|
||||
value={values.date_format || 'smart_date'}
|
||||
onChange={value => onChange('date_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 time format string. See ')}
|
||||
<a
|
||||
href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('D3 time format docs')}
|
||||
</a>
|
||||
{t(' for details.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showPercentage && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{percentageFormatLabel}</label>
|
||||
<Select
|
||||
value={values.percentage_format || '.0%'}
|
||||
onChange={value => onChange('percentage_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={[
|
||||
['.0%', t('0%')],
|
||||
['.1%', t('0.1%')],
|
||||
['.2%', t('0.12%')],
|
||||
['.3%', t('0.123%')],
|
||||
[',.0%', t('1,234%')],
|
||||
[',.1%', t('1,234.5%')],
|
||||
].map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 format for percentages')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormatControlGroup;
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, useMemo } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select } from '@superset-ui/core/components';
|
||||
import { ControlComponentProps, ColumnMeta } from '../../types';
|
||||
import { ControlHeader } from '../../components/ControlHeader';
|
||||
|
||||
export interface GranularityControlValue {
|
||||
column_name: string;
|
||||
type?: string;
|
||||
is_dttm?: boolean;
|
||||
}
|
||||
|
||||
export interface GranularityControlProps
|
||||
extends ControlComponentProps<GranularityControlValue | string> {
|
||||
columns?: ColumnMeta[];
|
||||
datasource?: {
|
||||
columns?: ColumnMeta[];
|
||||
verbose_map?: Record<string, string>;
|
||||
};
|
||||
clearable?: boolean;
|
||||
temporalColumnsOnly?: boolean;
|
||||
}
|
||||
|
||||
const GranularityControl: FC<GranularityControlProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
columns = [],
|
||||
datasource,
|
||||
clearable = false,
|
||||
temporalColumnsOnly = true,
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
validationErrors,
|
||||
renderTrigger,
|
||||
...props
|
||||
}) => {
|
||||
const allColumns = useMemo(() => {
|
||||
const cols = columns.length > 0 ? columns : datasource?.columns || [];
|
||||
if (temporalColumnsOnly) {
|
||||
return cols.filter(col => col.is_dttm);
|
||||
}
|
||||
return cols;
|
||||
}, [columns, datasource?.columns, temporalColumnsOnly]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
allColumns.map(col => ({
|
||||
value: col.column_name,
|
||||
label:
|
||||
datasource?.verbose_map?.[col.column_name] ||
|
||||
col.verbose_name ||
|
||||
col.column_name,
|
||||
})),
|
||||
[allColumns, datasource?.verbose_map],
|
||||
);
|
||||
|
||||
const currentValue = useMemo(() => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return value?.column_name;
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (newValue: string | undefined) => {
|
||||
if (onChange) {
|
||||
if (!newValue && clearable) {
|
||||
onChange(null as any);
|
||||
} else if (newValue) {
|
||||
const column = allColumns.find(col => col.column_name === newValue);
|
||||
if (column) {
|
||||
onChange({
|
||||
column_name: column.column_name,
|
||||
type: column.type,
|
||||
is_dttm: column.is_dttm,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader
|
||||
name={name}
|
||||
label={label || t('Time Column')}
|
||||
description={
|
||||
description ||
|
||||
t(
|
||||
'The time column for the visualization. Note that you ' +
|
||||
'can define arbitrary expression that return a DATETIME ' +
|
||||
'column in the table. Also note that the ' +
|
||||
'filter below is applied against this column or ' +
|
||||
'expression',
|
||||
)
|
||||
}
|
||||
validationErrors={validationErrors}
|
||||
renderTrigger={renderTrigger}
|
||||
/>
|
||||
<Select
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
placeholder={t('Select a temporal column')}
|
||||
allowClear={clearable}
|
||||
showSearch
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GranularityControl;
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { CustomControlItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Helper function to create a SelectControl configuration
|
||||
*/
|
||||
export const SelectControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
choices?: any[][] | (() => any[][]) | any[];
|
||||
description?: string;
|
||||
freeForm?: boolean;
|
||||
clearable?: boolean;
|
||||
multiple?: boolean;
|
||||
validators?: any[];
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a TextControl configuration
|
||||
*/
|
||||
export const TextControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
description?: string;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
validators?: any[];
|
||||
renderTrigger?: boolean;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a CheckboxControl configuration
|
||||
*/
|
||||
export const CheckboxControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SliderControl configuration
|
||||
*/
|
||||
export const SliderControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SliderControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a RadioButtonControl configuration
|
||||
*/
|
||||
export const RadioButtonControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
options?: any[][] | any[];
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'RadioButtonControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a BoundsControl configuration
|
||||
*/
|
||||
export const BoundsControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: [number | null, number | null];
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'BoundsControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a ColorPickerControl configuration
|
||||
*/
|
||||
export const ColorPickerControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: { r: number; g: number; b: number; a: number };
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'ColorPickerControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a DateFilterControl configuration
|
||||
*/
|
||||
export const DateFilterControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: string;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'DateFilterControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SwitchControl configuration
|
||||
*/
|
||||
export const SwitchControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SwitchControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a HiddenControl configuration
|
||||
*/
|
||||
export const HiddenControl = (config: {
|
||||
name: string;
|
||||
default?: any;
|
||||
initialValue?: any;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'HiddenControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SpatialControl configuration
|
||||
*/
|
||||
export const SpatialControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
validators?: any[];
|
||||
mapStateToProps?: (state: any) => any;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SpatialControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a ContourControl configuration
|
||||
*/
|
||||
export const ContourControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
mapStateToProps?: (state: any) => any;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'ContourControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a TextAreaControl configuration
|
||||
*/
|
||||
export const TextAreaControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: string;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'TextAreaControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user