Compare commits

..

2 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,7 @@
# https://github.com/apache/superset/issues/13351
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho
# Notify some committers of changes in the components

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

@@ -131,6 +131,3 @@ superset/static/stats/statistics.html
# LLM-related
CLAUDE.local.md
.aider*
.claude_rc*
.env.local
PROJECT.md

View File

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

View File

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

45
LLMS.md
View File

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

View File

@@ -32,10 +32,11 @@ else
SUPERSET_VERSION="${1}"
SUPERSET_RC="${2}"
SUPERSET_PGP_FULLNAME="${3}"
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
SUPERSET_RELEASE_RC_TARBALL="apache_superset-${SUPERSET_VERSION_RC}-source.tar.gz"
fi
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
if [ -z "${SUPERSET_SVN_DEV_PATH}" ]; then
SUPERSET_SVN_DEV_PATH="$HOME/svn/superset_dev"
fi

View File

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

View File

@@ -94,9 +94,9 @@ under the License.
| can available domains on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can request access on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can post on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can expanded on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can delete on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can post on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can expanded on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can post on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can delete query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|

View File

@@ -23,15 +23,6 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
- Change `"error.base"` to just `"error"` after this PR
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
- Custom colors are no longer supported to maintain consistency with Ant Design components
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
- [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`.
@@ -39,7 +30,6 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
## 5.0.0

View File

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

View File

@@ -1,217 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# -----------------------------------------------------------------------
# Lightweight docker-compose for running multiple Superset instances
# This includes only essential services: database and Superset app (no Redis)
#
# RUNNING SUPERSET:
# 1. Start services: docker-compose -f docker-compose-light.yml up
# 2. Access at: http://localhost:9001 (or NODE_PORT if specified)
#
# RUNNING MULTIPLE INSTANCES:
# - 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
#
# RUNNING TESTS WITH PYTEST:
# Tests run in an isolated environment with a separate test database.
# The pytest-runner service automatically creates and initializes the test database on first use.
#
# Basic usage:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/
#
# Run specific test file:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/test_foo.py
#
# Run with pytest options:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest -v -s -x tests/
#
# Force reload test database and run tests (when tests are failing due to bad state):
# docker-compose -f docker-compose-light.yml run --rm -e FORCE_RELOAD=true pytest-runner pytest tests/
#
# Run any command in test environment:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner bash
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest --collect-only
#
# For parallel test execution with different projects:
# docker-compose -p project1 -f docker-compose-light.yml run --rm pytest-runner pytest tests/
#
# DEVELOPMENT TIPS:
# - First test run takes ~20-30 seconds (database creation + initialization)
# - Subsequent runs are fast (~2-3 seconds startup)
# - Use FORCE_RELOAD=true when you need a clean test database
# - Tests use SimpleCache instead of Redis (no Redis required)
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed 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
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
# Increase max connections for test runs
command: postgres -c max_connections=200
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
pytest-runner:
build:
<<: *common-build
entrypoint: ["/app/docker/docker-pytest-entrypoint.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
profiles:
- test # Only starts when --profile test is used
depends_on:
db-light:
condition: service_started
user: *superset-user
volumes: *superset-volumes
environment:
# Test-specific database configuration
DATABASE_HOST: db-light
DATABASE_DB: test
POSTGRES_DB: test
# Point to test database
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@db-light:5432/test
# Use the light test config that doesn't require Redis
SUPERSET_CONFIG: superset_test_config_light
# Python path includes test directory
PYTHONPATH: /app/pythonpath:/app/docker/pythonpath_dev:/app
volumes:
superset_home_light:
external: false
db_home_light:
external: false

View File

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

View File

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

View File

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

View File

@@ -1,152 +0,0 @@
#!/bin/bash
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -e
# Wait for PostgreSQL to be ready
echo "Waiting for database to be ready..."
for i in {1..30}; do
if python3 -c "
import psycopg2
try:
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.close()
print('Database is ready!')
except:
exit(1)
" 2>/dev/null; then
echo "Database connection established!"
break
fi
echo "Waiting for database... ($i/30)"
if [ $i -eq 30 ]; then
echo "Database connection timeout after 30 seconds"
exit 1
fi
sleep 1
done
# Handle database setup based on FORCE_RELOAD
if [ "${FORCE_RELOAD}" = "true" ]; then
echo "Force reload requested - resetting test database"
# Drop and recreate the test database using Python
python3 -c "
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
# Connect to default database
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
# Drop and recreate test database
try:
cur.execute('DROP DATABASE IF EXISTS test')
except:
pass
cur.execute('CREATE DATABASE test')
conn.close()
# Connect to test database to create schemas
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute('CREATE SCHEMA sqllab_test_db')
cur.execute('CREATE SCHEMA admin_database')
cur.close()
conn.close()
print('Test database reset successfully')
"
# Use --no-reset-db since we already reset it
FLAGS="--no-reset-db"
else
echo "Using existing test database (set FORCE_RELOAD=true to reset)"
FLAGS="--no-reset-db"
# Ensure test database exists using Python
python3 -c "
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
# Check if test database exists
try:
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.close()
print('Test database already exists')
except:
print('Creating test database...')
# Connect to default database to create test database
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
# Create test database
cur.execute('CREATE DATABASE test')
conn.close()
# Connect to test database to create schemas
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute('CREATE SCHEMA IF NOT EXISTS sqllab_test_db')
cur.execute('CREATE SCHEMA IF NOT EXISTS admin_database')
cur.close()
conn.close()
print('Test database created successfully')
"
fi
# Always run database migrations to ensure schema is up to date
echo "Running database migrations..."
cd /app
superset db upgrade
# Initialize test environment if needed
if [ "${FORCE_RELOAD}" = "true" ] || [ ! -f "/app/superset_home/.test_initialized" ]; then
echo "Initializing test environment..."
# Run initialization commands
superset init
echo "Loading test users..."
superset load-test-users
# Mark as initialized
touch /app/superset_home/.test_initialized
else
echo "Test environment already initialized (skipping init and load-test-users)"
echo "Tip: Use FORCE_RELOAD=true to reinitialize the test database"
fi
# Create missing scripts needed for tests
if [ ! -f "/app/scripts/tag_latest_release.sh" ]; then
echo "Creating missing tag_latest_release.sh script for tests..."
cp /app/docker/tag_latest_release.sh /app/scripts/tag_latest_release.sh 2>/dev/null || true
fi
# Install pip module for Shillelagh compatibility (aligns with CI environment)
echo "Installing pip module for Shillelagh compatibility..."
uv pip install pip
# If arguments provided, execute them
if [ $# -gt 0 ]; then
exec "$@"
fi

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,55 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Test configuration for docker-compose-light.yml - uses SimpleCache instead of Redis
# Import all settings from the main test config first
import os
import sys
# Add the tests directory to the path to import the test config
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from tests.integration_tests.superset_test_config import * # noqa: F403
# Override Redis-based caching to use simple in-memory cache
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_KEY_PREFIX": "superset_test_",
}
DATA_CACHE_CONFIG = {
**CACHE_CONFIG,
"CACHE_DEFAULT_TIMEOUT": 30,
"CACHE_KEY_PREFIX": "superset_test_data_",
}
# Keep SimpleCache for these as they're already using it
# FILTER_STATE_CACHE_CONFIG - already SimpleCache in parent
# EXPLORE_FORM_DATA_CACHE_CONFIG - already SimpleCache in parent
# Disable Celery for lightweight testing
CELERY_CONFIG = None
# Use FileSystemCache for SQL Lab results instead of Redis
from flask_caching.backends.filesystemcache import FileSystemCache # noqa: E402
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab_test")
# Override WEBDRIVER_BASEURL for tests to match expected values
WEBDRIVER_BASEURL = "http://0.0.0.0:8080/"
WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL

View File

@@ -1,190 +0,0 @@
#! /bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
run_git_tag () {
if [[ "$DRY_RUN" == "false" ]] && [[ "$SKIP_TAG" == "false" ]]
then
git tag -a -f latest "${GITHUB_TAG_NAME}" -m "latest tag"
echo "${GITHUB_TAG_NAME} has been tagged 'latest'"
fi
exit 0
}
###
# separating out git commands into functions so they can be mocked in unit tests
###
git_show_ref () {
if [[ "$TEST_ENV" == "true" ]]
then
if [[ "$GITHUB_TAG_NAME" == "does_not_exist" ]]
# mock return for testing only
then
echo ""
else
echo "2817aebd69dc7d199ec45d973a2079f35e5658b6 refs/tags/${GITHUB_TAG_NAME}"
fi
fi
result=$(git show-ref "${GITHUB_TAG_NAME}")
echo "${result}"
}
get_latest_tag_list () {
if [[ "$TEST_ENV" == "true" ]]
then
echo "(tag: 2.1.0, apache/2.1test)"
else
result=$(git show-ref --tags --dereference latest | awk '{print $2}' | xargs git show --pretty=tformat:%d -s | grep tag:)
echo "${result}"
fi
}
###
split_string () {
local version="$1"
local delimiter="$2"
local components=()
local tmp=""
for (( i=0; i<${#version}; i++ )); do
local char="${version:$i:1}"
if [[ "$char" != "$delimiter" ]]; then
tmp="$tmp$char"
elif [[ -n "$tmp" ]]; then
components+=("$tmp")
tmp=""
fi
done
if [[ -n "$tmp" ]]; then
components+=("$tmp")
fi
echo "${components[@]}"
}
DRY_RUN=false
# get params passed in with script when it was run
# --dry-run is optional and returns the value of SKIP_TAG, but does not run the git tag statement
# A tag name is required as a param. A SHA won't work. You must first tag a sha with a release number
# and then run this script
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--dry-run)
DRY_RUN=true
shift # past value
;;
*) # this should be the tag name
GITHUB_TAG_NAME=$key
shift # past value
;;
esac
done
if [ -z "${GITHUB_TAG_NAME}" ]; then
echo "Missing tag parameter, usage: ./scripts/tag_latest_release.sh <GITHUB_TAG_NAME>"
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 1
fi
if [ -z "$(git_show_ref)" ]; then
echo "The tag ${GITHUB_TAG_NAME} does not exist. Please use a different tag."
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 0
fi
# check that this tag only contains a proper semantic version
if ! [[ ${GITHUB_TAG_NAME} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
echo "This tag ${GITHUB_TAG_NAME} is not a valid release version. Not tagging."
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 1
fi
## split the current GITHUB_TAG_NAME into an array at the dot
THIS_TAG_NAME=$(split_string "${GITHUB_TAG_NAME}" ".")
# look up the 'latest' tag on git
LATEST_TAG_LIST=$(get_latest_tag_list) || echo 'not found'
# if 'latest' tag doesn't exist, then set this commit to latest
if [[ -z "$LATEST_TAG_LIST" ]]
then
echo "there are no latest tags yet, so I'm going to start by tagging this sha as the latest"
run_git_tag
exit 0
fi
# remove parenthesis and tag: from the list of tags
LATEST_TAGS_STRINGS=$(echo "$LATEST_TAG_LIST" | sed 's/tag: \([^,]*\)/\1/g' | tr -d '()')
LATEST_TAGS=$(split_string "$LATEST_TAGS_STRINGS" ",")
TAGS=($(split_string "$LATEST_TAGS" " "))
# Initialize a flag for comparison result
compare_result=""
# Iterate through the tags of the latest release
for tag in $TAGS
do
if [[ $tag == "latest" ]]; then
continue
else
## extract just the version from this tag
LATEST_RELEASE_TAG="$tag"
echo "LATEST_RELEASE_TAG: ${LATEST_RELEASE_TAG}"
# check that this only contains a proper semantic version
if ! [[ ${LATEST_RELEASE_TAG} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
echo "'Latest' has been associated with tag ${LATEST_RELEASE_TAG} which is not a valid release version. Looking for another."
continue
fi
echo "The current release with the latest tag is version ${LATEST_RELEASE_TAG}"
# Split the version strings into arrays
THIS_TAG_NAME_ARRAY=($(split_string "$THIS_TAG_NAME" "."))
LATEST_RELEASE_TAG_ARRAY=($(split_string "$LATEST_RELEASE_TAG" "."))
# Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater"
break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser"
break
fi
done
fi
done
# Determine the result based on the comparison
if [[ -z "$compare_result" ]]; then
echo "Versions are equal"
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
elif [[ "$compare_result" == "greater" ]]; then
echo "This release tag ${GITHUB_TAG_NAME} is newer than the latest."
echo "SKIP_TAG=false" >> $GITHUB_OUTPUT
# Add other actions you want to perform for a newer version
elif [[ "$compare_result" == "lesser" ]]; then
echo "This release tag ${GITHUB_TAG_NAME} is older than the latest."
echo "This release tag ${GITHUB_TAG_NAME} is not the latest. Not tagging."
# if you've gotten this far, then we don't want to run any tags in the next step
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
fi

View File

@@ -10,183 +10,44 @@ version: 1
apache-superset>=6.0
:::
Superset now rides on **Ant Design v5's token-based theming**.
Superset now rides on **Ant Design v5s token-based theming**.
Every Antd token works, plus a handful of Superset-specific ones for charts and dashboard chrome.
## Managing Themes via UI
## 1 — Create a theme
Superset includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
### Creating a New Theme
1. Navigate to **Settings > Themes** in the Superset interface
2. Click **+ Theme** to create a new theme
3. Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to design your theme:
- Design your palette, typography, and component overrides
- Open the `CONFIG` modal and copy the JSON configuration
4. Paste the JSON into the theme definition field in Superset
5. Give your theme a descriptive name and save
1. Open the official [Ant Design Theme Editor](https://ant.design/theme-editor)
2. Design your palette, typography, and component overrides.
3. Open the `CONFIG` modal and paste the JSON.
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
### System Theme Administration
When `ENABLE_UI_THEME_ADMINISTRATION = True` is configured, administrators can manage system-wide themes directly from the UI:
#### Setting System Themes
- **System Default Theme**: Click the sun icon on any theme to set it as the system-wide default
- **System Dark Theme**: Click the moon icon on any theme to set it as the system dark mode theme
- **Automatic OS Detection**: When both default and dark themes are set, Superset automatically detects and applies the appropriate theme based on OS preferences
#### Managing System Themes
- System themes are indicated with special badges in the theme list
- Only administrators with write permissions can modify system theme settings
- Removing a system theme designation reverts to configuration file defaults
### 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
## Configuration Options
### Python Configuration
Configure theme behavior via `superset_config.py`:
## 2 — Apply it instance-wide
```python
# Enable UI-based theme administration for admins
ENABLE_UI_THEME_ADMINISTRATION = True
# Optional: Set initial default themes via configuration
# These can be overridden via the UI when ENABLE_UI_THEME_ADMINISTRATION = True
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
"colorSuccess": "#5ac189",
# ... your theme JSON configuration
}
# superset_config.py
THEME = {
# Paste your JSON theme definition here
}
# Optional: Dark theme configuration
THEME_DARK = {
"algorithm": "dark",
"token": {
"colorPrimary": "#2893B3",
# ... your dark theme overrides
}
}
# To force a single theme on all users, set THEME_DARK = None
# When both themes are defined (via UI or config):
# - Users can manually switch between themes
# - OS preference detection is automatically enabled
```
### Migration from Configuration to UI
Restart Superset to apply changes
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
1. System themes set via the UI take precedence over configuration file settings
2. The UI shows which themes are currently set as system defaults
3. Administrators can change system themes without restarting Superset
4. Configuration file themes serve as fallbacks when no UI themes are set
### Copying Themes Between Systems
To export a theme for use in configuration files or another instance:
1. Navigate to **Settings > Themes** and click the export icon on your desired theme
2. Extract the JSON configuration from the exported YAML file
3. Use this JSON in your `superset_config.py` or import it into another Superset instance
## Theme Development Workflow
1. **Design**: Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to iterate on your design
2. **Test**: Create themes in Superset's CRUD interface for testing
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
## Custom Fonts
Superset supports custom fonts through runtime configuration, allowing you to use branded or custom typefaces without rebuilding the application.
### Configuring Custom Fonts
Add font URLs to your `superset_config.py`:
## 3 — Tweak live in the app (beta)
Set the feature flag in your `superset_config`
```python
# Load fonts from Google Fonts, Adobe Fonts, or self-hosted sources
CUSTOM_FONT_URLS = [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
]
# Update CSP to allow font sources
TALISMAN_CONFIG = {
"content_security_policy": {
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
}
DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
{{ ... }}
THEME_ALLOW_THEME_EDITOR_BETA = True,
}
```
### Using Custom Fonts in Themes
- Enables a JSON editor panel inside Superset as a new icon in the navbar
- Intended for testing/design and rapid in-context iteration
- End-user theme switching & preferences coming later
Once configured, reference the fonts in your theme configuration:
## 4 — Potential Next Steps
```python
THEME_DEFAULT = {
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace",
# ... other theme tokens
}
}
```
Or in the CRUD interface theme JSON:
```json
{
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace"
}
}
```
### Font Sources
- **Google Fonts**: Free, CDN-hosted fonts with wide variety
- **Adobe Fonts**: Premium fonts (requires subscription and kit ID)
- **Self-hosted**: Place font files in `/static/assets/fonts/` and reference via CSS
This feature works with the stock Docker image - no custom build required!
## Advanced Features
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
- **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
- **OS Dark Mode Detection**: Automatically switches themes based on system preferences
- **Theme Import/Export**: Share themes between instances via YAML files
## API Access
For programmatic theme management, Superset provides REST endpoints:
- `GET /api/v1/theme/` - List all themes
- `POST /api/v1/theme/` - Create a new theme
- `PUT /api/v1/theme/{id}` - Update a theme
- `DELETE /api/v1/theme/{id}` - Delete a theme
- `PUT /api/v1/theme/{id}/set_system_default` - Set as system default theme (admin only)
- `PUT /api/v1/theme/{id}/set_system_dark` - Set as system dark theme (admin only)
- `DELETE /api/v1/theme/unset_system_default` - Remove system default designation
- `DELETE /api/v1/theme/unset_system_dark` - Remove system dark designation
- `GET /api/v1/theme/export/` - Export themes as YAML
- `POST /api/v1/theme/import/` - Import themes from YAML
These endpoints require appropriate permissions and are subject to RBAC controls.
- CRUD UI for managing multiple themes
- Per-dashboard & per-workspace theme assignment
- User-selectable theme preferences

View File

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

View File

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

View File

@@ -2,20 +2,6 @@
title: CVEs fixed by release
sidebar_position: 2
---
#### Version 5.0.0
| CVE | Title | Affected |
|:---------------|:-----------------------------------------------------------------------------------|---------:|
| CVE-2025-55673 | Exposure of Sensitive Information to an Unauthorized Actor | < 5.0.0 |
| CVE-2025-55674 | Improper Neutralization of Special Elements used in an SQL Command | < 5.0.0 |
| CVE-2025-55675 | Improper Access Control leading to Information Disclosure | < 5.0.0 |
#### Version 4.1.3
| CVE | Title | Affected |
|:---------------|:-----------------------------------------------------------------------------------|---------:|
| CVE-2025-55672 | Improper Neutralization of Input During Web Page Generation | < 4.1.3 |
#### Version 4.1.2
| CVE | Title | Affected |

View File

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

View File

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

View File

@@ -2150,7 +2150,14 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.1"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz"
integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
@@ -2198,20 +2205,20 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.32.0", "@eslint/js@^9.32.0":
version "9.32.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
"@eslint/plugin-kit@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz#c6b9f165e94bf4d9fdd493f1c028a94aaf5fc1cc"
integrity sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==
"@eslint/plugin-kit@^0.3.1":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
dependencies:
"@eslint/core" "^0.15.1"
levn "^0.4.1"
@@ -2505,10 +2512,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -3415,10 +3422,10 @@
dependencies:
"@types/estree" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.6"
@@ -3717,79 +3724,79 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.39.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz#c9afec1866ee1a6ea3d768b5f8e92201efbbba06"
integrity sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==
"@typescript-eslint/eslint-plugin@8.37.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/type-utils" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/type-utils" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.39.0", "@typescript-eslint/parser@^8.37.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.0.tgz#c4b895d7a47f4cd5ee6ee77ea30e61d58b802008"
integrity sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==
"@typescript-eslint/parser@8.37.0", "@typescript-eslint/parser@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
dependencies:
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz#71cb29c3f8139f99a905b8705127bffc2ae84759"
integrity sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==
"@typescript-eslint/project-service@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.39.0"
"@typescript-eslint/types" "^8.39.0"
"@typescript-eslint/tsconfig-utils" "^8.37.0"
"@typescript-eslint/types" "^8.37.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz#ba4bf6d8257bbc172c298febf16bc22df4856570"
integrity sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==
"@typescript-eslint/scope-manager@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
dependencies:
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/tsconfig-utils@8.39.0", "@typescript-eslint/tsconfig-utils@^8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz#b2e87fef41a3067c570533b722f6af47be213f13"
integrity sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==
"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
"@typescript-eslint/type-utils@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz#310ec781ae5e7bb0f5940bfd652573587f22786b"
integrity sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==
"@typescript-eslint/type-utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
dependencies:
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.39.0", "@typescript-eslint/types@^8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.0.tgz#80f010b7169d434a91cd0529d70a528dbc9c99c6"
integrity sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==
"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
"@typescript-eslint/typescript-estree@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz#b9477a5c47a0feceffe91adf553ad9a3cd4cb3d6"
integrity sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==
"@typescript-eslint/typescript-estree@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
dependencies:
"@typescript-eslint/project-service" "8.39.0"
"@typescript-eslint/tsconfig-utils" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
"@typescript-eslint/project-service" "8.37.0"
"@typescript-eslint/tsconfig-utils" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -3797,22 +3804,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.0.tgz#dfea42f3c7ec85f9f3e994ff0bba8f3b2f09e220"
integrity sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==
"@typescript-eslint/utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz#5d619a6e810cdd3fd1913632719cbccab08bf875"
integrity sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==
"@typescript-eslint/visitor-keys@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
dependencies:
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/types" "8.37.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -3959,11 +3966,6 @@ accepts@~1.3.4, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-import-phases@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -3976,7 +3978,12 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -4100,10 +4107,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.7:
version "5.26.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
@@ -4116,7 +4123,7 @@ antd@^5.26.7:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.3.0"
"@rc-component/trigger" "^2.2.7"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4146,7 +4153,7 @@ antd@^5.26.7:
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-tabs "~15.6.1"
rc-textarea "~1.10.1"
rc-textarea "~1.10.0"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
@@ -4313,12 +4320,12 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-loader@^9.2.1:
@@ -4501,7 +4508,17 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
browserslist@^4.25.0:
version "4.25.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
@@ -4603,7 +4620,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
version "1.0.30001714"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
@@ -5605,13 +5622,20 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0:
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decode-named-character-reference@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf"
@@ -5879,6 +5903,11 @@ electron-to-chromium@^1.5.160:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
electron-to-chromium@^1.5.73:
version "1.5.138"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -5923,10 +5952,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.17.2:
version "5.18.2"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
enhanced-resolve@^5.17.1:
version "5.18.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6132,15 +6161,15 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^10.1.8:
version "10.1.8"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
eslint-config-prettier@^10.1.5:
version "10.1.5"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-plugin-prettier@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
eslint-plugin-prettier@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"
@@ -6195,10 +6224,10 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.32.0:
version "9.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.32.0.tgz#4ea28df4a8dbc454e1251e0f3aed4bcf4ce50a47"
integrity sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==
eslint@^9.31.0:
version "9.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba"
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.12.1"
@@ -6206,8 +6235,8 @@ eslint@^9.32.0:
"@eslint/config-helpers" "^0.3.0"
"@eslint/core" "^0.15.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.32.0"
"@eslint/plugin-kit" "^0.3.4"
"@eslint/js" "9.31.0"
"@eslint/plugin-kit" "^0.3.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
@@ -6624,7 +6653,7 @@ form-data-encoder@^2.1.2:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.4:
form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
@@ -8034,10 +8063,10 @@ less-loader@^12.3.0:
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
less@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
less@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
dependencies:
copy-anything "^2.0.1"
parse-node-version "^1.0.1"
@@ -9040,6 +9069,11 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -10680,10 +10714,10 @@ rc-tabs@~15.6.1:
rc-resize-observer "^1.0.0"
rc-util "^5.34.1"
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
version "1.10.2"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
rc-textarea@~1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
@@ -12066,10 +12100,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -12348,15 +12382,15 @@ types-ramda@^0.30.0:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.39.0:
version "8.39.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.39.0.tgz#b19c1a925cf8566831ae3875d2881ee2349808a5"
integrity sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==
typescript-eslint@^8.37.0:
version "8.37.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.39.0"
"@typescript-eslint/parser" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
"@typescript-eslint/eslint-plugin" "8.37.0"
"@typescript-eslint/parser" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
typescript@~5.8.3:
version "5.8.3"
@@ -12491,7 +12525,7 @@ unraw@^3.0.0:
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.3:
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
@@ -12760,27 +12794,26 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.101.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
version "5.99.9"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
acorn "^8.14.0"
browserslist "^4.24.0"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.2"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -12794,7 +12827,7 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
tapable "^2.1.1"
terser-webpack-plugin "^5.3.11"
watchpack "^2.4.1"
webpack-sources "^3.3.3"
webpack-sources "^3.2.3"
webpackbar@^6.0.1:
version "6.0.1"

View File

@@ -15,7 +15,7 @@
# limitations under the License.
#
apiVersion: v2
appVersion: "5.0.0"
appVersion: "4.1.2"
description: Apache Superset is a modern, enterprise-ready business intelligence web application
name: superset
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.15.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.14.3
dependencies:
- name: postgresql
version: 13.4.4

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.15.0](https://img.shields.io/badge/Version-0.15.0-informational?style=flat-square)
![Version: 0.14.3](https://img.shields.io/badge/Version-0.14.3-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -336,6 +336,3 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
| tolerations | list | `[]` | |
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
## Versioning
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.

View File

@@ -48,6 +48,3 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
## Versioning
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.

View File

@@ -79,7 +79,6 @@ dependencies = [
"parsedatetime",
"paramiko>=3.4.0",
"pgsanity",
"Pillow>=11.0.0, <12",
"polyline>=2.0.0, <3.0",
"pyparsing>=3.0.6, <4",
"python-dateutil",
@@ -96,7 +95,7 @@ dependencies = [
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.3, <0.39",
"sqlglot>=27.3.0, <28",
"sqlglot>=26.1.3, <27",
# newer pandas needs 0.9+
"tabulate>=0.9.0, <1.0",
"typing-extensions>=4, <5",
@@ -112,7 +111,7 @@ athena = ["pyathena[pandas]>=2, <3"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
"sqlalchemy-bigquery>=1.15.0",
"sqlalchemy-bigquery>=1.6.1",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
@@ -182,7 +181,7 @@ tdengine = [
"taos-ws-py>=0.3.8"
]
teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0
thumbnails = ["Pillow>=10.0.1, <11"]
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.0.0"]
@@ -196,7 +195,6 @@ development = [
"grpcio>=1.55.3",
"openapi-spec-validator",
"parameterized",
"pip",
"pre-commit",
"progress>=1.5,<2",
"psutil",
@@ -313,16 +311,15 @@ select = [
"Q",
"S",
"T",
"TID",
"W",
]
ignore = [
"S101",
"PT006",
"T201",
"N999",
]
extend-select = ["I"]
# Allow fix for all enabled rules (when `--fix`) is provided.
@@ -332,16 +329,6 @@ unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.per-file-ignores]
"scripts/*" = ["TID251"]
"setup.py" = ["TID251"]
"superset/config.py" = ["TID251"]
"superset/cli/update.py" = ["TID251"]
"superset/key_value/types.py" = ["TID251"]
"superset/translations/utils.py" = ["TID251"]
"superset/extensions/__init__.py" = ["TID251"]
"superset/utils/json.py" = ["TID251"]
[tool.ruff.lint.isort]
case-sensitive = false
combine-as-imports = true
@@ -358,9 +345,6 @@ section-order = [
"local-folder"
]
[tool.ruff.lint.flake8-tidy-imports]
banned-api = { json = { msg = "Use superset.utils.json instead" }, simplejson = { msg = "Use superset.utils.json instead" } }
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
@@ -401,7 +385,6 @@ authorized_licenses = [
"isc license (iscl)",
"isc license",
"mit",
"mit-cmu",
"mozilla public license 2.0 (mpl 2.0)",
"osi approved",
"osi approved",

View File

@@ -266,8 +266,6 @@ parsedatetime==2.6
# via apache-superset (pyproject.toml)
pgsanity==0.2.9
# via apache-superset (pyproject.toml)
pillow==11.3.0
# via apache_superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
@@ -380,7 +378,7 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
sqlglot==27.3.0
sqlglot==26.28.1
# via apache-superset (pyproject.toml)
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)

View File

@@ -537,12 +537,10 @@ pgsanity==0.2.9
# via
# -c requirements/base.txt
# apache-superset
pillow==11.3.0
pillow==10.3.0
# via
# apache-superset
# matplotlib
pip==25.1.1
# via apache-superset
platformdirs==4.3.8
# via
# -c requirements/base.txt
@@ -797,14 +795,14 @@ sqlalchemy==1.4.54
# shillelagh
# sqlalchemy-bigquery
# sqlalchemy-utils
sqlalchemy-bigquery==1.15.0
sqlalchemy-bigquery==1.12.0
# via apache-superset
sqlalchemy-utils==0.38.3
# via
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
sqlglot==27.3.0
sqlglot==26.28.1
# via
# -c requirements/base.txt
# apache-superset

View File

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

View File

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

View File

@@ -403,7 +403,6 @@ module.exports = {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': ['error', true],
'i18n-strings/sentence-case-buttons': 'error',
camelcase: [
'error',
{

View File

@@ -19,7 +19,7 @@
import { LOGIN } from 'cypress/utils/urls';
function interceptLogin() {
cy.intercept('POST', '**/login/').as('login');
cy.intercept('POST', '/login/').as('login');
}
describe('Login view', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10227,11 +10227,14 @@
"peer": true
},
"node_modules/tmp": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": {
"node": ">=14.14"
"node": ">=8.17.0"
}
},
"node_modules/to-regex-range": {
@@ -18595,9 +18598,12 @@
"peer": true
},
"tmp": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ=="
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
"rimraf": "^3.0.0"
}
},
"to-regex-range": {
"version": "5.0.1",

View File

@@ -41,7 +41,7 @@ module.exports = {
context.report({
node,
message:
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it cant handle strings that include variables",
});
}
}
@@ -52,67 +52,5 @@ module.exports = {
};
},
},
'sentence-case-buttons': {
create(context) {
function isTitleCase(str) {
// Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words)
return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str);
}
function isButtonContext(node) {
const { parent } = node;
if (!parent) return false;
// Check for button-specific props
if (parent.type === 'Property') {
const key = parent.key.name;
return [
'primaryButtonName',
'secondaryButtonName',
'confirmButtonText',
'cancelButtonText',
].includes(key);
}
// Check for Button components
if (parent.type === 'JSXExpressionContainer') {
const jsx = parent.parent;
if (jsx?.type === 'JSXElement') {
const elementName = jsx.openingElement.name.name;
return elementName === 'Button';
}
}
return false;
}
function handler(node) {
if (node.arguments.length) {
const firstArg = node.arguments[0];
if (
firstArg.type === 'Literal' &&
typeof firstArg.value === 'string'
) {
const text = firstArg.value;
if (isButtonContext(node) && isTitleCase(text)) {
const sentenceCase = text
.toLowerCase()
.replace(/^\w/, c => c.toUpperCase());
context.report({
node: firstArg,
message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`,
});
}
}
}
}
return {
"CallExpression[callee.name='t']": handler,
"CallExpression[callee.name='tn']": handler,
};
},
},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
"type": "tsc --noEmit",
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
"validate-release": "../RELEASING/validate_this_release.sh"
},
"browserslist": [
@@ -88,7 +88,7 @@
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.12",
"@rjsf/validator-ajv8": "^5.24.9",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -121,13 +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",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.6.0",
"dom-to-image-more": "^3.2.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
"emotion-rgba": "0.0.12",
@@ -142,7 +144,8 @@
"geostyler-qgis-parser": "2.0.1",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^154.1.0",
"googleapis": "^130.0.0",
"html-webpack-plugin": "^5.6.3",
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
@@ -174,7 +177,7 @@
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1",
"react-lines-ellipsis": "^0.15.4",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.9",
"react-resize-detector": "^7.1.2",
@@ -206,7 +209,7 @@
"devDependencies": {
"@applitools/eyes-storybook": "^3.55.6",
"@babel/cli": "^7.27.2",
"@babel/compat-data": "^7.28.0",
"@babel/compat-data": "^7.26.8",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/node": "^7.22.6",
@@ -214,11 +217,11 @@
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/plugin-transform-runtime": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.2",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/runtime": "^7.26.0",
"@babel/runtime-corejs3": "^7.26.0",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -241,6 +244,7 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/classnames": "^2.2.10",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^29.5.14",
"@types/js-levenshtein": "^1.1.3",
@@ -282,7 +286,7 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^7.2.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
@@ -313,7 +317,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.6.2",
"prettier": "3.5.3",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -328,7 +332,7 @@
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.20.3",
"tsx": "^4.19.2",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",
@@ -346,7 +350,7 @@
"regenerator-runtime": "^0.14.1"
},
"engines": {
"node": "^20.18.1",
"node": "^20.16.0",
"npm": "^10.8.1"
},
"overrides": {

View File

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

View File

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

View File

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

View File

@@ -36,19 +36,18 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
const columns = ensureIsArray(
queryObject.series_columns || queryObject.columns,
);
const timeOffsets = ensureIsArray(formData.time_compare);
const { truncate_metric } = formData;
const xAxisLabel = getXAxisLabel(formData);
const isTimeComparisonValue = isTimeComparison(formData, queryObject);
// remove or rename top level of column name(metric name) in the MultiIndex when
// 1) at least 1 metric
// 2) dimension exist or multiple time shift metrics exist
// 2) dimension exist
// 3) xAxis exist
// 4) truncate_metric in form_data and truncate_metric is true
if (
metrics.length > 0 &&
(columns.length > 0 || timeOffsets.length > 1) &&
columns.length > 0 &&
xAxisLabel &&
truncate_metric !== undefined &&
!!truncate_metric
@@ -85,8 +84,7 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
ComparisonType.Percentage,
ComparisonType.Ratio,
].includes(formData.comparison_type) &&
metrics.length === 1 &&
renamePairs.length === 0
metrics.length === 1
) {
renamePairs.push([getMetricLabel(metrics[0]), null]);
}

View File

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

View File

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

View File

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

View File

@@ -29,4 +29,3 @@ export * from './getStandardizedControls';
export * from './getTemporalColumns';
export * from './displayTimeRelatedControls';
export * from './colorControls';
export * from './metricColumnFilter';

View File

@@ -1,135 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormMetric, SqlaFormData } from '@superset-ui/core';
import {
shouldSkipMetricColumn,
isRegularMetric,
isPercentMetric,
} from './metricColumnFilter';
const createMetric = (label: string): QueryFormMetric =>
({
label,
expressionType: 'SIMPLE',
column: { column_name: label },
aggregate: 'SUM',
}) as QueryFormMetric;
describe('metricColumnFilter', () => {
const createFormData = (
metrics: string[],
percentMetrics: string[],
): SqlaFormData =>
({
datasource: 'test_datasource',
viz_type: 'table',
metrics: metrics.map(createMetric),
percent_metrics: percentMetrics.map(createMetric),
}) as SqlaFormData;
describe('shouldSkipMetricColumn', () => {
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData([], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(true);
});
it('should not skip if column is also a regular metric', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData(['metric1'], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
it('should not skip if column starts with %', () => {
const colnames = ['%metric1'];
const formData = createFormData(['metric1'], []);
const result = shouldSkipMetricColumn({
colname: '%metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
it('should not skip if no prefixed version exists', () => {
const colnames = ['metric1'];
const formData = createFormData([], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
});
describe('isRegularMetric', () => {
it('should return true for regular metrics', () => {
const formData = createFormData(['metric1', 'metric2'], []);
expect(isRegularMetric('metric1', formData)).toBe(true);
expect(isRegularMetric('metric2', formData)).toBe(true);
});
it('should return false for non-metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isRegularMetric('non_metric', formData)).toBe(false);
});
it('should return false for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
});
});
describe('isPercentMetric', () => {
it('should return true for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
});
it('should return false for non-percentage metrics', () => {
const formData = createFormData(['regular_metric'], []);
expect(isPercentMetric('regular_metric', formData)).toBe(false);
});
it('should return false for regular metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isPercentMetric('metric1', formData)).toBe(false);
});
});
});

View File

@@ -1,95 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
QueryFormMetric,
getMetricLabel,
SqlaFormData,
} from '@superset-ui/core';
export interface MetricColumnFilterParams {
colname: string;
colnames: string[];
formData: SqlaFormData;
}
/**
* Determines if a column should be skipped based on metric filtering logic.
*
* This function implements the logic to skip unprefixed percent metric columns
* if a prefixed version exists, but doesn't skip if it's also a regular metric.
*
* @param params - The parameters for metric column filtering
* @returns true if the column should be skipped, false otherwise
*/
export function shouldSkipMetricColumn({
colname,
colnames,
formData,
}: MetricColumnFilterParams): boolean {
if (!colname) {
return false;
}
// Check if this column name exists as a percent metric in form data
const isPercentMetric = formData.percent_metrics?.some(
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
);
// Check if this column name exists as a regular metric in form data
const isRegularMetric = formData.metrics?.some(
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
);
// Check if there's a prefixed version of this column in the column list
const hasPrefixedVersion = colnames.includes(`%${colname}`);
// Skip if: has prefixed version AND is percent metric AND is NOT regular metric
return hasPrefixedVersion && isPercentMetric && !isRegularMetric;
}
/**
* Determines if a column is a regular metric.
*
* @param colname - The column name to check
* @param formData - The form data containing metrics
* @returns true if the column is a regular metric, false otherwise
*/
export function isRegularMetric(
colname: string,
formData: SqlaFormData,
): boolean {
return !!formData.metrics?.some(metric => getMetricLabel(metric) === colname);
}
/**
* Determines if a column is a percentage metric.
*
* @param colname: string,
* @param formData - The form data containing percent_metrics
* @returns true if the column is a percentage metric, false otherwise
*/
export function isPercentMetric(
colname: string,
formData: SqlaFormData,
): boolean {
return !!formData.percent_metrics?.some(
(metric: QueryFormMetric) => `%${getMetricLabel(metric)}` === colname,
);
}

View File

@@ -65,20 +65,6 @@ test('should skip renameOperator if series does not exist', () => {
).toEqual(undefined);
});
test('should skip renameOperator if series does not exist and a single time shift exists', () => {
expect(
renameOperator(
{ ...formData, ...{ time_compare: ['1 year ago'] } },
{
...queryObject,
...{
columns: [],
},
},
),
).toEqual(undefined);
});
test('should skip renameOperator if does not exist x_axis and is_timeseries', () => {
expect(
renameOperator(
@@ -107,26 +93,6 @@ test('should add renameOperator', () => {
});
});
test('should add renameOperator if a metric exists and multiple time shift', () => {
expect(
renameOperator(
{
...formData,
...{ time_compare: ['1 year ago', '2 years ago'] },
},
{
...queryObject,
...{
columns: [],
},
},
),
).toEqual({
operation: 'rename',
options: { columns: { 'count(*)': null }, inplace: true, level: 0 },
});
});
test('should add renameOperator if exists derived metrics', () => {
[
ComparisonType.Difference,
@@ -210,6 +176,7 @@ test('should add renameOperator if exist "actual value" time comparison', () =>
operation: 'rename',
options: {
columns: {
'count(*)': null,
'count(*)__1 year ago': '1 year ago',
'count(*)__1 year later': '1 year later',
},

View File

@@ -25,13 +25,11 @@
],
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.28.2",
"@babel/runtime": "^7.25.6",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@types/json-bigint": "^1.0.4",
"ace-builds": "^1.43.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"csstype": "^3.1.3",
@@ -39,7 +37,7 @@
"d3-format": "^1.3.2",
"dayjs": "^1.11.13",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-scale": "^3.0.0",
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dompurify": "^3.2.4",
@@ -48,10 +46,10 @@
"lodash": "^4.17.21",
"math-expression-evaluator": "^2.0.6",
"pretty-ms": "^9.2.0",
"re-resizable": "^6.11.2",
"re-resizable": "^6.10.1",
"react-ace": "^10.1.0",
"react-js-cron": "^5.2.0",
"react-draggable": "^4.5.0",
"react-draggable": "^4.4.6",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^15.4.5",
"react-ultimate-pagination": "^1.3.2",
@@ -61,7 +59,7 @@
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"reselect": "^5.1.1",
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
"@visx/responsive": "^3.12.0",
@@ -80,7 +78,7 @@
"@types/lodash": "^4.17.20",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.10.3",
"@types/prop-types": "^15.7.15",
"@types/prop-types": "^15.7.2",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^11.1.4",

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ export const Badge = styled((props: BadgeProps) => <AntdBadge {...props} />)`
${({ theme, color, count }) => `
& > sup,
& > sup.ant-badge-count {
box-shadow: none;
${
count !== undefined ? `background: ${color || theme.colorPrimary};` : ''
}

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { Children, ReactElement, Fragment } from 'react';
import cx from 'classnames';
import { Button as AntdButton } from 'antd';
import { useTheme } from '@superset-ui/core';
@@ -87,7 +88,6 @@ export function Button(props: ButtonProps) {
const element = children as ReactElement;
let renderedChildren = [];
if (element && element.type === Fragment) {
renderedChildren = Children.toArray(element.props.children);
} else {
@@ -118,7 +118,7 @@ export function Button(props: ButtonProps) {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1,
lineHeight: 1.5715,
fontSize: fontSizeSM,
fontWeight: fontWeightStrong,
height,
@@ -132,12 +132,11 @@ export function Button(props: ButtonProps) {
'& > span > :first-of-type': {
marginRight: firstChildMargin,
},
':not(:hover)': effectiveButtonStyle === 'secondary' &&
!disabled && {
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
color: `${theme.colorPrimaryTextHover} !important`,
},
':not(:hover)': effectiveButtonStyle === 'secondary' && {
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
color: `${theme.colorPrimaryTextHover} !important`,
},
}}
icon={icon}
{...restProps}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { DrawerProps } from './types';
export { Drawer } from 'antd';
export type { DrawerProps };

View File

@@ -1,22 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { DrawerProps } from 'antd/es/drawer';
export { DrawerProps };

View File

@@ -1,395 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
cloneElement,
forwardRef,
RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useState,
useRef,
useCallback,
} from 'react';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
import { DropdownContainerProps, DropdownItem, DropdownRef } from './types';
const MAX_HEIGHT = 500;
export const DropdownContainer = forwardRef(
(
{
items,
onOverflowingStateChange,
dropdownContent,
dropdownRef,
dropdownStyle = {},
dropdownTriggerCount,
dropdownTriggerIcon,
dropdownTriggerText = t('More'),
dropdownTriggerTooltip = null,
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
let targetRef = useRef<HTMLDivElement>(null);
if (dropdownRef) {
targetRef = dropdownRef;
}
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
items.push({
id: item.id,
element: cloneElement(item.element, { key: item.id }),
});
ids.push(item.id);
return [items, ids];
},
[[], []] as [DropdownItem[], string[]],
);
const [notOverflowedItems, notOverflowedIds] = useMemo(
() =>
reduceItems(
items.slice(
0,
overflowingIndex !== -1 ? overflowingIndex : items.length,
),
),
[items, overflowingIndex],
);
const [overflowedItems, overflowedIds] = useMemo(
() =>
overflowingIndex !== -1
? reduceItems(items.slice(overflowingIndex))
: [[], []],
[items, overflowingIndex],
);
useEffect(() => {
const container = current?.children.item(0);
if (!container) return;
const childrenArray = Array.from(container.children);
const resizeObserver = new ResizeObserver(() => {
recalculateItemWidths();
});
childrenArray.map(child => resizeObserver.observe(child));
// eslint-disable-next-line consistent-return
return () => {
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length, current, recalculateItemWidths]);
useLayoutEffect(() => {
if (popoverVisible) {
return;
}
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
// If items length change, add all items to the container
// and recalculate the widths
if (itemsWidth.length !== items.length) {
if (childrenArray.length === items.length) {
setItemsWidth(
childrenArray.map(child => child.getBoundingClientRect().width),
);
} else {
setOverflowingIndex(-1);
return;
}
}
// Calculates the index of the first overflowed element
// +1 is to give at least one pixel of difference and avoid flakiness
const index = childrenArray.findIndex(
child =>
child.getBoundingClientRect().right >
container.getBoundingClientRect().right + 1,
);
// If elements fit (-1) and there's overflowed items
// then preserve the overflow index. We can't use overflowIndex
// directly because the items may have been modified
let newOverflowingIndex =
index === -1 && overflowedItems.length > 0
? items.length - overflowedItems.length
: index;
if (width > previousWidth) {
// Calculates remaining space in the container
const button = current?.children.item(1);
const buttonRight = button?.getBoundingClientRect().right || 0;
const containerRight = current?.getBoundingClientRect().right || 0;
const remainingSpace = containerRight - buttonRight;
// Checks if some elements in the dropdown fits in the remaining space
let sum = 0;
for (let i = childrenArray.length; i < items.length; i += 1) {
sum += itemsWidth[i];
if (sum <= remainingSpace) {
newOverflowingIndex = i + 1;
} else {
break;
}
}
}
setOverflowingIndex(newOverflowingIndex);
}
}, [
current,
items.length,
itemsWidth,
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
if (onOverflowingStateChange) {
onOverflowingStateChange({
notOverflowed: notOverflowedIds,
overflowed: overflowedIds,
});
}
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
const popoverContent = useMemo(
() =>
dropdownContent || overflowingCount ? (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.sizeUnit * 4}px;
`}
data-test="dropdown-content"
style={dropdownStyle}
ref={targetRef}
>
{dropdownContent
? dropdownContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
) : null,
[
dropdownContent,
overflowingCount,
theme.sizeUnit,
dropdownStyle,
overflowedItems,
],
);
useLayoutEffect(() => {
if (popoverVisible) {
// Measures scroll height after rendering the elements
setTimeout(() => {
if (targetRef.current) {
// We only set overflow when there's enough space to display
// Select's popovers because they are restrained by the overflow property.
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
}
}, 100);
}
}, [popoverVisible]);
useImperativeHandle(
outerRef,
() => ({
...(ref.current as HTMLDivElement),
open: () => setPopoverVisible(true),
}),
[ref],
);
// Closes the popover when scrolling on the document
useEffect(() => {
document.onscroll = popoverVisible
? () => setPopoverVisible(false)
: null;
return () => {
document.onscroll = null;
};
}, [popoverVisible]);
return (
<div
ref={ref}
css={css`
display: flex;
align-items: center;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.sizeUnit * 4}px;
margin-right: ${theme.sizeUnit * 4}px;
min-width: 0px;
`}
data-test="container"
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{popoverContent && (
<>
<Global
styles={css`
.ant-popover-inner {
// Some OS versions only show the scroll when hovering.
// These settings will make the scroll always visible.
::-webkit-scrollbar {
-webkit-appearance: none;
width: 14px;
}
::-webkit-scrollbar-thumb {
border-radius: 9px;
background-color: ${theme.colors.grayscale.light1};
border: 3px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-track {
background-color: ${theme.colors.grayscale.light4};
border-left: 1px solid ${theme.colors.grayscale.light2};
}
}
`}
/>
<Popover
styles={{
body: {
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
},
}}
content={popoverContent}
trigger="click"
open={popoverVisible}
onOpenChange={visible => setPopoverVisible(visible)}
placement="bottom"
forceRender={forceRender}
>
<Tooltip title={dropdownTriggerTooltip}>
<Button
buttonStyle="secondary"
data-test="dropdown-container-btn"
icon={dropdownTriggerIcon}
css={css`
padding-left: ${theme.paddingXS}px;
padding-right: ${theme.paddingXXS}px;
gap: ${theme.sizeXXS}px;
`}
>
{dropdownTriggerText}
<Badge
count={dropdownTriggerCount ?? overflowingCount}
color={
(dropdownTriggerCount ?? overflowingCount) > 0
? theme.colorPrimary
: theme.colors.grayscale.light1
}
showZero
css={css`
margin-left: ${theme.sizeUnit * 2}px;
`}
/>
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.light1}
css={css`
.anticon {
display: flex;
}
`}
/>
</Button>
</Tooltip>
</Popover>
</>
)}
</div>
);
},
);

View File

@@ -16,6 +16,448 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
CSSProperties,
cloneElement,
forwardRef,
ReactElement,
RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useState,
useRef,
ReactNode,
useCallback,
} from 'react';
export { DropdownContainer } from './DropdownContainer';
export type * from './types';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
/**
* Container item.
*/
export interface DropdownItem {
/**
* String that uniquely identifies the item.
*/
id: string;
/**
* The element to be rendered.
*/
element: ReactElement;
}
/**
* Horizontal container that displays overflowed items in a dropdown.
* It shows an indicator of how many items are currently overflowing.
*/
export interface DropdownContainerProps {
/**
* Array of items. The id property is used to uniquely identify
* the elements when rendering or dealing with event handlers.
*/
items: DropdownItem[];
/**
* Event handler called every time an element moves between
* main container and dropdown.
*/
onOverflowingStateChange?: (overflowingState: {
notOverflowed: string[];
overflowed: string[];
}) => void;
/**
* Option to customize the content of the dropdown.
*/
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
/**
* Dropdown ref.
*/
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Dropdown additional style properties.
*/
dropdownStyle?: CSSProperties;
/**
* Displayed count in the dropdown trigger.
*/
dropdownTriggerCount?: number;
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactElement;
/**
* Text of the dropdown trigger.
*/
dropdownTriggerText?: string;
/**
* Text of the dropdown trigger tooltip
*/
dropdownTriggerTooltip?: ReactNode | null;
/**
* Main container additional style properties.
*/
style?: CSSProperties;
/**
* Force render popover content before it's first opened
*/
forceRender?: boolean;
}
export type DropdownRef = HTMLDivElement & { open: () => void };
const MAX_HEIGHT = 500;
export const DropdownContainer = forwardRef(
(
{
items,
onOverflowingStateChange,
dropdownContent,
dropdownRef,
dropdownStyle = {},
dropdownTriggerCount,
dropdownTriggerIcon,
dropdownTriggerText = t('More'),
dropdownTriggerTooltip = null,
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
let targetRef = useRef<HTMLDivElement>(null);
if (dropdownRef) {
targetRef = dropdownRef;
}
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
items.push({
id: item.id,
element: cloneElement(item.element, { key: item.id }),
});
ids.push(item.id);
return [items, ids];
},
[[], []] as [DropdownItem[], string[]],
);
const [notOverflowedItems, notOverflowedIds] = useMemo(
() =>
reduceItems(
items.slice(
0,
overflowingIndex !== -1 ? overflowingIndex : items.length,
),
),
[items, overflowingIndex],
);
const [overflowedItems, overflowedIds] = useMemo(
() =>
overflowingIndex !== -1
? reduceItems(items.slice(overflowingIndex))
: [[], []],
[items, overflowingIndex],
);
useEffect(() => {
const container = current?.children.item(0);
if (!container) return;
const childrenArray = Array.from(container.children);
const resizeObserver = new ResizeObserver(() => {
recalculateItemWidths();
});
childrenArray.map(child => resizeObserver.observe(child));
// eslint-disable-next-line consistent-return
return () => {
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length, current, recalculateItemWidths]);
useLayoutEffect(() => {
if (popoverVisible) {
return;
}
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
// If items length change, add all items to the container
// and recalculate the widths
if (itemsWidth.length !== items.length) {
if (childrenArray.length === items.length) {
setItemsWidth(
childrenArray.map(child => child.getBoundingClientRect().width),
);
} else {
setOverflowingIndex(-1);
return;
}
}
// Calculates the index of the first overflowed element
// +1 is to give at least one pixel of difference and avoid flakiness
const index = childrenArray.findIndex(
child =>
child.getBoundingClientRect().right >
container.getBoundingClientRect().right + 1,
);
// If elements fit (-1) and there's overflowed items
// then preserve the overflow index. We can't use overflowIndex
// directly because the items may have been modified
let newOverflowingIndex =
index === -1 && overflowedItems.length > 0
? items.length - overflowedItems.length
: index;
if (width > previousWidth) {
// Calculates remaining space in the container
const button = current?.children.item(1);
const buttonRight = button?.getBoundingClientRect().right || 0;
const containerRight = current?.getBoundingClientRect().right || 0;
const remainingSpace = containerRight - buttonRight;
// Checks if some elements in the dropdown fits in the remaining space
let sum = 0;
for (let i = childrenArray.length; i < items.length; i += 1) {
sum += itemsWidth[i];
if (sum <= remainingSpace) {
newOverflowingIndex = i + 1;
} else {
break;
}
}
}
setOverflowingIndex(newOverflowingIndex);
}
}, [
current,
items.length,
itemsWidth,
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
if (onOverflowingStateChange) {
onOverflowingStateChange({
notOverflowed: notOverflowedIds,
overflowed: overflowedIds,
});
}
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
const popoverContent = useMemo(
() =>
dropdownContent || overflowingCount ? (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.sizeUnit * 4}px;
`}
data-test="dropdown-content"
style={dropdownStyle}
ref={targetRef}
>
{dropdownContent
? dropdownContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
) : null,
[
dropdownContent,
overflowingCount,
theme.sizeUnit,
dropdownStyle,
overflowedItems,
],
);
useLayoutEffect(() => {
if (popoverVisible) {
// Measures scroll height after rendering the elements
setTimeout(() => {
if (targetRef.current) {
// We only set overflow when there's enough space to display
// Select's popovers because they are restrained by the overflow property.
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
}
}, 100);
}
}, [popoverVisible]);
useImperativeHandle(
outerRef,
() => ({
...(ref.current as HTMLDivElement),
open: () => setPopoverVisible(true),
}),
[ref],
);
// Closes the popover when scrolling on the document
useEffect(() => {
document.onscroll = popoverVisible
? () => setPopoverVisible(false)
: null;
return () => {
document.onscroll = null;
};
}, [popoverVisible]);
return (
<div
ref={ref}
css={css`
display: flex;
align-items: center;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.sizeUnit * 4}px;
margin-right: ${theme.sizeUnit * 4}px;
min-width: 0px;
`}
data-test="container"
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{popoverContent && (
<>
<Global
styles={css`
.ant-popover-inner {
// Some OS versions only show the scroll when hovering.
// These settings will make the scroll always visible.
::-webkit-scrollbar {
-webkit-appearance: none;
width: 14px;
}
::-webkit-scrollbar-thumb {
border-radius: 9px;
background-color: ${theme.colors.grayscale.light1};
border: 3px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-track {
background-color: ${theme.colors.grayscale.light4};
border-left: 1px solid ${theme.colors.grayscale.light2};
}
}
`}
/>
<Popover
styles={{
body: {
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
},
}}
content={popoverContent}
trigger="click"
open={popoverVisible}
onOpenChange={visible => setPopoverVisible(visible)}
placement="bottom"
forceRender={forceRender}
>
<Tooltip title={dropdownTriggerTooltip}>
<Button
buttonStyle="secondary"
data-test="dropdown-container-btn"
>
{dropdownTriggerIcon}
{dropdownTriggerText}
<Badge
count={dropdownTriggerCount ?? overflowingCount}
color={
(dropdownTriggerCount ?? overflowingCount) > 0
? theme.colorPrimary
: theme.colors.grayscale.light1
}
showZero
css={css`
margin-left: ${theme.sizeUnit * 2}px;
`}
/>
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.light1}
css={css`
.anticon {
display: flex;
}
`}
/>
</Button>
</Tooltip>
</Popover>
</>
)}
</div>
);
},
);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: IconType;
dropdownTriggerIcon?: ReactElement;
/**
* Text of the dropdown trigger.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,16 +45,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
const labelType = datasetType === 'physical' ? 'primary' : 'default';
return (
<Label
icon={icon}
type={labelType}
style={{
color:
datasetType === 'physical'
? theme.colorPrimaryText
: theme.colorPrimary,
}}
>
<Label icon={icon} type={labelType}>
{label}
</Label>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1047,119 +1047,6 @@ test('typing and deleting the last character for a new option displays correctly
jest.useRealTimers();
});
describe('grouped options search', () => {
const GROUPED_OPTIONS = [
{
label: 'Male',
options: OPTIONS.filter(option => option.gender === 'Male'),
},
{
label: 'Female',
options: OPTIONS.filter(option => option.gender === 'Female'),
},
];
it('searches within grouped options and shows matching groups', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('John');
expect(await findSelectOption('John')).toBeInTheDocument();
expect(await findSelectOption('Johnny')).toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
expect(screen.queryByText('Olivia')).not.toBeInTheDocument();
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
});
it('shows multiple groups when search matches both', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('er');
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(await findSelectOption('Oliver')).toBeInTheDocument();
expect(await findSelectOption('Cher')).toBeInTheDocument();
expect(await findSelectOption('Her')).toBeInTheDocument();
});
it('handles case-insensitive search in grouped options', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('EMMA');
expect(await findSelectOption('Emma')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(screen.queryByText('Male')).not.toBeInTheDocument();
});
it('shows no options when search matches nothing in any group', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('xyz123');
expect(screen.queryByText('Male')).not.toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
expect(
screen.getByText(NO_DATA, { selector: '.ant-empty-description' }),
).toBeInTheDocument();
});
it('works in multiple selection mode with grouped options', async () => {
render(
<Select {...defaultProps} options={GROUPED_OPTIONS} mode="multiple" />,
);
await open();
await type('John');
await userEvent.click(await findSelectOption('John'));
// Clear search and search for female name
await clearTypedText();
await type('Emma');
await userEvent.click(await findSelectOption('Emma'));
// Both should be selected
const values = await findAllSelectValues();
expect(values).toHaveLength(2);
expect(values[0]).toHaveTextContent('John');
expect(values[1]).toHaveTextContent('Emma');
});
it('preserves group structure when not searching', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(await findSelectOption('John')).toBeInTheDocument();
expect(await findSelectOption('Emma')).toBeInTheDocument();
});
it('handles empty groups gracefully', async () => {
const optionsWithEmptyGroup = [
...GROUPED_OPTIONS,
{
label: 'Empty Group',
options: [],
},
];
render(<Select {...defaultProps} options={optionsWithEmptyGroup} />);
await open();
await type('John');
expect(await findSelectOption('John')).toBeInTheDocument();
expect(screen.queryByText('Empty Group')).not.toBeInTheDocument();
});
});
/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available

View File

@@ -373,27 +373,9 @@ const Select = forwardRef(
setSelectOptions(updatedOptions);
}
const filteredOptions = updatedOptions
.map((option: any) => {
/*
If it's a group, filter its nested options and only return it
if it has matching options
*/
if ('options' in option && Array.isArray(option.options)) {
const filteredGroupOptions = option.options.filter(
(subOption: AntdLabeledValue) =>
handleFilterOption(search, subOption),
);
return filteredGroupOptions.length > 0
? { ...option, options: filteredGroupOptions }
: null;
}
return handleFilterOption(search, option as AntdLabeledValue)
? option
: null;
})
.filter((option): option is AntdLabeledValue => option !== null);
const filteredOptions = updatedOptions.filter(
(option: AntdLabeledValue) => handleFilterOption(search, option),
);
setVisibleOptions(filteredOptions);
setInputValue(searchValue);

View File

@@ -32,9 +32,8 @@ export const StyledHeader = styled.span<{ headerPosition: string }>`
`;
export const StyledContainer = styled.div<{ headerPosition: string }>`
${({ headerPosition, theme }) => `
${({ headerPosition }) => `
display: flex;
gap: ${theme.sizeUnit}px;
flex-direction: ${headerPosition === 'top' ? 'column' : 'row'};
align-items: ${headerPosition === 'left' ? 'center' : undefined};
width: 100%;

View File

@@ -132,7 +132,7 @@ const VirtualTable = <RecordType extends object>(
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft;
}
return 0;
return null;
},
set: (scrollLeft: number) => {
if (gridRef.current) {

View File

@@ -52,9 +52,7 @@ interface TableCollectionProps<T extends object> {
const StyledTable = styled(Table)`
${({ theme }) => `
th.ant-column-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: fit-content;
}
.actions {
opacity: 0;
@@ -85,6 +83,7 @@ const StyledTable = styled(Table)`
font-feature-settings: 'tnum' 1;
text-overflow: ellipsis;
overflow: hidden;
max-width: 320px;
line-height: 1;
vertical-align: middle;
padding-left: ${theme.sizeUnit * 4}px;
@@ -150,7 +149,7 @@ function TableCollection<T extends object>({
size={size}
data-test="listview-table"
pagination={false}
tableLayout="fixed"
tableLayout="auto"
rowKey="rowId"
rowSelection={rowSelection}
locale={{ emptyText: null }}

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