Compare commits

..

58 Commits

Author SHA1 Message Date
Maxime Beauchemin
6d7467bafd feat: add devcontainer profiles for MCP development
- Split devcontainer config into base + profile structure
  - default: Standard Superset development (port 9001)
  - with-mcp: Superset + MCP service (ports 9001, 5008)
- Add MCP service to docker-compose-light.yml as optional profile
- Update start-superset.sh to conditionally start MCP when ENABLE_MCP=true
- Add MCP case to docker-bootstrap.sh for starting MCP service
- Preserve original devcontainer.json as .old for migration reference

This allows developers to choose their development environment:
- Use default profile for standard Superset development
- Use with-mcp profile for MCP/LLM agent development
- MCP service runs on port 5008 when enabled via profile

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-30 18:59:27 -07:00
Amin Ghadersohi
71f294b7d3 docs: update MCP Chart Test Plan with accurate implementation details
- Add authentication setup and dataset discovery prerequisites
- Remove references to unsupported preview formats (base64, interactive, vega_lite)
- Fix schema examples: operator -> op, xlsx -> excel
- Add performance limits for preview formats
- Update error expectations for empty query results
- Enhance cache testing with cache_timeout parameter
- Add Important Schema Notes section for common gotchas
- Update response patterns to match actual implementation
2025-07-30 20:15:41 -04:00
Amin Ghadersohi
e839d0989a refactor: remove redundant docs folder from mcp_service 2025-07-30 19:41:05 -04:00
Amin Ghadersohi
714e21b3ec feat: enhance MCP documentation with Superset best practices
- Add Docusaurus admonitions (:::note, :::tip, :::warning) following Superset patterns
- Improve code block formatting and language tags
- Add important notes about authentication and installation
- Ensure consistency with Superset documentation style
- Maintain accuracy with FastMCP specifications
2025-07-30 19:26:08 -04:00
Amin Ghadersohi
bf78eb69ed fix: resolve Mermaid diagram syntax errors in development guide
Replace @ symbols in node names that cause Mermaid parsing errors
2025-07-30 19:21:45 -04:00
Amin Ghadersohi
a13c1ba8c2 fix: update broken README.md links in MCP documentation
Replace ./README.md references with proper Docusaurus links to intro page
2025-07-30 19:20:38 -04:00
Amin Ghadersohi
422c34a6ee feat: integrate MCP service documentation into Superset Docusaurus site
- Add comprehensive MCP service documentation to docs/docs/mcp-service/
- Create 7 new documentation files covering all aspects of MCP service
- Add MCP Service section to Docusaurus sidebar configuration
- Update README files to reference official documentation
- Remove outdated standalone architecture documentation
- Install missing docs dependencies to fix eslint-docs pre-commit hook

Documentation includes:
- Introduction and overview with getting started guide
- Complete API reference with all 16 tools and examples
- Authentication setup with JWT and identity provider integration
- Development guide with internal architecture and patterns
- System architecture overview with deployment considerations
- Preset integration guide for enterprise features
2025-07-30 19:17:18 -04:00
Amin Ghadersohi
cd213fc57d update: enhance MCP service README with comprehensive setup instructions
Improve README with official Superset installation instructions, detailed Claude Desktop
connection steps using FastMCP proxy, and troubleshooting guide. Adds verification steps
and clear instructions for HTTP-based MCP service deployment.
2025-07-30 18:24:54 -04:00
Amin Ghadersohi
0f20a88598 feat(mcp): major enhancements to chart generation with validation, pooled screenshots, and simplified tests
This commit introduces significant improvements to the MCP chart generation system:

## Core Features
- Enhanced chart generation with comprehensive validation and error handling
- Implemented WebDriver pooling for 10x faster screenshot generation
- Added detailed validation for chart configurations with helpful error messages
- Support for both table and XY (line/bar/area/scatter) chart types

## Validation & Error Handling
- Column existence validation with similarity-based suggestions
- Data type compatibility checks for aggregations (SUM/AVG for numbers only)
- Filter operator validation
- Detailed error responses with actionable suggestions and available columns/metrics

## Performance Improvements
- WebDriver connection pooling eliminates browser startup overhead
- Reuses browser instances across multiple screenshot requests
- Automatic health checking and recovery of WebDriver instances
- Configurable pool size and timeout settings

## Screenshot Enhancements
- New PooledChartScreenshot and PooledExploreScreenshot classes
- FastAPI endpoints for serving chart and explore screenshots as PNG images
- Proper caching with TTL support
- Clean chart-only screenshots for explore views (hides UI elements)

## Test Improvements
- Simplified test structure focusing on schema validation over complex mocking
- Removed brittle integration-style tests in favor of unit tests
- Fixed all test failures with cleaner, more maintainable approach
- Added comprehensive test plan for manual testing with Claude Desktop

## API Enhancements
- Centralized URL generation for screenshots and explore links
- Consistent error response format across all endpoints
- Support for UUID-based chart lookups alongside numeric IDs

## Code Quality
- Fixed all pre-commit issues (mypy, ruff, long lines, complexity)
- Extracted helper functions to reduce cyclomatic complexity
- Proper type annotations throughout
- Better separation of concerns

## Documentation
- Added comprehensive test plan for chart tools
- Detailed guides for table chart configuration
- WebDriver pooling architecture documentation
- Demo scripts for testing functionality

The changes maintain backward compatibility while significantly improving
performance, reliability, and developer experience for chart generation in the
MCP service.
2025-07-30 18:09:23 -04:00
Amin Ghadersohi
33d16eaca1 refactor: rename pydantic_schemas to schemas for MCP service
Simplified the directory structure by renaming `pydantic_schemas` to `schemas` throughout the MCP service codebase. This follows standard Python conventions and makes imports more concise.

Changes:
- Renamed directory: superset/mcp_service/pydantic_schemas/ → superset/mcp_service/schemas/
- Updated all import statements across 42 files
- Updated documentation references in README files
- All tests pass, pre-commit hooks clean
2025-07-30 14:20:42 -04:00
Amin Ghadersohi
5b56bc622b feat: major MCP service enhancements with cache control, testing infrastructure, and bug fixes
This comprehensive update adds significant functionality and improvements to the MCP service:

  New Features:
  - Cache control utilities for metadata operations with configurable TTL
  - Entity testing plan documentation for systematic API validation
  - Chart data retrieval tool with caching support
  - Comprehensive request schema validation framework
  - Sortable columns testing infrastructure

  Documentation Updates:
  - Expanded README with architecture details and usage examples
  - Phase 1 status tracking and implementation notes
  - Enhanced schema documentation with cache control patterns
  - LLMS.md reference added to CLAUDE.md

  Search Functionality Fixes:
  - Dashboard search: removed invalid columns (owners, roles, tags, published)
  - Dataset search: removed non-existent 'tags' and problematic 'catalog' columns
  - Chart search: maintained correct text-only columns (slice_name, description)
  - Updated corresponding unit tests to match implementations

  Infrastructure Improvements:
  - Added comprehensive cache control test coverage
  - Request schema validation testing
  - Sortable columns validation across all entity types
  - Enhanced error handling and logging

  Technical Debt:
  - Standardized import patterns across MCP tools
  - Improved type safety in Pydantic schemas
  - Better separation of concerns for cache operations
2025-07-30 14:20:42 -04:00
Amin Ghadersohi
8cbfe027b5 update: add chart update tools and centralized URL configuration
- Add update_chart and update_chart_preview tools for modifying saved and cached charts
  - Implement centralized URL configuration using SUPERSET_WEBSERVER_ADDRESS
  - Add comprehensive MCP audit logging with impersonation tracking and payload  sanitization
  - Refactor duplicate chart analysis functions to chart_utils.py
  - Update all README documentation with new tools and features
  - Add 194+ unit tests covering URL utils, audit logging, and chart updates
  - Fix all pre-commit issues and ensure test coverage
2025-07-30 14:20:42 -04:00
Amin Ghadersohi
c3b3edc6ba refactor: rename create_chart to generate_chart and standardize MCP test naming
- Rename create_chart function to generate_chart throughout MCP service
  - Update CreateChartRequest to GenerateChartRequest schema
  - Rename test files to follow test_*.py convention consistently
  - Fix test class and method names (TestCreateChart → TestGenerateChart)
  - Update all documentation and README files with new naming
  - Standardize auth scope mappings and tool references
  - Maintain backward compatibility for Superset CreateChartCommand references
2025-07-30 14:20:42 -04:00
Amin Ghadersohi
aa06bb9fda revert unnecessary config change 2025-07-30 14:20:41 -04:00
Amin Ghadersohi
0f222b9034 update: revert comment change 2025-07-30 14:20:41 -04:00
Amin Ghadersohi
7044153ca4 revert: switch webdriver back to Firefox for chart
screenshots

  Changed WEBDRIVER_TYPE from "chrome" to "firefox" and removed
   Chrome-specific
  binary path from WEBDRIVER_CONFIGURATION. Firefox provides
  more reliable
  screenshot generation for the MCP service chart preview
  functionality.

  - Set WEBDRIVER_TYPE = "firefox"
  - Cleared binary_location in WEBDRIVER_CONFIGURATION to use
  default Firefox
  - Chart preview tools now use geckodriver instead of Chrome
  headless mode

  This resolves webdriver timeout issues in chart screenshot
  generation.
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
ce82c35bb6 docs: clarify MCP service capabilities and available operations
Updated all MCP service READMEs to accurately reflect available operations:

- Removed overuse of 'comprehensive' and 'complete' where it overstated capabilities
- Added clear 'Available Operations' section showing:
  - Read operations available for all entities
  - Create operations only for charts and dashboards
  - No update or delete operations available
- Updated tool descriptions to be more specific about capabilities
- Changed 'complete workflow automation' to 'dashboard creation and chart addition'
- Clarified that we have 5 chart types, not 'comprehensive' support
- Updated test count references to be consistent (185+ tests)

This ensures documentation accurately represents Phase 1 capabilities without
overstating what operations are available.
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
9b25bd973f docs: update MCP service documentation to reflect Phase 1 completion
Updated all MCP service README files to reflect current status:

- README_ARCHITECTURE.md: Updated to 95% completion status, documented new
  dashboard generation and SQL Lab tools, corrected file references
- README_PHASE1_STATUS.md: Marked all core epics as complete, updated tool
  count to 16, reflected recent technical completions
- README_SCHEMAS.md: Updated test coverage numbers, documented new schema
  enhancements, marked Phase 1 features as complete

Changes reflect the successful completion of core MCP service functionality
including BaseDAO enhancements, comprehensive testing coverage, and all
planned Phase 1 tools and features.
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
5f7502b85c feat: comprehensive MCP service enhancements with type safety and new tools
Core Improvements:
- Enhanced BaseDAO with type-safe UUID handling and flexible column support
- Added comprehensive test coverage (185+ passing unit tests)
- Eliminated hardcoded string checks with SQLAlchemy type inspection
- Removed unnecessary async patterns for better performance consistency

New MCP Tools & Features:
- Dashboard generation: create_dashboard and add_chart_to_existing_dashboard
- Chart data/preview: get_chart_data and get_chart_preview with multiple formats
- SQL Lab integration: open_sql_lab_with_context with proper parameter handling
- Enhanced chart creation with comprehensive type support

Technical Enhancements:
- Fixed SQL Lab parameter naming (dbid) for frontend compatibility
- Improved error handling with graceful UUID conversion fallbacks
- Enhanced multi-identifier support (ID, UUID, slug) across all tools
- Comprehensive request schema pattern eliminating LLM validation issues

Testing & Quality:
- Added BaseDAO unit tests with edge cases and error scenarios
- Enhanced MCP tool testing with proper mocking patterns
- Fixed trailing whitespace and pre-commit compliance
- Professional error handling and type validation

Documentation:
- Updated all MCP service READMEs with current status (95% complete)
- Documented all 16 MCP tools with examples and usage patterns
- Enhanced architecture documentation with recent improvements
- Updated Phase 1 status reflecting completion of core features

This commit consolidates Phase 1 MCP service development with production-ready
authentication, comprehensive testing, and complete tool functionality.
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
1d5372210f update: read files 2025-07-30 14:20:41 -04:00
Amin Ghadersohi
64af53f6f6 update: enhance MCP service auth with factory pattern and code cleanup
- Implement configurable auth factory pattern as suggested by @dpgaspar
  - Add unit test coverage
  - Refactor auth.py for better pythonic patterns and error handling
  - Add proper type annotations and logging throughout
  - Split complex functions into focused helper functions
  - Add detailed architecture documentation with Mermaid diagrams
  - Improve JWT token extraction with robust fallback logic
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
56b308340e update: use real Superset user for MCP authentication instead of MCPUser
Replace MCPUser wrapper with actual Flask-AppBuilder User from database
  to enable proper RBAC permission filtering. Fixes MCP service returning
  0 counts due to empty permission queries.
2025-07-30 14:20:41 -04:00
Amin Ghadersohi
3b2b7609a8 update: add optional JWT Bearer authentication to MCP service
Implement comprehensive JWT authentication system for production deployments:

  • Add BearerAuthProvider integration with FastMCP for RS256 token validation
  • Support both static public keys and JWKS endpoints for enterprise key rotation
  • Implement scope-based authorization (dashboard:read, chart:write, etc.)
  • Extract user identity from JWT sub claim for proper audit trails
  • Maintain backward compatibility with admin fallback when auth disabled
  • Add comprehensive test coverage for authentication flows
  • Update documentation with human-friendly security setup guide

  Authentication is disabled by default for development convenience.
  Configure via environment variables for production use.
2025-07-30 14:20:40 -04:00
Amin Ghadersohi
61a91f80fe update deps 2025-07-30 14:20:40 -04:00
Amin Ghadersohi
ed3c5ecbc2 enhance MCP service with multi-identifier support and comprehensive UUID/slug functionality
- Add UUID field to ChartInfo and DatasetInfo Pydantic schemas for complete serialization
  - Include UUID in chart and dataset serialization functions (serialize_chart_object,
  serialize_dataset_object)
  - UUID and slug are now included in default response columns for better discoverability:
    * Dashboards: UUID and slug in DEFAULT_DASHBOARD_COLUMNS and returned by default
    * Charts: UUID in DEFAULT_CHART_COLUMNS and returned by default
    * Datasets: UUID in DEFAULT_DATASET_COLUMNS and returned by default
  - Search functionality enhanced to include UUID/slug fields across all relevant tools
  - Add comprehensive test coverage for UUID/slug functionality:
    * Default column verification tests ensuring UUID/slug are in default responses
    * Response data verification tests confirming UUID/slug values are returned
    * Custom column selection tests for explicit UUID/slug requests
    * Metadata accuracy tests verifying columns_requested/columns_loaded tracking
  - Update documentation to reflect enhanced multi-identifier capabilities
  - All 132 tests pass with comprehensive verification of UUID/slug support
2025-07-30 14:20:40 -04:00
Amin Ghadersohi
c8fbd4233c add multi-identifier support and fix columns metadata accuracy
**Multi-Identifier Support:**
  - Enhance ModelGetInfoTool to support ID, UUID, and slug lookup
  - Add intelligent identifier detection for get_*_info tools
  - Dashboards: support ID, UUID, and slug via id_or_slug_filter
  - Datasets/Charts: support ID and UUID via direct database queries
  - Add GetDashboardInfoRequest, GetDatasetInfoRequest, GetChartInfoRequest schemas

  **Enhanced Default Columns & Metadata:**
  - Add uuid to default columns for all list tools (dashboards, datasets, charts)
  - Include uuid/slug in search columns for better discoverability
  - Fix columns_requested to accurately reflect user input vs defaults
  - Fix columns_loaded to show actual DAO columns requested vs serialized fields

  **Testing & Code Quality:**
  - Add multi-identifier tests for all get_*_info tools (ID/UUID/slug scenarios)
  - Remove unused serialized_columns variable in ModelListTool
  - Fix linting issues (line length, docstrings)

  This provides flexible identifier support while ensuring accurate metadata
  tracking for better LLM compatibility and debugging.
2025-07-30 14:20:40 -04:00
Amin Ghadersohi
fc85f68585 update: implement FastMCP complex inputs pattern for MCP tools
- Replace individual parameters with structured request schemas for list_datasets, list_charts, and
  list_dashboards
  - Fix validation issues where LLMs passed arrays/objects as strings
  - Add ListDatasetsRequest, ListChartsRequest, ListDashboardsRequest schemas
  - Fix object serialization (charts, dashboards, datasets)
  - Add validation to prevent search+filters conflicts
  - Update all tests and fix linting issues

  Resolves string/array validation ambiguity that caused tool failures when LLMs sent complex
  parameters incorrectly.
2025-07-30 14:20:40 -04:00
Amin Ghadersohi
e825bbe1f4 update: generate_explore_link and create_chart working well 2025-07-30 14:20:40 -04:00
Amin Ghadersohi
a80b637a2a update: add a save_chart boolean parameter to create chart so that generate link to explore can use its code and just pass false 2025-07-30 14:20:40 -04:00
Amin Ghadersohi
541b3bd727 update: add docs to proxy 2025-07-30 14:20:40 -04:00
Amin Ghadersohi
a909799e5c update: simplify create chart a lot: support area, line, bar, scatter and table charts.
example prompt:
- can you use superset dataset 2 to plot popular baby names in 2024

- plot airline delay by day of week and group by airline use dataset 6
2025-07-30 14:20:39 -04:00
Amin Ghadersohi
01329f1c62 update: tests and deps 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
67f621d360 update: remove create_chart_simple tool 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
b552dbf4a1 update: fix all lint issues 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
364af98c04 update: create_chart: Remove x_axis from groupby if present, update docs and tests
- Updates create_chart logic to automatically remove x_axis from groupby for ECharts timeseries charts, preventing duplicate dimension usage.
- Updates and expands unit test to verify x_axis is excluded from groupby, using improved test mocks for accurate backend simulation.
- Updates documentation (README.md, README_ARCHITECTURE.md, README_PHASE1_STATUS.md, README_SCHEMAS.md) to clarify create_chart tool behavior and schema, including new groupby/x_axis handling.
- No breaking changes to tool signatures; behavior is now more robust and LLM-friendly.
2025-07-30 14:20:39 -04:00
Amin Ghadersohi
afdb8b38a6 update: Add columns and metrics to DatasetInfo/list_datasets, improve test coverage
- Updated DatasetInfo schema to include columns and metrics fields, with new TableColumnInfo and SqlMetricInfo models.
- Updated serialize_dataset_object to serialize columns and metrics for each dataset.
- Modified list_datasets tool to use serialize_dataset_object and include columns/metrics by default.
- Improved and fixed all related unit tests to use proper MagicMock objects for columns/metrics and to parse JSON responses.
- Ensured LLM/OpenAPI compatibility for dataset listing and info tools.
2025-07-30 14:20:39 -04:00
Amin Ghadersohi
fc7ea804bc update: chart create working with union of types 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
7c256ae9aa update: docs 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
1b190abc3b update: add new ModelGetAvailableFiltersTool 2025-07-30 14:20:39 -04:00
Amin Ghadersohi
c6c71bf835 update: update tool registration to use decorators, and update all tests to use mcp client directly in the tests as recommended 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
cd5ead7f11 update: update tool registration 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
e5eebe28f9 update: refactor auth and model tools 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
9eac6ef433 update: add tests for mcp list and info tools 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
d523d523e5 update: refactor tools directories 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
91a3214ed4 update: wip 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
95b787f024 update: wip 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
39121791e8 update: wip 2025-07-30 14:20:38 -04:00
Amin Ghadersohi
9d40fe913f update: wip 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
748ae49c8c update: wip 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
a9d543b6f4 update: unify FastMCP server, modularize tools, and document new DAO-based architecture
- Replace dual Flask/FastAPI setup with a single, unified FastMCP server (`server.py`)
- Introduce `MCPDAOWrapper` for secure, context-aware DAO access (`dao_wrapper.py`)
- Refactor all MCP tools to be modular and domain-organized (`tools/dashboard/`, `tools/chart/`, `tools/dataset/`, `tools/system/`)
- Strongly type all tool contracts with Pydantic v2 models, including full field documentation for LLM/OpenAPI compatibility
- Refactor and extend `BaseDAO` for robust, generic CRUD/list operations
- Add and update documentation:
  - Architecture and flow diagrams (`README_ARCHITECTURE.md`)
  - Tool schema reference and usage instructions (`README.md`, `README_SCHEMAS.md`)
  - Phase 1 status and roadmap (`README_PHASE1_STATUS.md`)
- Implement and test all core list/info tools for dashboards, datasets, and charts, with full search and filter support
- Add chart creation tool (`create_chart_simple`)
- Provide extension points for Preset-specific auth, RBAC, and logging (stubbed in Phase 1)
- Prepare for LLM/agent workflows and future command-based mutations (create/update/delete)
- Expand and update unit/integration test coverage for all tools
2025-07-30 14:20:37 -04:00
Amin Ghadersohi
55d6130fc4 update: docs and deps 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
b98e3eb309 Superset MCP Service - Adding get_instance_info tool 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
b469077e0e update: add list function to BaseDAO and use it to get and filter the dashboards 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
397b4e450b Superset MCP Service - Initial Commit DRAFT 2025-07-30 14:20:37 -04:00
Amin Ghadersohi
0f97002520 update: Refactor BaseDAO: enhance column operator logic and expand test coverage
- Improved the BaseDAO class to robustly handle column operator logic, ensuring all supported operators (eq, ne, sw, ew, in, nin, gt, gte, lt, lte, like, ilike, is_null, is_not_null) are consistently applied via ColumnOperatorEnum.
- Refactored the apply_column_operators and list methods for clarity and reliability, including better handling of columns, relationships, and search.
- Removed 1 index base page handing from list
2025-07-30 14:20:37 -04:00
Amin Ghadersohi
2312250127 update: add idea of ColumnOperator 2025-07-30 14:20:36 -04:00
Amin Ghadersohi
cd52193869 update: add helper for applying base filter 2025-07-30 14:20:36 -04:00
Amin Ghadersohi
9ffe680aaa feat(dao): add robust generic list and count methods to BaseDAO with full test coverage
- Introduced generic `list` and `count` methods to BaseDAO for consistent querying and counting across all DAOs.
- Both methods support filtering (including IN queries), ordering, pagination, search across columns, custom FAB-style filters, and always-on base filters.
- Added comprehensive unit tests for `list` and `count` in `tests/unit_tests/dao/base_test.py`, covering:
  - Filtering (including boolean, None, and IN queries)
  - Ordering (asc/desc, multiple columns)
  - Pagination (including out-of-range)
  - Search across columns
  - Custom filter logic
  - Always-on base filter logic
  - Edge cases and skip_base_filter
- Moved common test fixtures to `conftest.py` for reuse.
2025-07-30 14:20:36 -04:00
597 changed files with 32210 additions and 38819 deletions

View File

@@ -1,40 +0,0 @@
# Claude Code RC for move-controls
This is a claudette-managed Apache Superset development environment.
## Project: move-controls
- Worktree Path: /Users/evan_1/.claudette/worktrees/move-controls
- Frontend Port: 9004
- Frontend URL: http://localhost:9004
## Quick Commands
Start services:
```bash
claudette docker up
```
Access frontend:
```bash
open http://localhost:9004
```
Run tests:
```bash
# Backend
pytest tests/unit_tests/
# Frontend
cd superset-frontend && npm test
```
## Environment Details
- Python venv: `.venv/` (auto-activated in claudette shell)
- Node modules: `superset-frontend/node_modules/`
- Docker prefix: `move-controls_`
## Development Tips
- Always use `claudette shell` to work in this project
- Run `pre-commit run --all-files` before committing
- Use `claudette docker` instead of docker-compose directly
- The frontend dev server runs on port 9004 to avoid conflicts

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

@@ -3,14 +3,3 @@
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

@@ -0,0 +1,19 @@
{
// Extend the base configuration
"extends": "../devcontainer-base.json",
"name": "Apache Superset Development (Default)",
// Forward ports for development
"forwardPorts": [9001],
"portsAttributes": {
"9001": {
"label": "Superset (via Webpack Dev Server)",
"onAutoForward": "notify",
"visibility": "public"
}
},
// Auto-start Superset on Codespace resume
"postStartCommand": ".devcontainer/start-superset.sh"
}

View File

@@ -0,0 +1,39 @@
{
"name": "Apache Superset Development",
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
// Using the same base as Dockerfile, but non-slim for dev tools
"image": "python:3.11.13-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true,
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"configureZshAsDefaultShell": true
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
},
// Run commands after container is created
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
// VS Code customizations
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}

View File

@@ -1,15 +1,8 @@
{
"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"]
},
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
// Using the same base as Dockerfile, but non-slim for dev tools
"image": "python:3.11.13-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
@@ -39,17 +32,10 @@
},
// Run commands after container is created
"postCreateCommand": "bash .devcontainer/setup-dev.sh || echo '⚠️ Setup had issues - run .devcontainer/setup-dev.sh manually'",
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
// Auto-start Superset after ensuring Docker is ready
// Run in foreground to see any errors, but don't block on failures
"postStartCommand": "bash -c 'echo \"Waiting 30s for services to initialize...\"; sleep 30; .devcontainer/start-superset.sh || echo \"⚠️ Auto-start failed - run start-superset manually\"'",
// Set environment variables
"remoteEnv": {
// Removed automatic venv activation to prevent startup issues
// The setup script will handle this
},
// Auto-start Superset on Codespace resume
"postStartCommand": ".devcontainer/start-superset.sh",
// VS Code customizations
"customizations": {

View File

@@ -3,76 +3,30 @@
echo "🔧 Setting up Superset development environment..."
# System dependencies and uv are now pre-installed in the Docker image
# This speeds up Codespace creation significantly!
# The universal image has most tools, just need Superset-specific libs
echo "📦 Installing Superset-specific dependencies..."
sudo apt-get update
sudo apt-get install -y \
libsasl2-dev \
libldap2-dev \
libpq-dev \
tmux \
gh
# Create virtual environment using uv
echo "🐍 Creating Python virtual environment..."
if ! uv venv; then
echo "❌ Failed to create virtual environment"
exit 1
fi
# Install uv for fast Python package management
echo "📦 Installing uv..."
curl -LsSf https://astral.sh/uv/install.sh | sh
# 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
# Add cargo/bin to PATH for uv
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
# 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
npm install -g @anthropic-ai/claude-code
# 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 ""
echo "🚀 Run '.devcontainer/start-superset.sh' to start Superset"

View File

@@ -1,14 +1,14 @@
#!/bin/bash
# Startup script for Superset in Codespaces
# Log to a file for debugging
LOG_FILE="/tmp/superset-startup.log"
echo "[$(date)] Starting Superset startup script" >> "$LOG_FILE"
echo "[$(date)] User: $(whoami), PWD: $(pwd)" >> "$LOG_FILE"
echo "🚀 Starting Superset in Codespaces..."
echo "🌐 Frontend will be available at port 9001"
# Check if MCP is enabled
if [ "$ENABLE_MCP" = "true" ]; then
echo "🤖 MCP Service will be available at port 5008"
fi
# Find the workspace directory (Codespaces clones as 'superset', not 'superset-2')
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
if [ -n "$WORKSPACE_DIR" ]; then
@@ -18,71 +18,32 @@ 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
# Check if docker is running
if ! docker info > /dev/null 2>&1; then
echo " Waiting for Docker to start..."
sleep 5
fi
# Clean up any existing containers
echo "🧹 Cleaning up existing containers..."
docker-compose -f docker-compose-light.yml down
docker-compose -f docker-compose-light.yml --profile mcp down
# Start services
echo "🏗️ Starting Superset in background (daemon mode)..."
echo "🏗️ Building and starting services..."
echo ""
echo "📝 Once started, login with:"
echo " Username: admin"
echo " Password: admin"
echo ""
echo "📋 Running in foreground with live logs (Ctrl+C to stop)..."
# 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
# Run docker-compose and capture exit code
if [ "$ENABLE_MCP" = "true" ]; then
echo "🤖 Starting with MCP Service enabled..."
docker-compose -f docker-compose-light.yml --profile mcp up
else
docker-compose -f docker-compose-light.yml up
fi
EXIT_CODE=$?
# If it failed, provide helpful instructions

View File

@@ -0,0 +1,29 @@
{
// Extend the base configuration
"extends": "../devcontainer-base.json",
"name": "Apache Superset Development with MCP",
// Forward ports for development
"forwardPorts": [9001, 5008],
"portsAttributes": {
"9001": {
"label": "Superset (via Webpack Dev Server)",
"onAutoForward": "notify",
"visibility": "public"
},
"5008": {
"label": "MCP Service (Model Context Protocol)",
"onAutoForward": "notify",
"visibility": "private"
}
},
// Auto-start Superset with MCP on Codespace resume
"postStartCommand": "ENABLE_MCP=true .devcontainer/start-superset.sh",
// Environment variables
"containerEnv": {
"ENABLE_MCP": "true"
}
}

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 }}

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ __pycache__
.cache
.bento*
.cache-loader
.claude_rc
.coverage
cover
.DS_Store

215
CHART_METADATA_API.md Normal file
View File

@@ -0,0 +1,215 @@
# Chart Metadata API Reference
The Superset MCP service provides rich metadata alongside chart generation to enable better UI integration and user experiences.
## Background & Design Philosophy
Modern chart systems need to provide more than just visual output. Inspired by contemporary web standards and LLM integration patterns, this metadata system addresses several key needs:
**Accessibility-First Design**: Following WCAG guidelines and `aria-*` attribute patterns, charts include semantic descriptions and accessibility metadata to ensure inclusive experiences.
**Rich Context for AI Systems**: Similar to how platforms like social media generate rich previews (OpenGraph, Twitter Cards), charts provide semantic understanding beyond just visual representation - enabling AI agents to reason about and describe visualizations meaningfully.
**Performance-Aware Integration**: Modern web APIs emphasize performance transparency (Core Web Vitals, etc.). Charts include execution metrics and optimization suggestions to help UIs make informed decisions about rendering and user feedback.
**Capability-Driven UX**: Rather than requiring UIs to hardcode chart type behaviors, the system exposes what each chart can actually do - enabling dynamic, contextual interfaces that adapt to chart capabilities.
## Overview
When generating charts via `generate_chart`, the response includes structured metadata that helps UIs:
- Present appropriate controls and interactions
- Generate accessible descriptions
- Optimize rendering performance
- Guide user workflows
## Metadata Types
### ChartCapabilities
Describes what interactions and features the chart supports.
```python
{
"supports_interaction": bool, # User can interact (zoom, pan, hover)
"supports_real_time": bool, # Chart can update with live data
"supports_drill_down": bool, # Can navigate to more detailed views
"supports_export": bool, # Can be exported to other formats
"optimal_formats": [ # Recommended preview formats
"url", # Static image URL
"interactive", # HTML with JavaScript controls
"ascii", # Text-based representation
"vega_lite" # Vega-Lite specification
],
"data_types": [ # Types of data visualized
"time_series", # Time-based data
"categorical", # Discrete categories
"metric" # Numeric measurements
]
}
```
**UI Integration:**
- Show/hide interaction controls based on `supports_interaction`
- Enable real-time updates if `supports_real_time`
- Display drill-down options for `supports_drill_down`
- Choose optimal preview format from `optimal_formats`
### ChartSemantics
Provides semantic understanding of what the chart represents and reveals.
```python
{
"primary_insight": "Shows trends and changes over time",
"data_story": "This line chart analyzes sales, revenue over Q1-Q4",
"recommended_actions": [
"Review data patterns and trends",
"Consider filtering for more detail",
"Export chart for reporting"
],
"anomalies": [], # Notable outliers (future enhancement)
"statistical_summary": {} # Key statistics (future enhancement)
}
```
**UI Integration:**
- Display `primary_insight` as chart description
- Use `data_story` for accessibility and tooltips
- Show `recommended_actions` as suggested next steps
- Highlight `anomalies` in the visualization
### AccessibilityMetadata
Information for creating inclusive, accessible chart experiences.
```python
{
"color_blind_safe": bool, # Uses colorblind-friendly palette
"alt_text": "Chart showing Sales Data over time",
"high_contrast_available": bool # High contrast version available
}
```
**UI Integration:**
- Use `alt_text` for screen readers
- Show accessibility indicators if `color_blind_safe`
- Offer high contrast mode if available
### PerformanceMetadata
Performance information for optimization and user feedback.
```python
{
"query_duration_ms": 1250, # Time to generate chart data
"cache_status": "hit|miss|error", # Whether data came from cache
"optimization_suggestions": [ # Performance improvement tips
"Consider adding date filters to reduce data volume",
"Chart complexity may impact load time"
]
}
```
**UI Integration:**
- Show loading indicators based on `query_duration_ms`
- Display cache status for debugging
- Present `optimization_suggestions` to users
- Warn about slow queries
## Example Response
```json
{
"chart": {
"id": 123,
"slice_name": "Sales Trends Q1-Q4",
"viz_type": "echarts_timeseries_line",
"url": "/explore/?slice_id=123"
},
"capabilities": {
"supports_interaction": true,
"supports_real_time": false,
"supports_drill_down": false,
"supports_export": true,
"optimal_formats": ["url", "interactive", "ascii"],
"data_types": ["time_series", "metric"]
},
"semantics": {
"primary_insight": "Shows trends and changes over time",
"data_story": "This line chart analyzes sales over Q1-Q4",
"recommended_actions": [
"Review seasonal patterns",
"Export for quarterly report"
]
},
"accessibility": {
"color_blind_safe": true,
"alt_text": "Line chart showing sales trends from Q1 to Q4",
"high_contrast_available": false
},
"performance": {
"query_duration_ms": 450,
"cache_status": "miss",
"optimization_suggestions": []
}
}
```
## Usage Examples
### React Component Integration
```jsx
function ChartComponent({ chartData }) {
const { capabilities, semantics, accessibility, performance } = chartData;
return (
<div>
{/* Accessibility */}
<img
src={chartData.chart.url}
alt={accessibility.alt_text}
aria-describedby="chart-description"
/>
{/* Semantic description */}
<p id="chart-description">{semantics.primary_insight}</p>
{/* Conditional controls based on capabilities */}
{capabilities.supports_interaction && (
<InteractiveControls />
)}
{capabilities.supports_export && (
<ExportButton />
)}
{/* Performance feedback */}
{performance.query_duration_ms > 2000 && (
<SlowQueryWarning suggestions={performance.optimization_suggestions} />
)}
{/* Recommended actions */}
<ActionSuggestions actions={semantics.recommended_actions} />
</div>
);
}
```
## Chart Type Mapping
Different chart types provide different capabilities:
| Chart Type | Interaction | Real-time | Drill-down | Optimal Formats |
|------------|------------|-----------|------------|-----------------|
| `echarts_timeseries_line` | ✅ | ✅ | ❌ | url, interactive, ascii |
| `echarts_timeseries_bar` | ✅ | ✅ | ❌ | url, interactive, ascii |
| `table` | ❌ | ❌ | ✅ | url, table, ascii |
| `pie` | ✅ | ❌ | ❌ | url, interactive |
## Future Enhancements
- **Statistical Summary**: Automatic calculation of mean, median, trends
- **Anomaly Detection**: Identification of outliers and unusual patterns
- **Smart Recommendations**: ML-powered suggestions for chart improvements
- **Accessibility Scoring**: Automated accessibility compliance checking

View File

@@ -1,802 +0,0 @@
# Control Panel Migration Agent
A comprehensive guide for migrating Apache Superset control panels from the legacy config-based approach to the new React-based approach.
## Overview
This migration transforms control panels from complex string-referenced configurations (`controlPanelSections`/`controlSetRows`) to pure React components. The new approach provides:
- **Direct React components** instead of config objects
- **Full TypeScript support** with proper type safety
- **Simplified architecture** with no JSON intermediary
- **Better developer experience** with IDE autocomplete and refactoring support
## Architecture Comparison
### Legacy Architecture
```typescript
// Old approach: Config-based with string references and config objects
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[GroupByControl()], // String reference
[MetricControl()], // String reference
[
{
name: 'show_labels',
config: {
type: 'CheckboxControl',
label: t('Show Labels'),
renderTrigger: true,
default: true,
},
},
], // Config object
],
},
],
};
```
### New React-Based Architecture
```typescript
// New approach: Pure React components with direct JSX
export const PieControlPanel: FC<PieControlPanelProps> = ({ ... }) => {
return (
<div>
<DndColumnSelect
value={formValues.groupby || []}
onChange={handleChange('groupby')}
// ... other props
/>
<CheckboxControl
label={t('Show Labels')}
value={formValues.show_labels ?? true}
onChange={handleChange('show_labels')}
renderTrigger
/>
</div>
);
};
// Mark as modern panel
(PieControlPanel as any).isModernPanel = true;
```
## Migration Steps
### 1. File Structure
Keep the existing file location but change from config to React component:
- `controlPanel.ts``[ChartName]ControlPanelSimple.tsx`
- The file remains in the same directory as the original
### 2. Update Imports
**From (Legacy):**
```typescript
import {
ControlPanelConfig,
sharedControls,
GroupByControl,
MetricControl
} from '@superset-ui/chart-controls';
```
**To (Modern):**
```typescript
import { FC, useState } from 'react';
import { t } from '@superset-ui/core';
import { Tabs } from 'antd';
import {
ColorSchemeControl,
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
} from '@superset-ui/chart-controls';
// Direct component imports
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import TextControl from '../../../../src/explore/components/controls/TextControl';
import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
import SliderControl from '../../../../src/explore/components/controls/SliderControl';
import SelectControl from '../../../../src/explore/components/controls/SelectControl';
import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
import ControlHeader from '../../../../src/explore/components/ControlHeader';
import Control from '../../../../src/explore/components/Control';
```
### 3. Component Structure Template
Create a React component with this structure:
```typescript
interface [ChartName]ControlPanelProps {
onChange?: (field: string, value: any) => void;
value?: Record<string, any>;
datasource?: any;
actions?: any;
controls?: any;
form_data?: any;
}
export const [ChartName]ControlPanel: FC<[ChartName]ControlPanelProps> = ({
onChange,
value,
datasource,
form_data,
actions,
controls,
}) => {
// Safety checks for datasource
if (!datasource || !form_data) {
return <div>Loading control panel...</div>;
}
// Ensure safe data structures
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
// Helper for control changes
const handleChange = (field: string) => (val: any) => {
if (actions?.setControlValue) {
actions.setControlValue(field, val);
} else if (onChange) {
onChange(field, val);
}
};
// Get form values
const formValues = form_data || value || {};
// Tab state (if using tabs)
const [activeTab, setActiveTab] = useState('data');
// Component implementation here...
return (
<div style={{ padding: '16px' }}>
{/* Your controls here */}
</div>
);
};
// CRITICAL: Mark as modern panel
([ChartName]ControlPanel as any).isModernPanel = true;
// Export wrapper config for compatibility
const config = {
controlPanelSections: [
{
label: null,
expanded: true,
controlSetRows: [[[ChartName]ControlPanel as any]],
},
],
controlOverrides: {
// Move all defaults here
field_name: {
default: defaultValue,
label: t('Field Label'),
renderTrigger: true,
},
},
};
export default config;
```
### 4. Control Mapping Reference
#### String References → React Components
| Legacy String | Modern React Component | Notes |
|---------------|----------------------|--------|
| `['groupby']` | `<DndColumnSelect ... />` | Multi-select columns |
| `['metric']` | `<DndMetricSelect ... />` | Single metric select |
| `['metrics']` | `<DndMetricSelect ... />` | Multi metric select |
| `['adhoc_filters']` | `<DndFilterSelect ... />` | Advanced filters |
| `['row_limit']` | `<TextControl isInt ... />` | Numeric input |
| `['color_scheme']` | `ColorSchemeControl()` with `Control` wrapper | Special handling needed |
#### Config Objects → React Components
| Legacy Config | Modern Component | Example |
|---------------|------------------|---------|
| `{ type: 'TextControl', ... }` | `<TextControl ... />` | `<TextControl value={val} onChange={fn} />` |
| `{ type: 'CheckboxControl', ... }` | `<CheckboxControl ... />` | `<CheckboxControl label="..." value={val} />` |
| `{ type: 'SelectControl', ... }` | `<SelectControl ... />` | `<SelectControl choices={[...]} value={val} />` |
| `{ type: 'SliderControl', ... }` | `<SliderControl ... />` | `<SliderControl {...{min: 0, max: 100}} />` |
### 5. Props Mapping Guide
| Legacy Config Property | Modern React Prop | Notes |
|----------------------|------------------|-------|
| `label` | `label` prop OR `<ControlHeader>` | Use ControlHeader for tooltips |
| `description` | `description` prop OR `<ControlHeader>` | ControlHeader for complex descriptions |
| `default` | Move to `controlOverrides` | Don't set as component prop |
| `renderTrigger: true` | `renderTrigger` prop | Controls instant chart updates |
| `visibility` | Conditional rendering | `{condition && <Control />}` |
| `choices` | `choices` prop | For SelectControl |
| `min/max/step` | Spread object | `{...{ min: 10, max: 100, step: 1 }}` |
### 6. Implementing Tabbed Layout
Most modern control panels use a Data/Customize tab structure:
```typescript
const [activeTab, setActiveTab] = useState('data');
const dataTabContent = (
<div>
{/* Query-related controls: columns, metrics, filters, row limit */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
label={t('Group by')}
description={t('Columns to group by')}
hovered
/>
<DndColumnSelect
value={formValues.groupby || []}
onChange={handleChange('groupby')}
options={safeColumns}
name="groupby"
label="" // Avoid duplicate labels
multi
canDelete
ghostButtonText={t('Add dimension')}
type="DndColumnSelect"
actions={actions}
/>
</div>
{/* More data controls... */}
</div>
);
const customizeTabContent = (
<div>
{/* Styling and display controls */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Chart Options')}</h4>
{/* Styling controls... */}
</div>
</div>
);
const tabItems = [
{ key: 'data', label: t('Data'), children: dataTabContent },
{ key: 'customize', label: t('Customize'), children: customizeTabContent },
];
return (
<div style={{ padding: '16px' }}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
size="large"
/>
</div>
);
```
### 7. Common Control Patterns
#### Drag-and-Drop Controls
```typescript
{/* Group By - Column Selection */}
<DndColumnSelect
value={formValues.groupby || []}
onChange={handleChange('groupby')}
options={safeColumns}
name="groupby"
label="" // Empty to avoid duplicate with ControlHeader
multi
canDelete
ghostButtonText={t('Add dimension')}
type="DndColumnSelect"
actions={actions}
/>
{/* Metric Selection */}
<DndMetricSelect
value={formValues.metric}
onChange={handleChange('metric')}
datasource={safeDataSource}
name="metric"
label=""
multi={false}
savedMetrics={safeMetrics}
/>
{/* Filters */}
<DndFilterSelect
value={formValues.adhoc_filters || []}
onChange={handleChange('adhoc_filters')}
datasource={safeDataSource}
columns={safeColumns}
formData={formValues}
name="adhoc_filters"
savedMetrics={safeMetrics}
selectedMetrics={formValues.metric ? [formValues.metric] : []}
type="DndFilterSelect"
actions={actions}
/>
```
#### Color Scheme Control (Special Case)
```typescript
{/* Color Scheme requires special Control wrapper */}
{(() => {
const colorSchemeControl = ColorSchemeControl();
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
return (
<Control
{...cleanConfig}
name="color_scheme"
value={formValues.color_scheme}
actions={{
...actions,
setControlValue: (field: string, val: any) => {
handleChange('color_scheme')(val);
},
}}
renderTrigger
/>
);
})()}
```
#### Basic Controls
```typescript
{/* Text Input */}
<TextControl
value={formValues.row_limit}
onChange={handleChange('row_limit')}
isInt
placeholder="100"
controlId="row_limit"
/>
{/* Checkbox */}
<CheckboxControl
label={t('Show Labels')}
description={t('Whether to display the labels.')}
value={formValues.show_labels ?? true}
onChange={handleChange('show_labels')}
renderTrigger
hovered
/>
{/* Select Dropdown */}
<SelectControl
label={t('Label Type')}
description={t('What should be shown on the label?')}
value={formValues.label_type || 'key'}
onChange={handleChange('label_type')}
choices={[
['key', t('Category Name')],
['value', t('Value')],
['percent', t('Percentage')],
]}
clearable={false}
renderTrigger
hovered
/>
{/* Slider */}
<SliderControl
value={formValues.outerRadius || 70}
onChange={handleChange('outerRadius')}
{...{ min: 10, max: 100, step: 1 }}
/>
```
#### Control Headers with Tooltips
```typescript
<ControlHeader
label={t('Percentage threshold')}
description={t('Minimum threshold in percentage points for showing labels.')}
renderTrigger
hovered
/>
```
#### Conditional Rendering
```typescript
{/* Show control only when condition is met */}
{formValues.show_labels && (
<CheckboxControl
label={t('Put labels outside')}
description={t('Put the labels outside of the pie?')}
value={formValues.labels_outside ?? true}
onChange={handleChange('labels_outside')}
renderTrigger
hovered
/>
)}
{/* Nested conditional rendering */}
{formValues.label_type === 'template' && (
<TextControl
value={formValues.label_template || ''}
onChange={handleChange('label_template')}
placeholder="{name}: {value}"
controlId="label_template"
/>
)}
```
### 8. Section Organization
Use HTML headers and spacing for logical groupings:
```typescript
{/* Chart Options Section */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Chart Options')}</h4>
{/* Controls for this section */}
<div style={{ marginBottom: 16 }}>
{/* Individual control */}
</div>
</div>
{/* Labels Section */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Labels')}</h4>
{/* Label-related controls */}
</div>
```
### 9. Control Defaults in controlOverrides
Move all default values to the `controlOverrides` section:
```typescript
const config = {
controlPanelSections: [
{
label: null,
expanded: true,
controlSetRows: [[PieControlPanel as any]],
},
],
controlOverrides: {
groupby: {
default: [],
label: t('Group by'),
},
metric: {
default: null,
label: t('Metric'),
},
show_labels: {
default: true,
label: t('Show labels'),
renderTrigger: true,
},
color_scheme: {
default: 'supersetColors',
label: t('Color scheme'),
renderTrigger: true,
},
// ... all other defaults
},
};
```
### 10. Chart Plugin Integration
Update the chart plugin to use the new control panel:
```typescript
// In your chart's index.ts file
import controlPanel from './PieControlPanelSimple'; // New React-based panel
export default class EchartsPieChartPlugin extends EchartsChartPlugin {
constructor() {
super({
controlPanel,
// ... other config
});
}
}
```
## Testing Your Migration
### 1. Visual Validation
- [ ] All controls render properly in the UI
- [ ] Tab navigation works (if using tabs)
- [ ] Control layout matches the original
- [ ] Conditional controls show/hide correctly
### 2. Functional Testing
- [ ] Control changes update the chart immediately (if `renderTrigger: true`)
- [ ] Form values persist when switching between tabs
- [ ] Drag-and-drop controls work with datasource
- [ ] Error states display appropriately
- [ ] Default values apply correctly
### 3. Integration Testing
- [ ] Control panel works in Explore view
- [ ] Values save correctly when creating charts
- [ ] Dashboard filters work with the controls
- [ ] Chart reloading preserves control values
## Common Issues & Solutions
### Issue: Double Labels on Controls
**Problem:** Control shows both ControlHeader label and control's built-in label
**Solution:** Set `label=""` on the control when using ControlHeader:
```typescript
<ControlHeader label={t('Group by')} />
<DndColumnSelect
label="" // Empty to prevent duplicate
// ... other props
/>
```
### Issue: Slider Min/Max Not Working
**Problem:** Slider doesn't respect min/max values
**Solution:** Use spread operator with object literal:
```typescript
<SliderControl
value={formValues.outerRadius || 70}
onChange={handleChange('outerRadius')}
{...{ min: 10, max: 100, step: 1 }} // Use spread with object
/>
```
### Issue: Controls Not Triggering Chart Updates
**Problem:** Chart doesn't refresh when controls change
**Solution:** Ensure `renderTrigger` is set where needed:
```typescript
<CheckboxControl
// ... other props
renderTrigger // Add this for instant updates
/>
```
### Issue: "Cannot read properties of undefined"
**Problem:** Attempting to access undefined datasource or form_data
**Solution:** Add safety checks and fallbacks:
```typescript
// Safety checks at component start
if (!datasource || !form_data) {
return <div>Loading control panel...</div>;
}
// Safe array access
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
```
### Issue: Color Scheme Control Not Working
**Problem:** ColorSchemeControl doesn't integrate properly
**Solution:** Use the special Control wrapper pattern:
```typescript
{(() => {
const colorSchemeControl = ColorSchemeControl();
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
return (
<Control
{...cleanConfig}
name="color_scheme"
value={formValues.color_scheme}
actions={{
...actions,
setControlValue: (field: string, val: any) => {
handleChange('color_scheme')(val);
},
}}
renderTrigger
/>
);
})()}
```
## Migration Checklist
### Pre-Migration
- [ ] Identify all controls in the legacy control panel
- [ ] Note any conditional control visibility rules
- [ ] Check for custom control configurations
- [ ] Understand the chart's specific requirements
### During Migration
- [ ] Create new `[ChartName]ControlPanelSimple.tsx` file
- [ ] Implement component structure with proper interface
- [ ] Map all legacy controls to React components
- [ ] Add safety checks for datasource/form_data
- [ ] Implement tab structure (Data/Customize)
- [ ] Add all control defaults to `controlOverrides`
- [ ] Mark component as modern with `isModernPanel = true`
- [ ] Update chart plugin to import new control panel
### Post-Migration Testing
- [ ] Test all control interactions
- [ ] Verify chart updates on control changes
- [ ] Check conditional control visibility
- [ ] Validate default values
- [ ] Test with different datasources
- [ ] Run pre-commit hooks: `pre-commit run`
- [ ] Test in Explore and Dashboard contexts
## Advanced Patterns
### Dynamic Control Visibility
```typescript
// Show additional controls based on current selection
{formValues.chart_type === 'pie' && (
<div>
{/* Pie-specific controls */}
<CheckboxControl
label={t('Show as Donut')}
value={formValues.donut ?? false}
onChange={handleChange('donut')}
/>
{formValues.donut && (
<SliderControl
value={formValues.innerRadius || 30}
onChange={handleChange('innerRadius')}
{...{ min: 0, max: 100, step: 1 }}
/>
)}
</div>
)}
```
### Complex Control Groups
```typescript
{/* Side-by-side controls */}
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1 }}>
<ControlHeader label={t('Min Value')} />
<TextControl
value={formValues.min_value}
onChange={handleChange('min_value')}
isFloat
/>
</div>
<div style={{ flex: 1 }}>
<ControlHeader label={t('Max Value')} />
<TextControl
value={formValues.max_value}
onChange={handleChange('max_value')}
isFloat
/>
</div>
</div>
```
### Custom Validation
```typescript
// Add validation logic to handleChange
const handleChange = (field: string) => (val: any) => {
// Custom validation
if (field === 'row_limit' && val && val < 1) {
console.warn('Row limit must be positive');
return;
}
if (actions?.setControlValue) {
actions.setControlValue(field, val);
} else if (onChange) {
onChange(field, val);
}
};
```
## Additional Migration Patterns
### Single Column Selection
When a control expects a single column value (not an array):
```typescript
// For Sankey source/target columns
const handleSingleColumnChange = (field: string) => (val: any) => {
const singleValue = Array.isArray(val) ? val[0] : val;
actions.setControlValue(field, singleValue);
};
// Usage
<DndColumnSelect
value={formValues.source ? [formValues.source] : []}
onChange={handleSingleColumnChange('source')}
options={safeColumns}
multi={false}
/>
```
### Required Field Validation
For controls that must have values:
```typescript
import { validateNonEmpty } from '@superset-ui/core';
// In controlOverrides
source: {
validators: [validateNonEmpty],
label: t('Source Column'),
},
```
### Chart Type Descriptions
Add helpful descriptions at the top of control panels:
```typescript
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
{t('Sankey Diagram')}
</div>
<div style={{ fontSize: '12px', opacity: 0.65 }}>
{t('Visualize flow between different entities')}
</div>
</div>
```
## Common Migration Issues & Solutions
### Issue 1: "Cannot read properties of undefined (reading 'map')"
**Problem:** DndColumnSelect crashes when datasource is undefined
**Solution:** Pass `options={datasource?.columns || []}` instead of `datasource={datasource}`
### Issue 2: Tabs import error ("Element type is invalid")
**Problem:** Runtime error when loading control panel
**Solution:** Import from 'antd' directly: `import { Tabs } from 'antd';` (NOT from '@superset-ui/core')
### Issue 3: React hooks error
**Problem:** "React Hook 'useState' is called conditionally"
**Solution:** Always declare state hooks before any conditional returns:
```typescript
const [activeTab, setActiveTab] = useState('data'); // FIRST
if (!datasource) return <div>Loading...</div>; // THEN conditions
```
### Issue 4: ESLint color literal warnings
**Problem:** theme-colors/no-literal-colors ESLint rule
**Solution:** Use opacity instead of color literals:
```typescript
// Bad: style={{ color: '#666' }}
// Good: style={{ opacity: 0.65 }}
```
### Issue 5: Single value vs array handling
**Problem:** Some controls expect single values but DndColumnSelect returns arrays
**Solution:** See "Single Column Selection" pattern above
### Issue 6: antd import warnings
**Problem:** "'antd' should be listed in the project's dependencies"
**Solution:** Use `SKIP=eslint-frontend` when committing if antd is already available
## Reference Implementation
The Pie chart control panel migration (`PieControlPanelSimple.tsx`) serves as the definitive reference implementation showing:
- Complete tab-based layout (Data/Customize)
- All major control types (DndColumnSelect, CheckboxControl, SelectControl, SliderControl, etc.)
- Conditional control rendering
- Proper safety checks and error handling
- Color scheme integration
- Control grouping and organization
- Modern React patterns and TypeScript usage
Study this implementation for best practices and patterns that can be applied to any chart control panel migration.
## Summary
The new React-based control panel approach provides:
1. **Better Developer Experience** - Direct React components with TypeScript
2. **Improved Maintainability** - Clear component structure and patterns
3. **Enhanced Flexibility** - Easy conditional rendering and dynamic controls
4. **Type Safety** - Full TypeScript support with proper interfaces
5. **Simplified Architecture** - No complex config intermediaries
The migration process involves converting string references and config objects to direct React components, implementing proper safety checks, and organizing controls in a logical tab-based structure. The key is to maintain compatibility with the existing Superset infrastructure while providing a more modern and maintainable development experience.

View File

@@ -1,153 +0,0 @@
# Control Panel Modernization Guide
## Current State
Apache Superset's control panels currently use a legacy `controlSetRows` structure that relies on nested arrays to define layout. This approach has several limitations:
1. **Rigid Layout**: The nested array structure makes it difficult to create responsive or complex layouts
2. **Poor Type Safety**: Arrays of arrays don't provide good TypeScript support
3. **Mixed Paradigms**: String references, configuration objects, and React components are mixed together
4. **Limited Reusability**: Layout logic is embedded in the structure rather than using composable components
## Migration Strategy
### Phase 1: Component Modernization ✅ COMPLETED
- Replaced string-based control references with React components
- Updated individual control components to use Ant Design
- Modernized the `ControlRow` component to use Ant Design's Grid
### Phase 2: Layout Utilities ✅ COMPLETED
- Created `ControlPanelLayout.tsx` with reusable layout components
- Implemented `ControlSection`, `SingleControlRow`, `TwoColumnRow`, `ThreeColumnRow`
- Updated control group components to use Ant Design Row/Col
### Phase 3: React-Based Control Panels 🚧 IN PROGRESS
- Create `ReactControlPanel` component for rendering modern panels
- Support both legacy and modern formats during transition
- Provide migration helpers and examples
### Phase 4: Gradual Migration 📋 TODO
- Migrate chart control panels one by one
- Start with simpler charts (Pie, Bar) before complex ones
- Maintain backward compatibility throughout
## Modern Control Panel Structure
### Legacy Structure (controlSetRows)
```typescript
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[GroupByControl()],
[MetricControl()],
[AdhocFiltersControl()],
[RowLimitControl()],
],
},
],
};
```
### Modern Structure (React Components)
```typescript
const modernConfig: ReactControlPanelConfig = {
sections: [
{
key: 'query',
label: t('Query'),
expanded: true,
render: ({ values, onChange }) => (
<>
<SingleControlRow>
<GroupByControl value={values.groupby} onChange={onChange} />
</SingleControlRow>
<SingleControlRow>
<MetricControl value={values.metrics} onChange={onChange} />
</SingleControlRow>
<TwoColumnRow
left={<AdhocFiltersControl value={values.adhoc_filters} onChange={onChange} />}
right={<RowLimitControl value={values.row_limit} onChange={onChange} />}
/>
</>
),
},
],
};
```
## Benefits of Modernization
1. **Better Type Safety**: Full TypeScript support with proper interfaces
2. **Flexible Layouts**: Use Ant Design's Grid system for responsive layouts
3. **Cleaner Code**: React components instead of nested arrays
4. **Improved DX**: Better IDE support and autocomplete
5. **Easier Testing**: Component-based architecture is easier to test
6. **Consistent Styling**: Leverage Ant Design's theme system
## Migration Example
To migrate a control panel:
1. **Create a modern version** alongside the existing one:
```typescript
// controlPanelModern.tsx
export const modernConfig: ReactControlPanelConfig = {
sections: [/* ... */]
};
```
2. **Use the compatibility wrapper** for backward compatibility:
```typescript
export default createReactControlPanel(modernConfig);
```
3. **Update the chart plugin** to use the new control panel:
```typescript
import controlPanel from './controlPanelModern';
```
## Layout Components Available
- `ControlSection`: Collapsible section container
- `SingleControlRow`: Full-width single control
- `TwoColumnRow`: Two controls side by side (50/50)
- `ThreeColumnRow`: Three controls in a row (33/33/33)
- `Row` and `Col` from Ant Design for custom layouts
## Files Created
1. `packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx`
- Layout utility components
2. `packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx`
- Example of modern control panel structure
3. `plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx`
- Modern version of Pie chart control panel
## Next Steps
1. **Complete the ReactControlPanel integration** with ControlPanelsContainer
2. **Create migration tooling** to help convert existing panels
3. **Document best practices** for control panel design
4. **Update chart plugin template** to use modern structure
5. **Gradually migrate all 90+ control panels** in the codebase
## Technical Debt Addressed
- Eliminates nested array layout structure
- Removes string-based control references
- Reduces coupling between layout and configuration
- Improves maintainability and testability
- Enables better code splitting and lazy loading
## Backward Compatibility
The migration maintains full backward compatibility:
- Existing control panels continue to work
- Both formats can coexist during migration
- No breaking changes to the public API
- Charts can be migrated incrementally

75
LLMS.md
View File

@@ -180,6 +180,7 @@ pre-commit run eslint # Frontend linting
## Platform-Specific Instructions
- **[LLMS.md](LLMS.md)** - General LLM development guide (READ THIS FIRST)
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
@@ -189,77 +190,3 @@ pre-commit run eslint # Frontend linting
---
**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
## Control Panel Migration Guide
### New React-Based Control Panel Architecture
We've successfully migrated from the complex string-referenced control panel system to a simpler React-based approach. Here's how to migrate existing control panels:
#### Key Changes
1. **No more string references** - Use React component functions instead of strings
2. **Redux integration** - Controls connect directly to Redux via useFormData hook
3. **Type safety** - Full TypeScript support with proper types
4. **Simplified architecture** - Direct React components, no JSON intermediary
#### Migration Steps
##### 1. Update imports
Replace string references with component imports:
```typescript
// OLD
controlSetRows: [
['series'],
['metric'],
]
// NEW
import { SeriesControl, MetricControl } from '@superset-ui/chart-controls';
controlSetRows: [
[SeriesControl()],
[MetricControl()],
]
```
##### 2. Custom controls
Use InlineTextControl or InlineSelectControl for custom controls:
```typescript
// OLD
{
name: 'size_from',
config: { type: 'TextControl', ... }
}
// NEW
InlineTextControl('sizeFrom', {
label: t('Minimum Font Size'),
default: 10,
...
})
```
##### 3. Control naming
Use camelCase for new controls, but handle both in transformProps:
```typescript
// In transformProps.ts
const finalSizeFrom = sizeFrom ?? size_from ?? 10;
```
#### Available Control Components
All these return CustomControlItem and can be used in controlSetRows:
- `SeriesControl()` - Column selection
- `MetricControl()` - Metric selection
- `AdhocFiltersControl()` - Filter configuration
- `RowLimitControl()` - Row limit
- `ColorSchemeControl()` - Color scheme picker
- `InlineTextControl(name, config)` - Custom text input
- `InlineSelectControl(name, config)` - Custom select
#### Example Migration
See `plugins/plugin-chart-word-cloud/src/plugin/controlPanelFixed.ts` for a complete example.
# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.

View File

@@ -1,257 +0,0 @@
# Pie Chart Control Panel Migration - Phased Approach
## Phase 1: Parallel Implementation ✅ COMPLETED
We've created a modern control panel alongside the legacy one:
### Files Created:
1. **`controlPanelModern.tsx`** - Modern React-based control panel
2. **`ModernControlPanelRenderer.tsx`** - Bridge component for compatibility
3. **Updated `ControlPanelsContainer.tsx`** - Support for modern panels
### Key Features:
- Full React component structure (no `controlSetRows`)
- Uses Ant Design Grid directly
- Type-safe with TypeScript interfaces
- Conditional rendering based on form values
- Organized into logical sections
## Phase 2: Integration Testing 🚧 NEXT STEP
### 2.1 Update the Pie Chart Plugin
```typescript
// In plugins/plugin-chart-echarts/src/Pie/index.ts
import controlPanel from './controlPanel'; // Legacy
import controlPanelModern from './controlPanelModern'; // Modern
// Feature flag to toggle between old and new
const useModernPanel = window.featureFlags?.MODERN_CONTROL_PANELS;
export default class EchartsPieChartPlugin extends ChartPlugin {
constructor() {
super({
// ... other config
controlPanel: useModernPanel ? controlPanelModern : controlPanel,
});
}
}
```
### 2.2 Test the Modern Panel
Create test file to verify both panels produce same output:
```typescript
// controlPanel.test.tsx
describe('Pie Control Panel Migration', () => {
it('modern panel handles all legacy controls', () => {
// Test that all controls from legacy panel exist in modern
});
it('produces same form_data structure', () => {
// Verify form_data compatibility
});
it('visibility conditions work correctly', () => {
// Test conditional rendering
});
});
```
## Phase 3: Feature Flag Rollout
### 3.1 Add Feature Flag
```python
# In superset/config.py
FEATURE_FLAGS = {
"MODERN_CONTROL_PANELS": False, # Start disabled
}
```
### 3.2 Gradual Rollout
1. **Internal Testing**: Enable for development environment
2. **Beta Users**: Enable for select users (5%)
3. **Wider Rollout**: Increase to 50%
4. **Full Migration**: Enable for all users
5. **Cleanup**: Remove legacy code
## Phase 4: Migration Utilities
### 4.1 Control Panel Converter
```typescript
// convertLegacyPanel.ts
export function convertControlSetRows(rows: ControlSetRow[]): ReactElement {
return rows.map(row => {
if (row.length === 1) {
return <SingleControlRow>{convertControl(row[0])}</SingleControlRow>;
}
if (row.length === 2) {
return (
<TwoColumnRow
left={convertControl(row[0])}
right={convertControl(row[1])}
/>
);
}
// ... handle other cases
});
}
```
### 4.2 Common Patterns Library
```typescript
// commonPanelPatterns.tsx
export const QuerySection = ({ values, onChange }) => (
<>
<GroupByControl />
<MetricControl />
<AdhocFiltersControl />
<RowLimitControl />
</>
);
export const AppearanceSection = ({ values, onChange }) => (
<>
<ColorSchemeControl />
<OpacityControl />
<LegendControls />
</>
);
```
## Phase 5: Migrate Other Charts
### Priority Order (Simple to Complex):
1. **Simple Charts** (1-2 weeks each)
- Bar Chart
- Line Chart
- Area Chart
- Scatter Plot
2. **Medium Complexity** (2-3 weeks each)
- Table
- Pivot Table
- Heatmap
- Treemap
3. **Complex Charts** (3-4 weeks each)
- Mixed Time Series
- Box Plot
- Sankey
- Graph/Network
### Migration Checklist per Chart:
- [ ] Create `controlPanelModern.tsx`
- [ ] Update plugin index to support both
- [ ] Write migration tests
- [ ] Test with feature flag
- [ ] Document any chart-specific patterns
- [ ] Update TypeScript types if needed
## Phase 6: System-Wide Updates
### 6.1 Update Control Panel Registry
```typescript
// getChartControlPanelRegistry.ts
export interface ModernControlPanelRegistry {
get(key: string): ControlPanelConfig | ReactControlPanelConfig;
registerModern(key: string, config: ReactControlPanelConfig): void;
}
```
### 6.2 Update Explore Components
- `ControlPanelsContainer` - Full support for modern panels ✅
- `Control` - Ensure all control types work
- `ControlRow` - Already modernized ✅
- `getSectionsToRender` - Update to handle React components
### 6.3 Update Types
```typescript
// types.ts
export type ControlPanelConfig = LegacyControlPanelConfig | ModernControlPanelConfig;
export interface ModernControlPanelConfig {
type: 'modern';
sections: ReactControlPanelSection[];
controlOverrides?: ControlOverrides;
formDataOverrides?: FormDataOverrides;
}
```
## Benefits Tracking
### Metrics to Monitor:
1. **Developer Velocity**: Time to add new controls
2. **Bug Rate**: Control panel-related issues
3. **Performance**: Rendering time for control panels
4. **Type Safety**: TypeScript coverage percentage
5. **Code Maintainability**: Lines of code, complexity metrics
### Expected Improvements:
- 50% reduction in control panel code
- 80% reduction in control panel bugs
- 100% TypeScript coverage
- 30% faster control panel rendering
- Easier onboarding for new developers
## Rollback Plan
If issues arise:
1. **Feature Flag**: Immediately disable `MODERN_CONTROL_PANELS`
2. **Hotfix**: Revert to legacy panel for affected charts
3. **Investigation**: Debug issues in staging environment
4. **Fix Forward**: Address issues and re-enable gradually
## Timeline Estimate
- **Phase 1**: ✅ Completed
- **Phase 2**: 1 week (testing and integration)
- **Phase 3**: 2 weeks (feature flag and rollout)
- **Phase 4**: 1 week (utilities and patterns)
- **Phase 5**: 3-6 months (all charts migration)
- **Phase 6**: 2 weeks (system updates)
- **Cleanup**: 1 week (remove legacy code)
**Total: 4-7 months for complete migration**
## Next Immediate Steps
1. Test the modern Pie control panel in development
2. Fix any issues with value binding and onChange handlers
3. Create feature flag in Python backend
4. Write comprehensive tests
5. Get team buy-in on approach
6. Start incremental migration
## Code Snippets for Testing
```bash
# Test the modern panel
cd superset-frontend
npm run dev
# In browser console
window.featureFlags = { MODERN_CONTROL_PANELS: true };
# Create a new Pie chart and verify controls work
```
## Success Criteria
- [ ] All control panels migrated to modern format
- [ ] No regression in functionality
- [ ] Improved developer experience
- [ ] Better performance metrics
- [ ] Reduced maintenance burden
- [ ] Full TypeScript coverage

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

@@ -32,7 +32,6 @@ assists people when migrating to a new version.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
## 5.0.0

View File

@@ -25,6 +25,12 @@
# - Volumes are isolated by project name (e.g., project1_db_home_light, project2_db_home_light)
# - Database name is intentionally different (superset_light) to prevent accidental cross-connections
#
# MCP Service (Model Context Protocol):
# - Optional service for LLM agent integration, available under 'mcp' profile
# - To include MCP: docker-compose -f docker-compose-light.yml --profile mcp up
# - MCP runs on port 5008 by default (customize with MCP_PORT=5009)
# - Enable SQL debugging with MCP_SQL_DEBUG=true
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# -----------------------------------------------------------------------
@@ -150,6 +156,37 @@ services:
required: false
volumes: *superset-volumes
superset-mcp-light:
profiles:
- mcp
build:
<<: *common-build
command: ["/app/docker/docker-bootstrap.sh", "mcp"]
restart: unless-stopped
ports:
- "127.0.0.1:${MCP_PORT:-5008}:5008" # Parameterized port
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user
depends_on:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
environment:
# Override DB connection for light service
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
# Enable SQL debugging for MCP if needed
SQLALCHEMY_DEBUG: ${MCP_SQL_DEBUG:-false}
volumes:
superset_home_light:
external: false

View File

@@ -78,6 +78,10 @@ case "${1}" in
echo "Starting web app..."
/usr/bin/run-server.sh
;;
mcp)
echo "Starting MCP service..."
superset mcp run --host 0.0.0.0 --port ${MCP_PORT:-5008} --debug
;;
*)
echo "Unknown Operation!!!"
;;

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

@@ -87,66 +87,8 @@ Restart Superset to apply changes.
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
## Custom Fonts
Superset supports custom fonts through runtime configuration, allowing you to use branded or custom typefaces without rebuilding the application.
### Configuring Custom Fonts
Add font URLs to your `superset_config.py`:
```python
# Load fonts from Google Fonts, Adobe Fonts, or self-hosted sources
CUSTOM_FONT_URLS = [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
]
# Update CSP to allow font sources
TALISMAN_CONFIG = {
"content_security_policy": {
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
}
}
```
### Using Custom Fonts in Themes
Once configured, reference the fonts in your theme configuration:
```python
THEME_DEFAULT = {
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace",
# ... other theme tokens
}
}
```
Or in the CRUD interface theme JSON:
```json
{
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace"
}
}
```
### Font Sources
- **Google Fonts**: Free, CDN-hosted fonts with wide variety
- **Adobe Fonts**: Premium fonts (requires subscription and kit ID)
- **Self-hosted**: Place font files in `/static/assets/fonts/` and reference via CSS
This feature works with the stock Docker image - no custom build required!
## Advanced Features
- **System Themes**: Superset includes built-in light and dark themes
- **Per-Dashboard Theming**: Each dashboard can have its own visual identity
- **JSON Editor**: Edit theme configurations directly within Superset's interface
- **Custom Fonts**: Load external fonts via configuration without rebuilding

View File

@@ -137,7 +137,7 @@ contributing to Apache Superset more accessible to developers worldwide.
1. **Create a Codespace**: Use this pre-configured link that sets up everything you need:
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=master&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=UsWest)
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=codespaces&geo=UsWest&devcontainer_path=.devcontainer%2Fdevcontainer.json)
:::caution
**Important**: You must select at least the **4 CPU / 16GB RAM** machine type (pre-selected in the link above).
@@ -421,6 +421,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

@@ -0,0 +1,741 @@
---
title: API Reference
sidebar_position: 3
version: 1
---
# MCP Tools API Reference
Complete reference for all 16 MCP tools with request/response examples.
> 🚀 **First time here?** Start with [Dashboard Tools](#dashboard-tools) or [Chart Tools](#chart-tools) to see the most commonly used features.
>
> 🔐 **Need authentication?** See the [Authentication Guide](./authentication) for JWT setup.
>
> 🔧 **Want to add tools?** Check the [Development Guide](./development#adding-new-tools) for step-by-step instructions.
## Dashboard Tools
### list_dashboards
List dashboards with search, filtering, and pagination support.
**Request Schema:**
```json
{
"search": "sales", // Optional: Search term
"filters": [ // Optional: Advanced filters
{
"col": "published",
"opr": "eq",
"value": true
}
],
"page": 1, // Optional: Page number (default: 1)
"page_size": 20, // Optional: Items per page (default: 20)
"select_columns": [ // Optional: Specific columns
"id", "dashboard_title", "uuid"
],
"use_cache": true // Optional: Use cached data (default: true)
}
```
**Response Example:**
```json
{
"dashboards": [
{
"id": 1,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dashboard_title": "Sales Performance",
"url": "/superset/dashboard/1/",
"published": true,
"owners": ["admin"],
"created_on": "2024-01-15T10:30:00Z",
"changed_on": "2024-01-20T14:15:00Z"
}
],
"total_count": 45,
"page": 1,
"page_size": 20,
"cache_status": {
"cache_hit": true,
"cache_age_seconds": 300
}
}
```
### get_dashboard_info
Get detailed information about a specific dashboard.
**Request Schema:**
```json
{
"identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // ID, UUID, or slug
"use_cache": true
}
```
**Response Example:**
```json
{
"dashboard_id": 1,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dashboard_title": "Sales Performance Dashboard",
"slug": "sales-performance",
"url": "/superset/dashboard/1/",
"published": true,
"owners": ["admin", "analyst"],
"roles": ["Sales Team"],
"charts": [
{
"id": 10,
"slice_name": "Monthly Revenue",
"viz_type": "line"
},
{
"id": 11,
"slice_name": "Regional Sales",
"viz_type": "bar"
}
],
"filters": [
{
"column": "region",
"type": "select"
}
],
"created_on": "2024-01-15T10:30:00Z",
"changed_on": "2024-01-20T14:15:00Z"
}
```
### generate_dashboard
Create a new dashboard with multiple charts.
**Request Schema:**
```json
{
"chart_ids": [10, 11, 12, 13],
"dashboard_title": "Q4 Performance Dashboard",
"description": "Quarterly performance metrics and KPIs",
"published": true,
"layout_type": "grid" // Optional: "grid" or "tabs"
}
```
**Response Example:**
```json
{
"dashboard_id": 25,
"uuid": "new-dash-uuid-here",
"dashboard_title": "Q4 Performance Dashboard",
"url": "/superset/dashboard/25/",
"charts_added": 4,
"layout": {
"type": "grid",
"columns": 2,
"rows": 2
},
"created_on": "2024-01-25T16:45:00Z"
}
```
## Chart Tools
### list_charts
List charts with advanced filtering and search capabilities.
**Request Schema:**
```json
{
"search": "revenue",
"filters": [
{
"col": "viz_type",
"opr": "in",
"value": ["line", "bar", "area"]
}
],
"page": 1,
"page_size": 25,
"select_columns": ["id", "slice_name", "viz_type", "uuid"],
"use_cache": true
}
```
**Response Example:**
```json
{
"charts": [
{
"id": 10,
"uuid": "chart-uuid-1",
"slice_name": "Monthly Revenue Trend",
"viz_type": "line",
"datasource_name": "sales_data",
"owners": ["admin"],
"created_on": "2024-01-10T09:15:00Z"
}
],
"total_count": 125,
"page": 1,
"page_size": 25
}
```
### get_chart_info
Get comprehensive chart information including configuration.
**Request Schema:**
```json
{
"identifier": 10, // ID or UUID
"include_form_data": true, // Include chart configuration
"use_cache": true
}
```
**Response Example:**
```json
{
"chart_id": 10,
"uuid": "chart-uuid-1",
"slice_name": "Monthly Revenue Trend",
"viz_type": "line",
"datasource_id": 5,
"datasource_name": "sales_data",
"datasource_type": "table",
"form_data": {
"viz_type": "line",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 12 months"
},
"query_context": {
"datasource": {"id": 5, "type": "table"},
"queries": [{"columns": [], "metrics": ["sum__revenue"]}]
},
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A10%7D",
"owners": ["admin"],
"created_on": "2024-01-10T09:15:00Z",
"changed_on": "2024-01-15T11:30:00Z"
}
```
### generate_chart
Create a new chart with specified configuration.
**Request Schema:**
```json
{
"dataset_id": "5",
"config": {
"chart_type": "xy",
"x": {"name": "month", "label": "Month"},
"y": [
{
"name": "revenue",
"aggregate": "SUM",
"label": "Total Revenue"
},
{
"name": "orders",
"aggregate": "COUNT",
"label": "Order Count"
}
],
"kind": "line",
"x_axis": {
"title": "Month",
"format": "smart_date"
},
"y_axis": {
"title": "Revenue ($)",
"format": "$,.0f"
},
"legend": {
"show": true,
"position": "top"
}
},
"slice_name": "Revenue and Orders Trend",
"description": "Monthly revenue and order count comparison",
"save_chart": true,
"generate_preview": true,
"preview_formats": ["url", "ascii"]
}
```
**Response Example:**
```json
{
"chart_id": 45,
"uuid": "new-chart-uuid",
"slice_name": "Revenue and Orders Trend",
"viz_type": "echarts_timeseries_line",
"datasource_id": 5,
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A45%7D",
"query_executed": true,
"query_result": {
"status": "success",
"row_count": 12,
"execution_time": 0.145
},
"preview": {
"url": {
"preview_url": "http://localhost:5008/screenshot/chart/45.png",
"width": 800,
"height": 600
},
"ascii": {
"ascii_content": "Revenue Trend\n==============\nJan |████████████████ $125K\nFeb |██████████████████ $140K\n...",
"width": 80,
"height": 20
}
},
"created_on": "2024-01-25T14:20:00Z"
}
```
### get_chart_data
Export chart data in multiple formats.
**Request Schema:**
```json
{
"identifier": 10,
"format": "json", // "json", "csv", "excel"
"limit": 1000, // Optional: Row limit
"offset": 0, // Optional: Row offset
"filters": [ // Optional: Additional filters
{
"column": "region",
"op": "=",
"value": "US"
}
],
"use_cache": true,
"force_refresh": false
}
```
**Response Example:**
```json
{
"data": [
{
"month": "2024-01",
"revenue": 125000,
"orders": 450
},
{
"month": "2024-02",
"revenue": 140000,
"orders": 520
}
],
"total_rows": 12,
"columns": [
{"name": "month", "type": "DATE"},
{"name": "revenue", "type": "BIGINT"},
{"name": "orders", "type": "BIGINT"}
],
"query": {
"sql": "SELECT month, SUM(revenue) as revenue, COUNT(*) as orders FROM sales_data GROUP BY month ORDER BY month",
"execution_time": 0.089
},
"cache_status": {
"cache_hit": false,
"cache_type": "query",
"refreshed": true
}
}
```
### get_chart_preview
Generate chart previews in multiple formats.
**Request Schema:**
```json
{
"identifier": 10,
"format": "url", // "url", "base64", "ascii", "table"
"width": 800, // For image formats
"height": 600, // For image formats
"ascii_width": 80, // For ASCII format
"ascii_height": 20, // For ASCII format
"use_cache": true
}
```
**Response Examples:**
**URL Format:**
```json
{
"format": "url",
"preview_url": "http://localhost:5008/screenshot/chart/10.png",
"width": 800,
"height": 600,
"supports_interaction": false,
"expires_at": "2024-01-26T14:20:00Z"
}
```
**ASCII Format:**
```json
{
"format": "ascii",
"ascii_content": "Monthly Revenue Trend\n=====================\n\nJan |████████████████████ $125K\nFeb |██████████████████████ $140K\nMar |███████████████████ $135K\nApr |█████████████████████████ $155K\n\nRange: $125K to $155K\n▁▃▂▅▇▆▄▃▂▄▅▆▇▅▃▂",
"width": 80,
"height": 20,
"supports_color": false
}
```
**Table Format:**
```json
{
"format": "table",
"table_data": "Monthly Revenue Data\n====================\n\nMonth | Revenue | Orders\n---------|----------|--------\nJan 2024 | $125,000 | 450\nFeb 2024 | $140,000 | 520\nMar 2024 | $135,000 | 495\n\nTotal: 12 rows × 3 columns",
"row_count": 12,
"supports_sorting": true
}
```
## Dataset Tools
### list_datasets
List available datasets with columns and metrics.
**Request Schema:**
```json
{
"search": "sales",
"filters": [
{
"col": "is_active",
"opr": "eq",
"value": true
}
],
"include_columns": true, // Include column metadata
"include_metrics": true, // Include metric metadata
"page": 1,
"page_size": 15
}
```
**Response Example:**
```json
{
"datasets": [
{
"id": 1,
"uuid": "dataset-uuid-1",
"table_name": "sales_data",
"database_name": "main_warehouse",
"schema": "public",
"owners": ["admin"],
"columns": [
{
"column_name": "region",
"type": "VARCHAR",
"is_active": true,
"is_dttm": false
},
{
"column_name": "revenue",
"type": "DECIMAL",
"is_active": true,
"is_dttm": false
}
],
"metrics": [
{
"metric_name": "sum__revenue",
"expression": "SUM(revenue)",
"metric_type": "sum"
}
],
"created_on": "2024-01-05T08:00:00Z"
}
],
"total_count": 23,
"page": 1,
"page_size": 15
}
```
### get_dataset_info
Get detailed dataset information with full column/metric metadata.
**Request Schema:**
```json
{
"identifier": "dataset-uuid-1", // ID or UUID
"include_columns": true,
"include_metrics": true,
"use_cache": true
}
```
**Response Example:**
```json
{
"dataset_id": 1,
"uuid": "dataset-uuid-1",
"table_name": "sales_data",
"database_name": "main_warehouse",
"database_id": 1,
"schema": "public",
"sql": null,
"is_active": true,
"owners": ["admin", "data_team"],
"columns": [
{
"id": 101,
"column_name": "region",
"type": "VARCHAR",
"is_active": true,
"is_dttm": false,
"groupby": true,
"filterable": true,
"description": "Geographic region"
},
{
"id": 102,
"column_name": "order_date",
"type": "DATE",
"is_active": true,
"is_dttm": true,
"groupby": true,
"filterable": true
}
],
"metrics": [
{
"id": 201,
"metric_name": "sum__revenue",
"expression": "SUM(revenue)",
"metric_type": "sum",
"is_active": true,
"description": "Total revenue"
}
],
"created_on": "2024-01-05T08:00:00Z",
"changed_on": "2024-01-18T12:30:00Z"
}
```
## System Tools
### get_superset_instance_info
Get Superset instance information and statistics.
**Request Schema:**
```json
{
"include_statistics": true, // Include usage statistics
"include_tools": true, // Include available MCP tools
"use_cache": true
}
```
**Response Example:**
```json
{
"version": "4.1.0",
"build": "apache-superset-4.1.0",
"mcp_service_version": "1.0.0",
"authentication": {
"enabled": true,
"type": "jwt_bearer",
"required_scopes": ["dashboard:read", "chart:read"]
},
"statistics": {
"dashboards": {
"total": 45,
"published": 32
},
"charts": {
"total": 125,
"by_viz_type": {
"line": 35,
"bar": 28,
"table": 42,
"pie": 20
}
},
"datasets": {
"total": 23,
"active": 18
},
"users": {
"total": 15,
"active": 12
}
},
"mcp_tools": [
{
"name": "list_dashboards",
"description": "List dashboards with search and filtering",
"category": "dashboard"
},
{
"name": "generate_chart",
"description": "Create new charts programmatically",
"category": "chart"
}
],
"database_connections": [
{
"id": 1,
"database_name": "main_warehouse",
"backend": "postgresql",
"status": "healthy"
}
],
"cache_status": {
"enabled": true,
"backend": "redis",
"hit_rate": 0.85
}
}
```
### generate_explore_link
Generate Superset explore URLs with pre-configured chart settings.
**Request Schema:**
```json
{
"dataset_id": "1",
"chart_config": {
"viz_type": "line",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 6 months"
},
"title": "Revenue Analysis",
"cache_form_data": true
}
```
**Response Example:**
```json
{
"explore_url": "/superset/explore/?form_data_key=abc123def456",
"full_url": "http://localhost:8088/superset/explore/?form_data_key=abc123def456",
"form_data_key": "abc123def456",
"expires_at": "2024-01-26T16:45:00Z",
"chart_config": {
"viz_type": "line",
"datasource": "1__table",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 6 months"
}
}
```
## SQL Lab Tools
### open_sql_lab_with_context
Open SQL Lab with pre-configured database, schema, and SQL.
**Request Schema:**
```json
{
"database_connection_id": 1,
"schema": "public",
"dataset_in_context": "sales_data",
"sql": "SELECT region, SUM(revenue) as total_revenue\nFROM sales_data \nWHERE order_date >= '2024-01-01'\nGROUP BY region\nORDER BY total_revenue DESC",
"title": "Regional Sales Analysis"
}
```
**Response Example:**
```json
{
"sql_lab_url": "/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
"full_url": "http://localhost:8088/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
"database_connection": {
"id": 1,
"database_name": "main_warehouse",
"backend": "postgresql"
},
"schema": "public",
"sql_template": "SELECT region, SUM(revenue) as total_revenue...",
"context": {
"dataset": "sales_data",
"title": "Regional Sales Analysis"
}
}
```
## Error Responses
All tools can return error responses with this structure:
```json
{
"error": "Chart not found with identifier: 999",
"error_type": "NotFound",
"suggestions": [
"Verify the chart ID exists",
"Check if you have permission to access this chart",
"Try using the chart UUID instead of ID"
],
"details": {
"identifier": 999,
"identifier_type": "id"
}
}
```
## Cache Status
Many responses include cache status information:
```json
{
"cache_status": {
"cache_hit": true, // Data served from cache
"cache_type": "query", // Type: query, metadata, form_data
"cache_age_seconds": 300, // Age of cached data
"refreshed": false // Whether cache was refreshed
}
}
```
This API reference provides complete documentation for integrating with the Superset MCP service, including all request schemas, response formats, and error handling patterns.
## What's Next?
### 🔐 **Ready for Production?**
Set up authentication and security with the [Authentication Guide](./authentication).
### 🔧 **Want to Add More Tools?**
Learn how to extend the MCP service in the [Development Guide](./development).
### 🏗️ **Need Architecture Details?**
Understand the system design in the [Architecture Overview](./architecture).
### 🏢 **Enterprise Features?**
Explore advanced capabilities in the [Preset Integration Guide](./preset-integration).
> 📖 **Back to Documentation Index**: [MCP Service](./intro)

View File

@@ -0,0 +1,191 @@
---
title: Architecture Overview
sidebar_position: 5
version: 1
---
# Architecture Overview
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP for LLM agents and automation tools.
**Status:** Phase 1 Complete. Core functionality stable, authentication production-ready. See [SIP-171](https://github.com/apache/superset/issues/33870) for roadmap.
## Core Architecture
### Tool Structure
- **16 MCP tools** organized by domain: `dashboard/`, `chart/`, `dataset/`, `system/`
- All tools decorated with `@mcp.tool` and `@mcp_auth_hook`
- **Import inside functions**: All Superset DAOs/commands imported in function body to ensure proper app context
- Pydantic v2 schemas with LLM/OpenAPI-compatible field descriptions
### Request Schema Pattern
Eliminates LLM parameter validation issues using structured request objects:
```python
# New approach - single request object
get_dataset_info(request={"identifier": 123}) # ID
get_dataset_info(request={"identifier": "uuid-string"}) # UUID
# Old approach - replaced
get_dataset_info(dataset_id=123)
```
### Multi-Identifier Support
- **Charts/Datasets**: ID (numeric) or UUID (string)
- **Dashboards**: ID (numeric), UUID (string), or slug (string)
- Validation prevents conflicting parameters (search + filters)
## Available Tools
### Dashboard Tools (5)
- `list_dashboards` - List with search/filters/pagination
- `get_dashboard_info` - Get by ID/UUID/slug
- `get_dashboard_available_filters` - Discover filterable columns
- `generate_dashboard` - Create dashboards with multiple charts
- `add_chart_to_existing_dashboard` - Add charts to existing dashboards
### Chart Tools (8)
- `list_charts` - List with search/filters/pagination
- `get_chart_info` - Get by ID/UUID
- `get_chart_available_filters` - Discover filterable columns
- `generate_chart` - Create charts (table, line, bar, area, scatter)
- `update_chart` - Update saved charts
- `update_chart_preview` - Update cached previews
- `get_chart_data` - Export data (JSON/CSV/Excel)
- `get_chart_preview` - Screenshots, ASCII art, table previews
### Dataset Tools (3)
- `list_datasets` - List with columns/metrics
- `get_dataset_info` - Get by ID/UUID with metadata
- `get_dataset_available_filters` - Discover filterable columns
### System Tools (2)
- `get_superset_instance_info` - Instance statistics and version
- `generate_explore_link` - Generate chart exploration URLs
### SQL Lab Tools (1)
- `open_sql_lab_with_context` - Pre-configured SQL Lab sessions
## Authentication & Security
### JWT Bearer Authentication
Production-ready authentication with configurable factory pattern:
```python
# In superset_config.py
MCP_AUTH_ENABLED = True
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
```
### Scope-Based Authorization
| Tool Category | Required Scope |
|---------------|----------------|
| Dashboard ops | `dashboard:read` |
| Chart ops | `chart:read` / `chart:write` |
| Dataset ops | `dataset:read` |
| System ops | `instance:read` |
### Audit Logging
All operations logged with MCP context:
- User impersonation tracking
- Tool execution details
- Sanitized payloads (sensitive data redacted)
## Cache Control
Leverages Superset's existing cache layers with comprehensive control:
### Cache Types
1. **Query Result Cache** - Database query results
2. **Metadata Cache** - Table schemas, columns, metrics
3. **Form Data Cache** - Chart configurations
4. **Dashboard Cache** - Rendered components
### Cache Parameters
Tools support cache control through request schemas:
- `use_cache`: Enable/disable caching (default: true)
- `force_refresh`: Force cache refresh (default: false)
- `cache_timeout`: Override timeout in seconds
- `refresh_metadata`: Force metadata refresh
### Cache Status Reporting
```json
{
"cache_status": {
"cache_hit": true,
"cache_type": "query",
"cache_age_seconds": 300,
"refreshed": false
}
}
```
## Tool Abstractions
### Generic Base Classes
- **ModelListTool**: Handles list/search/filter operations with pagination
- **ModelGetInfoTool**: Single object retrieval by multiple identifier types
- **ModelGetAvailableFiltersTool**: Returns filterable columns/operators
### Implementation Pattern
```python
@mcp.tool
@mcp_auth_hook
def my_tool(request: MyRequest) -> MyResponse:
# Import Superset modules inside function
from superset.daos.dashboard import DashboardDAO
from superset.commands.chart.create import CreateChartCommand
# Tool implementation
return response
```
## Configuration
### URL Configuration
Centralized URL management for consistent link generation:
```python
# In superset_config.py
SUPERSET_WEBSERVER_ADDRESS = "http://localhost:8088" # Development
SUPERSET_WEBSERVER_ADDRESS = "https://superset.company.com" # Production
```
### Schema Design Principles
- **Minimal columns** in list responses
- **Optional fields** in info schemas for missing data handling
- **Null exclusion** for cleaner JSON responses
- **Type safety** with clear Pydantic validation
## Adding New Tools
1. **Choose domain folder**: `dashboard/`, `chart/`, `dataset/`, or `system/`
2. **Define schemas**: Use Pydantic with field descriptions
3. **Implement tool**:
- Decorate with `@mcp.tool` and `@mcp_auth_hook`
- Import Superset modules inside function body
- Use generic abstractions where applicable
4. **Register**: Add to appropriate `__init__.py`
5. **Test**: Add unit tests in `tests/unit_tests/mcp_service/`
## Current Status
### ✅ Phase 1 Complete
- FastMCP server with CLI
- JWT authentication with RBAC
- All 16 core tools implemented
- Request schema pattern
- Cache control system
- Audit logging
- 194+ unit tests
### 🎯 Future Enhancements
- Demo notebooks and video examples
- OAuth integration for user impersonation
- Enhanced chart rendering formats
- Advanced security features
**Production Ready**: Core functionality stable with comprehensive testing and authentication.
---
For setup and usage, see the [MCP Service overview](./intro).

View File

@@ -0,0 +1,434 @@
---
title: Authentication & Security
sidebar_position: 4
version: 1
---
# Authentication & Security
The MCP service provides enterprise-grade JWT Bearer authentication with flexible configuration options and comprehensive security controls.
## Quick Start
### Development Mode (Default)
:::tip
Authentication is **disabled by default** for local development - no configuration needed.
:::
```bash
# No configuration needed - service runs without authentication
superset mcp run --port 5008 --debug
```
### Production Mode
:::warning
Always enable authentication for production deployments to secure your Superset instance.
:::
Enable JWT authentication in your Superset configuration:
```python
# In superset_config.py
MCP_AUTH_ENABLED = True
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read", "dataset:read"]
```
## Configuration Options
### Option 1: Simple Configuration
Add to your `superset_config.py`:
```python
# Enable authentication
MCP_AUTH_ENABLED = True
# JWT settings
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read"]
# Optional: User resolution
MCP_JWT_USER_CLAIM = "sub" # JWT claim for username (default: "sub")
MCP_JWT_EMAIL_CLAIM = "email" # JWT claim for email (default: "email")
MCP_FALLBACK_USER = "admin" # Fallback user if JWT user not found
```
### Option 2: Custom Factory
For advanced authentication requirements:
```python
def create_custom_mcp_auth(app):
"""Custom auth factory for enterprise environments."""
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
return BearerAuthProvider(
jwks_uri=app.config["MCP_JWKS_URI"],
issuer=app.config["MCP_JWT_ISSUER"],
audience=app.config["MCP_JWT_AUDIENCE"],
required_scopes=app.config.get("MCP_REQUIRED_SCOPES", []),
user_resolver=custom_user_resolver,
cache_ttl=300 # Cache JWKS for 5 minutes
)
MCP_AUTH_FACTORY = create_custom_mcp_auth
```
### Option 3: Environment Variables
For containerized deployments:
```bash
# Environment variables
export MCP_AUTH_ENABLED=true
export MCP_JWKS_URI=https://auth.company.com/.well-known/jwks.json
export MCP_JWT_ISSUER=https://auth.company.com/
export MCP_JWT_AUDIENCE=superset-mcp-api
export MCP_REQUIRED_SCOPES=dashboard:read,chart:read,dataset:read
```
## Identity Provider Integration
### Auth0
```python
# Auth0 configuration
MCP_JWKS_URI = "https://your-tenant.auth0.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://your-tenant.auth0.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
```
### Okta
```python
# Okta configuration
MCP_JWKS_URI = "https://your-org.okta.com/oauth2/default/v1/keys"
MCP_JWT_ISSUER = "https://your-org.okta.com/oauth2/default"
MCP_JWT_AUDIENCE = "api://superset-mcp"
```
### AWS Cognito
```python
# Cognito configuration
MCP_JWKS_URI = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}"
MCP_JWT_AUDIENCE = "your-app-client-id"
```
### Azure AD
```python
# Azure AD configuration
MCP_JWKS_URI = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
MCP_JWT_ISSUER = "https://login.microsoftonline.com/{tenant}/v2.0"
MCP_JWT_AUDIENCE = "api://superset-mcp"
```
## Scope-Based Authorization
### Standard Scopes
The MCP service defines these standard scopes:
| Scope | Description | Required For |
|-------|-------------|--------------|
| `dashboard:read` | Read dashboard information | `list_dashboards`, `get_dashboard_info` |
| `dashboard:write` | Create/modify dashboards | `generate_dashboard`, `add_chart_to_existing_dashboard` |
| `chart:read` | Read chart information | `list_charts`, `get_chart_info`, `get_chart_data` |
| `chart:write` | Create/modify charts | `generate_chart`, `update_chart` |
| `dataset:read` | Read dataset information | `list_datasets`, `get_dataset_info` |
| `instance:read` | Read instance information | `get_superset_instance_info` |
### Custom Scopes
Define custom scopes for specific use cases:
```python
# Custom scope definitions
CUSTOM_MCP_SCOPES = {
"analytics:export": "Export analytical data",
"reports:generate": "Generate automated reports",
"admin:config": "Access administrative configuration"
}
# Map tools to custom scopes
def get_custom_required_scopes(tool_name: str) -> List[str]:
scope_map = {
"get_chart_data": ["chart:read", "analytics:export"],
"generate_dashboard": ["dashboard:write", "reports:generate"],
"get_superset_instance_info": ["instance:read", "admin:config"]
}
return scope_map.get(tool_name, [])
MCP_SCOPE_RESOLVER = get_custom_required_scopes
```
## JWT Token Format
### Required Claims
Your JWT tokens must include these standard claims:
```json
{
"iss": "https://auth.company.com/", // Issuer
"aud": "superset-mcp-api", // Audience
"sub": "user@company.com", // Subject (username)
"exp": 1704118800, // Expiration timestamp
"iat": 1704115200, // Issued at timestamp
"scope": "dashboard:read chart:read" // Space-separated scopes
}
```
### Optional Claims
Additional claims for enhanced functionality:
```json
{
"email": "user@company.com", // User email
"name": "John Doe", // Full name
"groups": ["analysts", "sales_team"], // User groups
"tenant_id": "company_123", // Multi-tenant ID
"role": "analyst" // User role
}
```
## Client Integration
### API Client Usage
```python
import requests
# Get JWT token from your identity provider
token = get_jwt_token()
# Call MCP service with Bearer authentication
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(
"http://localhost:5008/call_tool",
headers=headers,
json={
"tool": "list_dashboards",
"arguments": {"search": "sales"}
}
)
data = response.json()
```
### Claude Desktop with Authentication
For Claude Desktop, the proxy script handles authentication:
```bash
#!/bin/bash
# run_proxy_with_auth.sh
# Get token from environment or file
if [ -f ~/.superset_mcp_token ]; then
TOKEN=$(cat ~/.superset_mcp_token)
else
TOKEN=${SUPERSET_MCP_TOKEN}
fi
# Export token for proxy
export MCP_AUTH_TOKEN="$TOKEN"
cd /path/to/superset
source venv/bin/activate
exec fastmcp proxy http://localhost:5008 --auth-header "Authorization: Bearer $TOKEN"
```
## User Resolution
### Default User Resolution
The service maps JWT claims to Superset users:
```python
def default_user_resolver(claims: Dict[str, Any]) -> User:
"""Default user resolution from JWT claims."""
# Extract username from configurable claim
username = claims.get(app.config.get("MCP_JWT_USER_CLAIM", "sub"))
# Find Superset user
user = security_manager.find_user(username=username)
if not user:
# Try email lookup
email = claims.get(app.config.get("MCP_JWT_EMAIL_CLAIM", "email"))
if email:
user = security_manager.find_user(email=email)
if not user and app.config.get("MCP_FALLBACK_USER"):
# Use fallback user for development
user = security_manager.find_user(username=app.config["MCP_FALLBACK_USER"])
return user
```
### Custom User Resolution
Implement custom user resolution logic:
```python
def custom_user_resolver(claims: Dict[str, Any]) -> User:
"""Custom user resolution for enterprise environments."""
# Extract custom claims
employee_id = claims.get("employee_id")
tenant_id = claims.get("tenant_id")
# Multi-tenant user lookup
user = find_user_by_employee_id(employee_id, tenant_id)
if user:
# Set additional context
user.mcp_tenant_id = tenant_id
user.mcp_groups = claims.get("groups", [])
return user
# Use custom resolver
MCP_USER_RESOLVER = custom_user_resolver
```
## Security Features
### Token Validation
Comprehensive JWT validation:
- **Signature verification**: RS256 with JWKS key rotation support
- **Expiration checking**: Automatic token expiry validation
- **Audience validation**: Prevents token reuse across services
- **Issuer validation**: Ensures tokens from trusted sources only
- **Scope validation**: Enforces tool-level permissions
### Request Security
- **HTTPS enforcement**: Production deployments should use HTTPS
- **Rate limiting**: Configurable per-user rate limits
- **Request logging**: All authenticated requests logged with user context
- **Input validation**: Comprehensive request schema validation
### Audit Logging
Every tool call is logged with security context:
```json
{
"timestamp": "2024-01-25T14:30:00Z",
"user_id": "user@company.com",
"tool_name": "get_chart_data",
"source": "mcp",
"jwt_subject": "user@company.com",
"jwt_scopes": ["chart:read", "analytics:export"],
"tenant_id": "company_123",
"request_id": "req_12345",
"execution_time": 0.145,
"status": "success"
}
```
## Testing Authentication
### Generate Test Tokens
For development and testing:
```python
from fastmcp.server.auth.providers.bearer import RSAKeyPair
# Generate test keypair
keypair = RSAKeyPair.generate()
print("Public key:", keypair.public_key)
# Create test token
token = keypair.create_token(
subject="test@example.com",
issuer="https://test.example.com",
audience="superset-mcp-api",
scopes=["dashboard:read", "chart:read", "dataset:read"],
expires_in=3600 # 1 hour
)
print("Test token:", token)
```
### Test Configuration
```python
# Test configuration with generated keypair
MCP_AUTH_ENABLED = True
MCP_JWT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
MCP_JWT_ISSUER = "https://test.example.com"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_FALLBACK_USER = "admin"
```
### Manual Testing
```bash
# Test with curl
curl -X POST http://localhost:5008/call_tool \
-H "Authorization: Bearer $TEST_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tool": "get_superset_instance_info",
"arguments": {"include_statistics": true}
}'
```
## Troubleshooting
### Common Issues
**Token Validation Errors:**
```
Error: Invalid JWT signature
Solution: Verify JWKS_URI is accessible and contains correct keys
```
**User Not Found:**
```
Error: User not found for JWT subject
Solution: Check MCP_JWT_USER_CLAIM configuration and user exists in Superset
```
**Insufficient Scopes:**
```
Error: Missing required scope 'chart:read'
Solution: Update JWT token to include required scopes
```
### Debug Configuration
Enable debug logging for authentication issues:
```python
# Enhanced logging for auth debugging
import logging
logging.getLogger('superset.mcp_service.auth').setLevel(logging.DEBUG)
# Log all JWT validation steps
MCP_AUTH_DEBUG = True
```
This authentication guide provides comprehensive coverage for securing the MCP service in production environments while maintaining development flexibility.

View File

@@ -0,0 +1,705 @@
---
title: Development Guide
sidebar_position: 2
version: 1
---
# MCP Service Development Guide
This guide covers the internal architecture, development workflows, and patterns for extending the Superset MCP service.
> 🚀 **New to MCP?** Start with the [Overview](./overview) to understand what the service does before diving into development.
>
> 📚 **Need API examples?** Check the [API Reference](./api-reference) to see how existing tools work.
>
> 🔐 **Planning production use?** Review [Authentication](./authentication) for security considerations.
## Internal Architecture
### Component Overview
The MCP service follows a layered architecture with clear separation of concerns:
```mermaid
graph TB
subgraph "Transport Layer"
HTTP[HTTP Server :5008]
FastMCP[FastMCP Protocol Handler]
end
subgraph "Auth & Middleware Layer"
AuthHook[Auth Hook Decorator]
JWT[JWT Validator]
RBAC[RBAC Engine]
Audit[Audit Logger]
end
subgraph "Tool Layer"
Tools[16 MCP Tools<br/>Tool Decorated]
Schemas[Pydantic Schemas]
Validation[Request Validation]
end
subgraph "Business Logic Layer"
Generic[Generic Tool Abstractions]
ModelList[ModelListTool]
ModelGet[ModelGetInfoTool]
ModelFilter[ModelGetAvailableFiltersTool]
end
subgraph "Data Access Layer"
DAOs[Superset DAOs]
Commands[Superset Commands]
Cache[Cache Manager]
end
subgraph "Storage Layer"
MetaDB[(Metadata DB)]
DataWH[(Data Warehouse)]
Redis[(Redis Cache)]
end
HTTP --> FastMCP
FastMCP --> AuthHook
AuthHook --> JWT
JWT --> RBAC
RBAC --> Audit
Audit --> Tools
Tools --> Schemas
Schemas --> Validation
Validation --> Generic
Generic --> ModelList
Generic --> ModelGet
Generic --> ModelFilter
ModelList --> DAOs
ModelGet --> DAOs
ModelFilter --> DAOs
Tools --> Commands
Commands --> Cache
DAOs --> MetaDB
Commands --> MetaDB
Commands --> DataWH
Cache --> Redis
```
### Request Flow
Every MCP tool call follows this execution pattern:
```mermaid
sequenceDiagram
participant Client as LLM Client
participant MCP as FastMCP Server
participant Auth as Auth Hook
participant Tool as MCP Tool
participant Generic as Generic Abstraction
participant DAO as Superset DAO
participant DB as Database
Client->>+MCP: tool_call(request)
MCP->>+Auth: validate_and_authorize()
Auth->>Auth: Validate JWT token
Auth->>Auth: Check required scopes
Auth->>Auth: Set Flask g.user context
Auth->>Auth: Log audit event
Auth->>+Tool: execute_tool(validated_request)
Tool->>Tool: Parse Pydantic request schema
Tool->>+Generic: Use generic abstraction
Generic->>+DAO: Query Superset data
DAO->>+DB: Execute SQL
DB-->>-DAO: Return results
DAO-->>-Generic: Return objects
Generic->>Generic: Apply pagination/filtering
Generic-->>-Tool: Return formatted data
Tool->>Tool: Build Pydantic response schema
Tool-->>-Auth: Return response
Auth->>Auth: Log success audit event
Auth-->>-MCP: Return validated response
MCP-->>-Client: JSON response
```
### Tool Registration System
Tools are automatically discovered and registered through the decorator pattern:
```python
# superset/mcp_service/mcp_app.py
from fastmcp import FastMCP
# Global MCP instance
mcp = FastMCP("Superset MCP Service")
# Tools register themselves via decorators
@mcp.tool
@mcp_auth_hook(['chart:read'])
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
# Tool implementation
pass
# All tool modules imported to trigger registration
from superset.mcp_service.chart.tool import *
from superset.mcp_service.dashboard.tool import *
from superset.mcp_service.dataset.tool import *
from superset.mcp_service.system.tool import *
```
## Development Patterns
### Tool Implementation Pattern
All tools follow this standardized pattern:
```python
# Example: superset/mcp_service/chart/tool/get_chart_info.py
from superset.mcp_service.auth import mcp_auth_hook
from superset.mcp_service.mcp_app import mcp
from superset.mcp_service.schemas.chart_schemas import (
GetChartInfoRequest,
GetChartInfoResponse,
ChartError
)
@mcp.tool
@mcp_auth_hook(['chart:read'])
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
"""
Get detailed information about a specific chart.
Supports lookup by ID or UUID with comprehensive metadata.
"""
try:
# CRITICAL: Import Superset modules inside function
from superset.daos.chart import ChartDAO
from superset.models.slice import Slice
# Use generic abstraction for common operations
from superset.mcp_service.generic_tools import ModelGetInfoTool
tool = ModelGetInfoTool(
dao=ChartDAO,
model=Slice,
response_schema=GetChartInfoResponse,
identifier_field_map={
'id': 'id',
'uuid': 'uuid'
}
)
return tool.execute(request)
except Exception as e:
return ChartError(
error=f"Failed to get chart info: {str(e)}",
error_type="ChartInfoError"
)
```
### Schema Design Patterns
Pydantic schemas follow these conventions:
```python
# Request Schema Pattern
class GetChartInfoRequest(BaseModel):
"""Request to get detailed chart information."""
identifier: Union[int, str] = Field(
...,
description="Chart ID (numeric) or UUID (string)"
)
include_form_data: bool = Field(
default=True,
description="Whether to include chart configuration"
)
use_cache: bool = Field(
default=True,
description="Whether to use cached data"
)
# Response Schema Pattern
class GetChartInfoResponse(BaseModel):
"""Detailed chart information response."""
chart_id: int = Field(description="Chart numeric ID")
uuid: Optional[str] = Field(description="Chart UUID")
slice_name: str = Field(description="Chart display name")
viz_type: str = Field(description="Visualization type")
datasource_id: Optional[int] = Field(description="Dataset ID")
form_data: Optional[Dict[str, Any]] = Field(description="Chart configuration")
explore_url: Optional[str] = Field(description="Explore URL for editing")
# Cache status for transparency
cache_status: Optional[CacheStatus] = Field(description="Cache hit information")
# Error Schema Pattern
class ChartError(BaseModel):
"""Chart operation error response."""
error: str = Field(description="Error message")
error_type: str = Field(description="Error type identifier")
suggestions: Optional[List[str]] = Field(description="Suggested fixes")
```
### Generic Tool Abstractions
Common operations are abstracted into reusable classes:
```python
# superset/mcp_service/generic_tools.py
from typing import Type, Dict, Any, List, Optional
from pydantic import BaseModel
class ModelListTool:
"""Generic tool for list operations with pagination and filtering."""
def __init__(self,
dao: Type,
model: Type,
response_schema: Type[BaseModel],
default_columns: List[str] = None,
searchable_columns: List[str] = None):
self.dao = dao
self.model = model
self.response_schema = response_schema
self.default_columns = default_columns or []
self.searchable_columns = searchable_columns or []
def execute(self, request: BaseModel) -> BaseModel:
"""Execute list operation with pagination and filtering."""
# Build query with filters
query = self.dao.find_all()
# Apply search if provided
if hasattr(request, 'search') and request.search:
query = self._apply_search(query, request.search)
# Apply filters if provided
if hasattr(request, 'filters') and request.filters:
query = self._apply_filters(query, request.filters)
# Apply pagination
total = query.count()
if hasattr(request, 'page') and hasattr(request, 'page_size'):
offset = (request.page - 1) * request.page_size
query = query.offset(offset).limit(request.page_size)
# Execute query and serialize
results = query.all()
serialized = [self._serialize_model(obj) for obj in results]
return self.response_schema(
results=serialized,
total_count=total,
page=getattr(request, 'page', 1),
page_size=getattr(request, 'page_size', len(serialized))
)
class ModelGetInfoTool:
"""Generic tool for getting single object by multiple identifier types."""
def __init__(self,
dao: Type,
model: Type,
response_schema: Type[BaseModel],
identifier_field_map: Dict[str, str]):
self.dao = dao
self.model = model
self.response_schema = response_schema
self.identifier_field_map = identifier_field_map
def execute(self, request: BaseModel) -> BaseModel:
"""Execute get operation with multi-identifier support."""
identifier = request.identifier
# Determine identifier type and field
if isinstance(identifier, int):
field = self.identifier_field_map.get('id', 'id')
obj = self.dao.find_by_id(identifier)
elif isinstance(identifier, str):
if len(identifier) == 36 and '-' in identifier: # UUID format
field = self.identifier_field_map.get('uuid', 'uuid')
obj = self.dao.find_by_uuid(identifier)
else: # Assume slug
field = self.identifier_field_map.get('slug', 'slug')
obj = getattr(self.dao, 'find_by_slug', lambda x: None)(identifier)
if not obj:
raise ValueError(f"Object not found with identifier: {identifier}")
# Serialize and return
serialized = self._serialize_model(obj)
return self.response_schema(**serialized)
```
## Adding New Tools
### Step-by-Step Process
1. **Define the Domain**
Choose the appropriate domain folder:
- `dashboard/` - Dashboard operations
- `chart/` - Chart operations
- `dataset/` - Dataset operations
- `system/` - System-level operations
2. **Create Schemas**
```bash
# Create schema file
touch superset/mcp_service/schemas/my_domain_schemas.py
```
```python
# Define request/response schemas
class MyToolRequest(BaseModel):
param1: str = Field(description="Parameter description")
param2: Optional[int] = Field(default=None, description="Optional parameter")
class MyToolResponse(BaseModel):
result: str = Field(description="Result description")
metadata: Dict[str, Any] = Field(description="Additional metadata")
```
3. **Implement the Tool**
```bash
# Create tool file
touch superset/mcp_service/my_domain/tool/my_tool.py
```
```python
@mcp.tool
@mcp_auth_hook(['required:scope'])
def my_tool(request: MyToolRequest) -> MyToolResponse:
"""Tool description for LLM."""
# Import Superset modules inside function
from superset.daos.my_dao import MyDAO
# Implement business logic
result = MyDAO.do_something(request.param1)
return MyToolResponse(
result=result,
metadata={"processed_at": datetime.utcnow()}
)
```
4. **Register the Tool**
```python
# Add to superset/mcp_service/my_domain/tool/__init__.py
from .my_tool import my_tool
__all__ = ['my_tool']
```
```python
# Import in superset/mcp_service/mcp_app.py
from superset.mcp_service.my_domain.tool import *
```
5. **Add Tests**
```bash
# Create test file
touch tests/unit_tests/mcp_service/test_my_tool.py
```
```python
import pytest
from superset.mcp_service.my_domain.tool.my_tool import my_tool
from superset.mcp_service.schemas.my_domain_schemas import MyToolRequest
class TestMyTool:
def test_my_tool_success(self):
request = MyToolRequest(param1="test")
response = my_tool(request)
assert response.result == "expected_result"
```
### Tool Best Practices
1. **Import Inside Functions**
```python
# ❌ DON'T: Import at module level
from superset.daos.chart import ChartDAO
@mcp.tool
def my_tool():
# Tool implementation
pass
# ✅ DO: Import inside function
@mcp.tool
def my_tool():
from superset.daos.chart import ChartDAO
# Tool implementation
pass
```
2. **Use Generic Abstractions**
```python
# ✅ Leverage existing patterns
@mcp.tool
def list_my_objects(request):
from superset.mcp_service.generic_tools import ModelListTool
tool = ModelListTool(
dao=MyDAO,
model=MyModel,
response_schema=ListMyObjectsResponse
)
return tool.execute(request)
```
3. **Comprehensive Error Handling**
```python
@mcp.tool
def my_tool(request):
try:
# Tool implementation
return success_response
except PermissionError as e:
return MyToolError(
error="Permission denied",
error_type="PermissionError",
suggestions=["Check user permissions"]
)
except Exception as e:
return MyToolError(
error=f"Unexpected error: {str(e)}",
error_type="InternalError"
)
```
## Testing Patterns
### Unit Test Structure
```python
# tests/unit_tests/mcp_service/test_chart_tools.py
import pytest
from unittest.mock import Mock, patch
from superset.mcp_service.chart.tool.get_chart_info import get_chart_info
from superset.mcp_service.schemas.chart_schemas import GetChartInfoRequest
class TestGetChartInfo:
"""Test suite for get_chart_info tool."""
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
def test_get_chart_info_by_id_success(self, mock_dao):
"""Test successful chart lookup by ID."""
# Setup mock
mock_chart = Mock()
mock_chart.id = 1
mock_chart.slice_name = "Test Chart"
mock_chart.viz_type = "line"
mock_dao.find_by_id.return_value = mock_chart
# Execute
request = GetChartInfoRequest(identifier=1)
response = get_chart_info(request)
# Verify
assert response.chart_id == 1
assert response.slice_name == "Test Chart"
mock_dao.find_by_id.assert_called_once_with(1)
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
def test_get_chart_info_not_found(self, mock_dao):
"""Test chart not found scenario."""
# Setup mock
mock_dao.find_by_id.return_value = None
# Execute
request = GetChartInfoRequest(identifier=999)
response = get_chart_info(request)
# Verify error response
assert hasattr(response, 'error')
assert "not found" in response.error.lower()
```
### Integration Test Patterns
```python
# tests/integration_tests/mcp_service/test_chart_integration.py
import pytest
from superset.app import create_app
from superset.mcp_service.mcp_app import mcp
from tests.integration_tests.base_tests import SupersetTestCase
class TestChartIntegration(SupersetTestCase):
"""Integration tests for chart tools."""
def setUp(self):
super().setUp()
self.app = create_app()
self.app_context = self.app.app_context()
self.app_context.push()
def tearDown(self):
self.app_context.pop()
super().tearDown()
def test_chart_workflow_integration(self):
"""Test complete chart workflow."""
# Create chart
create_request = {
"dataset_id": "1",
"config": {
"chart_type": "table",
"columns": [{"name": "region"}]
}
}
create_response = mcp.call_tool("generate_chart", create_request)
chart_id = create_response["chart_id"]
# Get chart info
info_request = {"identifier": chart_id}
info_response = mcp.call_tool("get_chart_info", info_request)
assert info_response["chart_id"] == chart_id
assert info_response["viz_type"] == "table"
# Get chart data
data_request = {"identifier": chart_id, "limit": 10}
data_response = mcp.call_tool("get_chart_data", data_request)
assert "data" in data_response
assert len(data_response["data"]) <= 10
```
## Performance Considerations
### Caching Strategy
The MCP service leverages Superset's existing cache layers:
```python
# Cache control in tools
@mcp.tool
def get_chart_data(request: GetChartDataRequest):
"""Tool with cache control."""
cache_config = {
'use_cache': request.use_cache,
'force_refresh': request.force_refresh,
'cache_timeout': request.cache_timeout
}
# Use Superset's cache infrastructure
result = execute_with_cache(query, cache_config)
return ChartDataResponse(
data=result.data,
cache_status=result.cache_status
)
```
### Query Optimization
```python
# Efficient pagination
def list_objects(query, page, page_size):
"""Optimized pagination pattern."""
# Count query optimization
total = query.count()
# Limit columns for list operations
query = query.options(load_only('id', 'name', 'created_on'))
# Apply pagination
offset = (page - 1) * page_size
results = query.offset(offset).limit(page_size).all()
return results, total
```
## Security Considerations
### Authentication Flow
```python
# JWT validation and user context
@mcp_auth_hook(['chart:read'])
def secure_tool(request):
"""Tool with proper security context."""
# g.user is set by auth hook
user_id = g.user.id
# Apply user-specific filtering
query = ChartDAO.find_all().filter(
Chart.owners.contains(g.user)
)
return execute_query(query)
```
### Input Validation
```python
# Comprehensive request validation
class CreateChartRequest(BaseModel):
"""Validated chart creation request."""
dataset_id: Union[int, str] = Field(
...,
description="Dataset ID or UUID"
)
config: ChartConfig = Field(
...,
description="Chart configuration"
)
@validator('dataset_id')
def validate_dataset_id(cls, v):
"""Validate dataset exists and user has access."""
# Validation logic
return v
@validator('config')
def validate_chart_config(cls, v):
"""Validate chart configuration."""
# Configuration validation
return v
```
This development guide provides comprehensive coverage of the MCP service's internal architecture and development patterns, enabling team members to effectively extend and maintain the system.
## Related Documentation
### 📚 **Ready to Use Your New Tools?**
Test your implementations with examples from the [API Reference](./api-reference).
### 🔐 **Securing Your Extensions?**
Add authentication to your tools using the [Authentication Guide](./authentication).
### 🏗️ **Understanding the Big Picture?**
See the complete system design in the [Architecture Overview](./architecture).
### 🏢 **Building Enterprise Features?**
Explore advanced patterns in the [Preset Integration Guide](./preset-integration).
> 📖 **Back to Documentation Index**: [MCP Service](./intro)

View File

@@ -0,0 +1,124 @@
---
title: MCP Service
sidebar_position: 1
version: 1
---
# Superset MCP Service
The Superset Model Context Protocol (MCP) service provides programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built for LLM agents and automation tools.
## What is MCP?
The Model Context Protocol (MCP) is an open standard that allows AI assistants to securely connect to data sources and tools. Superset's MCP service exposes **16 production-ready tools** that enable:
- 📊 **Data Exploration**: List and query dashboards, charts, and datasets
- 🔧 **Chart Creation**: Generate visualizations programmatically
- 📈 **Data Export**: Extract data in multiple formats (JSON, CSV, Excel)
- 🔗 **Navigation**: Generate explore links and SQL Lab sessions
## Quick Start
### Installation
:::note
The MCP service is included with Superset development setup. FastMCP dependencies are installed automatically with `make install`.
:::
```bash
# MCP service is included with Superset development setup
git clone https://github.com/apache/superset.git
cd superset
make venv && source venv/bin/activate
make install
# Start Superset
superset run -p 8088 --with-threads --reload --debugger
# Start MCP service (separate terminal)
source venv/bin/activate
superset mcp run --port 5008 --debug
```
### Claude Desktop Integration
```json
{
"mcpServers": {
"Superset MCP": {
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
"args": [],
"env": {}
}
}
}
```
## Key Features
### 🔧 **16 Production Tools**
| Category | Tools | Purpose |
|----------|-------|---------|
| **Dashboard** (5) | List, get info, create, add charts | Dashboard management |
| **Chart** (8) | Full CRUD, data export, previews | Chart operations |
| **Dataset** (3) | List, get info, discover filters | Dataset exploration |
| **System** (2) | Instance info, explore links | System integration |
| **SQL Lab** (1) | Pre-configured sessions | SQL development |
### 🔐 **Enterprise Security**
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
- **RBAC Integration**: Scope-based permissions with Superset's security model
- **Audit Logging**: Comprehensive MCP context tracking
### 📊 **Advanced Capabilities**
- **Multi-format Export**: JSON, CSV, Excel data export
- **Chart Previews**: Screenshots, ASCII art, table representations
- **Cache Control**: Leverage Superset's existing cache infrastructure
- **Request Schemas**: Eliminates LLM parameter validation issues
## Example Usage
```python
# List dashboards
dashboards = client.call_tool("list_dashboards", {
"search": "sales",
"page_size": 10
})
# Create a chart
chart = client.call_tool("generate_chart", {
"dataset_id": "1",
"config": {
"chart_type": "line",
"x": {"name": "date"},
"y": [{"name": "revenue", "aggregate": "SUM"}]
}
})
# Export chart data
data = client.call_tool("get_chart_data", {
"identifier": chart["chart_id"],
"format": "json",
"limit": 1000
})
```
## Status
✅ **Phase 1 Complete** - Core functionality stable, authentication production-ready, comprehensive testing coverage.
## Documentation Structure
### Getting Started
- **[Overview](./overview)** - Features, use cases, and examples
- **[API Reference](./api-reference)** - Complete tool documentation
### Development
- **[Development Guide](./development)** - Internal architecture and adding tools
- **[Architecture](./architecture)** - System design and patterns
### Production
- **[Authentication](./authentication)** - JWT setup and security
- **[Preset Integration](./preset-integration)** - Enterprise features
> 🚀 **Ready to start?** Continue with the [Overview](./overview) for detailed examples and use cases.

View File

@@ -0,0 +1,196 @@
---
title: MCP Service Overview
sidebar_position: 1
version: 1
---
# Superset MCP Service
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP, it's designed for LLM agents and automation tools.
**Status:** ✅ Phase 1 Complete. Core functionality stable, authentication production-ready, comprehensive testing coverage.
## What is MCP?
The Model Context Protocol (MCP) is an open standard for connecting AI assistants to data sources and tools. Superset's MCP service exposes 16 tools that allow LLM agents to:
- **Explore data**: List and query dashboards, charts, and datasets
- **Create visualizations**: Generate charts and dashboards programmatically
- **Export data**: Extract chart data in multiple formats
- **Navigate interfaces**: Generate explore links and SQL Lab sessions
## Key Features
### 🔧 **16 Production-Ready Tools**
- **Dashboard Tools (5)**: List, get info, create dashboards, add charts
- **Chart Tools (8)**: Full CRUD operations, data export, screenshot previews
- **Dataset Tools (3)**: List, get info, discover filterable columns
- **System Tools (2)**: Instance info, explore link generation
- **SQL Lab Tools (1)**: Pre-configured SQL sessions
### 🔐 **Enterprise Authentication**
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
- **RBAC Integration**: Scope-based permissions with Superset's security model
- **Audit Logging**: Comprehensive MCP context tracking with impersonation support
### 📊 **Advanced Capabilities**
- **Multi-format Export**: JSON, CSV, Excel data export
- **Chart Previews**: Screenshots, ASCII art, and table representations
- **Cache Control**: Comprehensive control over Superset's cache layers
- **Request Schema Pattern**: Eliminates LLM parameter validation issues
## Architecture Overview
```mermaid
graph TB
subgraph "Client Layer"
LLM[LLM/Agent Client]
Claude[Claude Desktop]
SDK[Custom SDK]
end
subgraph "MCP Service Layer"
FastMCP[FastMCP Server<br/>Port 5008]
Auth[JWT Auth Hook]
Tools[16 MCP Tools]
end
subgraph "Superset Integration"
DAOs[Superset DAOs]
Commands[Superset Commands]
Cache[Cache Layer]
end
subgraph "Data Layer"
DB[(Superset Database)]
DataWarehouse[(Data Warehouse)]
end
LLM --> FastMCP
Claude --> FastMCP
SDK --> FastMCP
FastMCP --> Auth
Auth --> Tools
Tools --> DAOs
Tools --> Commands
Tools --> Cache
DAOs --> DB
Commands --> DB
Commands --> DataWarehouse
```
## Getting Started
### Quick Setup
```bash
# Clone and install Superset
git clone https://github.com/apache/superset.git
cd superset
make venv && source venv/bin/activate
make install
# Start Superset
superset run -p 8088 --with-threads --reload --debugger
# Start MCP service (in separate terminal)
source venv/bin/activate
superset mcp run --port 5008 --debug
```
### Connect to Claude Desktop
:::note
The MCP service runs on HTTP and requires a proxy for Claude Desktop integration.
:::
```bash
# Install FastMCP proxy
pip install fastmcp
```
Configure Claude Desktop (`~/.config/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"Superset MCP": {
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
"args": [],
"env": {}
}
}
}
```
## Use Cases
### Data Exploration
- "List all dashboards related to sales"
- "Show me the charts in the Q4 Performance dashboard"
- "What datasets are available for customer analysis?"
### Chart Creation
- "Create a line chart showing revenue trends by month"
- "Generate a table showing top 10 products by sales"
- "Build a bar chart comparing regional performance"
### Data Export
- "Export the sales data from this chart as CSV"
- "Get the underlying data for this dashboard as JSON"
- "Show me a preview of this chart as ASCII art"
### Dashboard Management
- "Create a new dashboard with these 4 charts"
- "Add this revenue chart to the executive dashboard"
- "Generate an explore link for this chart configuration"
## Example Workflow
```python
# List available dashboards
dashboards = client.call_tool("list_dashboards", {
"search": "sales",
"page_size": 10
})
# Get detailed dashboard info
dashboard = client.call_tool("get_dashboard_info", {
"identifier": dashboards["dashboards"][0]["id"]
})
# Create a new chart
chart = client.call_tool("generate_chart", {
"dataset_id": "1",
"config": {
"chart_type": "line",
"x": {"name": "date"},
"y": [{"name": "revenue", "aggregate": "SUM"}]
}
})
# Export chart data
data = client.call_tool("get_chart_data", {
"identifier": chart["chart_id"],
"format": "json",
"limit": 1000
})
```
## Next Steps
### Ready to Use MCP?
- **[📚 API Reference](./api-reference)** - Try all 16 tools with request/response examples
- **[🔐 Authentication](./authentication)** - Set up JWT security for production use
### Want to Extend MCP?
- **[🔧 Development Guide](./development)** - Learn internal architecture and add new tools
- **[🏗️ Architecture](./architecture)** - Understand system design and deployment patterns
### Enterprise Deployment?
- **[🏢 Preset Integration](./preset-integration)** - RBAC extensions and OIDC integration for enterprise
> 💡 **Getting started?** Return to the [MCP Service intro](./intro) for a complete overview.

View File

@@ -0,0 +1,483 @@
---
title: Preset.io Integration
sidebar_position: 6
version: 1
---
# Preset.io Integration Guide
This document outlines integration points for the Preset.io team to extend the Superset MCP service with enterprise features, RBAC customizations, and OIDC integration.
## RBAC Extension Points
### Custom Authorization Factory
The MCP service supports custom authorization logic through the factory pattern:
```python
# In preset_config.py or superset_config.py
def create_preset_mcp_auth(app):
"""Custom auth factory for Preset.io environments."""
from superset.mcp_service.auth import create_auth_provider
from preset.auth.mcp import PresetMCPAuthProvider
return PresetMCPAuthProvider(
jwks_uri=app.config["PRESET_JWKS_URI"],
issuer=app.config["PRESET_JWT_ISSUER"],
audience=app.config["PRESET_JWT_AUDIENCE"],
tenant_resolver=preset_tenant_resolver,
rbac_manager=app.security_manager,
)
MCP_AUTH_FACTORY = create_preset_mcp_auth
```
### Multi-Tenant RBAC
Extend the base auth hook for tenant-aware permissions:
```python
# preset/mcp/auth.py
from superset.mcp_service.auth import mcp_auth_hook
from functools import wraps
def preset_tenant_auth_hook(required_permissions=None):
"""Preset-specific auth hook with tenant isolation."""
def decorator(func):
@wraps(func)
@mcp_auth_hook(required_permissions)
def wrapper(*args, **kwargs):
# Extract tenant from JWT claims
tenant_id = g.user.tenant_id if hasattr(g.user, 'tenant_id') else None
# Inject tenant context
g.mcp_tenant_id = tenant_id
g.mcp_tenant_context = get_tenant_context(tenant_id)
return func(*args, **kwargs)
return wrapper
return decorator
```
### Custom Permission Scopes
Define Preset-specific permission scopes:
```python
# preset/mcp/permissions.py
PRESET_MCP_SCOPES = {
# Tenant-level permissions
"tenant:admin": "Full tenant administration",
"tenant:read": "Read tenant resources",
# Workspace-level permissions
"workspace:admin": "Full workspace administration",
"workspace:read": "Read workspace resources",
# Enhanced dashboard permissions
"dashboard:publish": "Publish dashboards to marketplace",
"dashboard:embed": "Generate embed tokens",
# Enhanced chart permissions
"chart:export": "Export chart data and configs",
"chart:alerts": "Manage chart alerts and notifications",
# Dataset permissions with row-level security
"dataset:rls": "Apply row-level security filters",
"dataset:pii": "Access PII-flagged columns",
}
def get_preset_required_scopes(tool_name: str, context: dict = None) -> List[str]:
"""Map tool calls to Preset-specific permission requirements."""
base_scopes = get_base_required_scopes(tool_name)
# Add tenant-aware scopes
if context and context.get('tenant_id'):
base_scopes.append(f"tenant:{context['tenant_id']}")
# Add workspace-aware scopes
if context and context.get('workspace_id'):
base_scopes.append(f"workspace:{context['workspace_id']}")
return base_scopes
```
### Row-Level Security Integration
Extend data access tools with RLS:
```python
# preset/mcp/rls.py
def apply_preset_rls_filters(query_context: dict, user_context: dict) -> dict:
"""Apply Preset row-level security filters to query context."""
# Get user's RLS rules from Preset metadata
rls_rules = get_user_rls_rules(
user_id=user_context['user_id'],
tenant_id=user_context['tenant_id'],
workspace_id=user_context.get('workspace_id')
)
# Apply RLS filters to query
for rule in rls_rules:
if rule.applies_to_dataset(query_context['datasource']['id']):
query_context = rule.apply_filter(query_context)
return query_context
# Usage in custom tools
@mcp.tool
@preset_tenant_auth_hook(['dataset:read', 'dataset:rls'])
def preset_get_chart_data(request: GetChartDataRequest) -> ChartDataResponse:
"""Get chart data with Preset RLS applied."""
# Apply RLS before executing query
query_context = build_query_context(request)
query_context = apply_preset_rls_filters(
query_context,
{'user_id': g.user.id, 'tenant_id': g.mcp_tenant_id}
)
return execute_chart_data_query(query_context)
```
## OIDC Integration Points
### Preset OIDC Provider
Custom OIDC integration for Preset environments:
```python
# preset/mcp/oidc.py
from superset.mcp_service.auth.providers.bearer import BearerAuthProvider
import requests
from typing import Dict, Any
class PresetOIDCAuthProvider(BearerAuthProvider):
"""OIDC-specific auth provider for Preset.io."""
def __init__(self,
oidc_discovery_url: str,
client_id: str,
client_secret: str = None,
**kwargs):
# Discover OIDC endpoints
self.discovery_doc = self._fetch_discovery_document(oidc_discovery_url)
super().__init__(
jwks_uri=self.discovery_doc['jwks_uri'],
issuer=self.discovery_doc['issuer'],
**kwargs
)
self.client_id = client_id
self.client_secret = client_secret
def _fetch_discovery_document(self, discovery_url: str) -> Dict[str, Any]:
"""Fetch OIDC discovery document."""
response = requests.get(discovery_url)
response.raise_for_status()
return response.json()
def validate_token(self, token: str) -> Dict[str, Any]:
"""Validate JWT token with OIDC-specific claims."""
claims = super().validate_token(token)
# Validate OIDC-specific claims
if claims.get('aud') != self.client_id:
raise ValueError("Invalid audience claim")
# Extract Preset-specific claims
claims['preset_tenant_id'] = claims.get('tenant_id')
claims['preset_workspace_id'] = claims.get('workspace_id')
claims['preset_roles'] = claims.get('roles', [])
return claims
def resolve_user(self, claims: Dict[str, Any]) -> Any:
"""Resolve Superset user from OIDC claims."""
from preset.auth.user_resolver import resolve_preset_user
return resolve_preset_user(
subject=claims['sub'],
email=claims.get('email'),
tenant_id=claims.get('preset_tenant_id'),
roles=claims.get('preset_roles', [])
)
```
### Configuration for OIDC
```python
# In preset_config.py
def create_preset_oidc_auth(app):
"""Factory for Preset OIDC authentication."""
from preset.mcp.oidc import PresetOIDCAuthProvider
return PresetOIDCAuthProvider(
oidc_discovery_url=app.config["PRESET_OIDC_DISCOVERY_URL"],
client_id=app.config["PRESET_OIDC_CLIENT_ID"],
client_secret=app.config["PRESET_OIDC_CLIENT_SECRET"],
audience=app.config["PRESET_MCP_AUDIENCE"],
required_scopes=app.config.get("PRESET_MCP_REQUIRED_SCOPES", [])
)
# MCP Configuration
MCP_AUTH_ENABLED = True
MCP_AUTH_FACTORY = create_preset_oidc_auth
# OIDC Configuration
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
PRESET_OIDC_CLIENT_ID = "preset-mcp-service"
PRESET_OIDC_CLIENT_SECRET = os.environ.get("PRESET_OIDC_CLIENT_SECRET")
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
PRESET_MCP_REQUIRED_SCOPES = [
"openid", "profile", "email",
"superset:read", "superset:write"
]
```
## Preset-Specific Tools
### Tenant Management Tools
```python
# preset/mcp/tools/tenant.py
@mcp.tool
@preset_tenant_auth_hook(['tenant:read'])
def get_tenant_info(request: GetTenantInfoRequest) -> TenantInfoResponse:
"""Get Preset tenant information and quotas."""
tenant_id = g.mcp_tenant_id
tenant = get_tenant_by_id(tenant_id)
return TenantInfoResponse(
tenant_id=tenant.id,
name=tenant.name,
plan=tenant.plan,
quotas=tenant.quotas,
usage=get_tenant_usage(tenant_id),
workspaces=list_tenant_workspaces(tenant_id)
)
@mcp.tool
@preset_tenant_auth_hook(['workspace:read'])
def list_workspace_assets(request: ListWorkspaceAssetsRequest) -> ListWorkspaceAssetsResponse:
"""List all assets in a Preset workspace."""
workspace_id = request.workspace_id
tenant_id = g.mcp_tenant_id
# Validate workspace belongs to tenant
validate_workspace_access(workspace_id, tenant_id)
assets = {
'dashboards': list_workspace_dashboards(workspace_id),
'charts': list_workspace_charts(workspace_id),
'datasets': list_workspace_datasets(workspace_id)
}
return ListWorkspaceAssetsResponse(
workspace_id=workspace_id,
assets=assets,
total_count=sum(len(v) for v in assets.values())
)
```
### Embed Token Generation
```python
# preset/mcp/tools/embed.py
@mcp.tool
@preset_tenant_auth_hook(['dashboard:embed'])
def generate_embed_token(request: GenerateEmbedTokenRequest) -> EmbedTokenResponse:
"""Generate secure embed token for dashboard/chart."""
# Validate resource access
resource = validate_embed_resource_access(
resource_type=request.resource_type,
resource_id=request.resource_id,
tenant_id=g.mcp_tenant_id
)
# Generate signed embed token
embed_token = create_embed_token(
resource=resource,
user_id=g.user.id,
tenant_id=g.mcp_tenant_id,
permissions=request.permissions,
expiry=request.expiry_hours
)
return EmbedTokenResponse(
embed_token=embed_token,
embed_url=f"{get_preset_base_url()}/embed/{embed_token}",
expires_at=embed_token.expires_at
)
```
## Audit and Compliance Extensions
### Enhanced Audit Logging
```python
# preset/mcp/audit.py
from superset.mcp_service.auth import get_audit_context
def create_preset_audit_context(user_context: dict, tool_name: str,
request_data: dict) -> dict:
"""Create Preset-specific audit context."""
base_context = get_audit_context(user_context, tool_name, request_data)
# Add Preset-specific fields
preset_context = {
**base_context,
'tenant_id': user_context.get('tenant_id'),
'workspace_id': user_context.get('workspace_id'),
'preset_user_role': user_context.get('preset_role'),
'data_classification': classify_request_data(request_data),
'compliance_flags': get_compliance_flags(tool_name, request_data)
}
return preset_context
def log_preset_mcp_access(audit_context: dict):
"""Log MCP access to Preset audit systems."""
# Log to Superset's audit system
log_superset_audit_event(audit_context)
# Log to Preset's compliance system
log_preset_compliance_event(audit_context)
# Log to external SIEM if configured
if app.config.get('PRESET_SIEM_ENABLED'):
log_to_siem(audit_context)
```
### Data Classification
```python
# preset/mcp/classification.py
def classify_request_data(request_data: dict) -> dict:
"""Classify data sensitivity in MCP requests."""
classification = {
'contains_pii': False,
'data_level': 'public',
'retention_policy': 'standard'
}
# Check for PII in request
if contains_pii_fields(request_data):
classification['contains_pii'] = True
classification['data_level'] = 'restricted'
classification['retention_policy'] = 'pii_compliant'
# Check for sensitive datasets
if references_sensitive_datasets(request_data):
classification['data_level'] = 'confidential'
return classification
```
## Deployment Considerations
### Multi-Region Deployment
```python
# preset/mcp/deployment.py
def get_region_specific_config():
"""Get region-specific MCP configuration."""
region = os.environ.get('PRESET_REGION', 'us-east-1')
config_map = {
'us-east-1': {
'jwks_uri': 'https://auth-us.preset.io/.well-known/jwks.json',
'base_url': 'https://app.preset.io',
'data_residency': 'US'
},
'eu-west-1': {
'jwks_uri': 'https://auth-eu.preset.io/.well-known/jwks.json',
'base_url': 'https://eu.preset.io',
'data_residency': 'EU'
}
}
return config_map.get(region, config_map['us-east-1'])
# Usage in config
region_config = get_region_specific_config()
PRESET_JWKS_URI = region_config['jwks_uri']
SUPERSET_WEBSERVER_ADDRESS = region_config['base_url']
```
### Health Check Extensions
```python
# preset/mcp/health.py
@mcp.tool
def preset_health_check() -> HealthCheckResponse:
"""Preset-specific health check for MCP service."""
checks = {
'mcp_service': check_mcp_service_health(),
'database': check_database_health(),
'auth_provider': check_auth_provider_health(),
'tenant_isolation': check_tenant_isolation(),
'rls_engine': check_rls_engine_health()
}
overall_status = 'healthy' if all(
check['status'] == 'healthy' for check in checks.values()
) else 'degraded'
return HealthCheckResponse(
status=overall_status,
checks=checks,
region=os.environ.get('PRESET_REGION'),
version=get_preset_mcp_version()
)
```
## Configuration Templates
### Production Configuration
```python
# preset_production_config.py
from preset.mcp.auth import create_preset_oidc_auth
from preset.mcp.audit import create_preset_audit_context
# MCP Service Configuration
MCP_AUTH_ENABLED = True
MCP_AUTH_FACTORY = create_preset_oidc_auth
MCP_AUDIT_CONTEXT_FACTORY = create_preset_audit_context
# Preset OIDC Configuration
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
PRESET_OIDC_CLIENT_ID = "preset-mcp-production"
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
# Security Configuration
PRESET_MCP_REQUIRED_SCOPES = [
"openid", "profile", "email",
"tenant:read", "workspace:read",
"dashboard:read", "chart:read", "dataset:read"
]
# Audit Configuration
PRESET_AUDIT_ENABLED = True
PRESET_SIEM_ENABLED = True
PRESET_COMPLIANCE_MODE = "SOC2"
# Performance Configuration
PRESET_MCP_CACHE_ENABLED = True
PRESET_MCP_RATE_LIMIT = "1000/hour"
PRESET_MCP_TIMEOUT = 30
```
This integration guide provides the Preset.io team with concrete extension points for implementing enterprise features while maintaining compatibility with the base MCP service architecture.

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.31.0",
"eslint-config-prettier": "^10.1.8",
"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.37.0",
"webpack": "^5.101.0"
"webpack": "^5.99.9"
},
"browserslist": {
"production": [

View File

@@ -87,6 +87,16 @@ const sidebars = {
},
],
},
{
type: 'category',
label: 'MCP Service',
items: [
{
type: 'autogenerated',
dirName: 'mcp-service',
},
],
},
{
type: 'doc',
label: 'FAQ',

View File

@@ -2205,16 +2205,11 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.31.0":
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/js@^9.32.0":
version "9.32.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
@@ -2517,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"
@@ -3427,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"
@@ -3971,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"
@@ -3988,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==
@@ -4112,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"
@@ -4128,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"
@@ -4158,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"
@@ -4513,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==
@@ -4615,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==
@@ -5898,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"
@@ -5942,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"
@@ -6151,10 +6161,10 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^10.1.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.1:
version "5.5.1"
@@ -8053,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"
@@ -10704,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"
@@ -12090,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"
@@ -12515,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==
@@ -12784,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"
@@ -12818,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"

486
package-lock.json generated
View File

@@ -1,486 +0,0 @@
{
"name": "move-controls",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"glob": "^11.0.3"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"glob": "^11.0.3"
}
}

View File

@@ -133,6 +133,7 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = ["fastmcp>=2.8.1"]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
@@ -202,6 +203,7 @@ development = [
"pyinstrument>=4.0.2,<5",
"pylint",
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
"pytest-asyncio", # need this due to not using latest pytest
"pytest-cov",
"pytest-mock",
"python-ldap>=3.4.4",

View File

@@ -16,4 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,druid,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]

View File

@@ -10,6 +10,14 @@ amqp==5.3.1
# via
# -c requirements/base.txt
# kombu
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via
# httpx
# mcp
# sse-starlette
# starlette
apispec==6.6.1
# via
# -c requirements/base.txt
@@ -24,11 +32,14 @@ attrs==25.3.0
# via
# -c requirements/base.txt
# cattrs
# cyclopts
# jsonschema
# outcome
# referencing
# requests-cache
# trio
authlib==1.6.1
# via fastmcp
babel==2.17.0
# via
# -c requirements/base.txt
@@ -77,6 +88,8 @@ celery==5.5.2
certifi==2025.6.15
# via
# -c requirements/base.txt
# httpcore
# httpx
# requests
# selenium
cffi==1.17.1
@@ -101,6 +114,7 @@ click==8.2.1
# click-repl
# flask
# flask-appbuilder
# uvicorn
click-didyoumean==0.3.1
# via
# -c requirements/base.txt
@@ -140,10 +154,13 @@ cryptography==44.0.3
# via
# -c requirements/base.txt
# apache-superset
# authlib
# paramiko
# pyopenssl
cycler==0.12.1
# via matplotlib
cyclopts==3.22.2
# via fastmcp
db-dtypes==1.3.1
# via pandas-gbq
defusedxml==0.7.1
@@ -168,14 +185,23 @@ dnspython==2.7.0
# email-validator
docker==7.0.0
# via apache-superset
docstring-parser==0.17.0
# via cyclopts
docutils==0.21.2
# via rich-rst
email-validator==2.2.0
# via
# -c requirements/base.txt
# flask-appbuilder
# pydantic
et-xmlfile==2.0.0
# via
# -c requirements/base.txt
# openpyxl
exceptiongroup==1.3.0
# via fastmcp
fastmcp==2.10.6
# via apache-superset
filelock==3.12.2
# via virtualenv
flask==2.3.3
@@ -327,6 +353,8 @@ gunicorn==23.0.0
h11==0.16.0
# via
# -c requirements/base.txt
# httpcore
# uvicorn
# wsproto
hashids==1.3.1
# via
@@ -337,6 +365,14 @@ holidays==0.25
# -c requirements/base.txt
# apache-superset
# prophet
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# fastmcp
# mcp
httpx-sse==0.4.1
# via mcp
humanize==4.12.3
# via
# -c requirements/base.txt
@@ -346,7 +382,9 @@ identify==2.5.36
idna==3.10
# via
# -c requirements/base.txt
# anyio
# email-validator
# httpx
# requests
# trio
# url-normalize
@@ -378,6 +416,7 @@ jsonschema==4.23.0
# via
# -c requirements/base.txt
# flask-appbuilder
# mcp
# openapi-schema-validator
# openapi-spec-validator
jsonschema-path==0.3.4
@@ -437,6 +476,8 @@ matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mcp==1.12.0
# via fastmcp
mdurl==0.1.2
# via
# -c requirements/base.txt
@@ -475,6 +516,8 @@ odfpy==1.4.1
# via
# -c requirements/base.txt
# pandas
openapi-pydantic==0.5.1
# via fastmcp
openapi-schema-validator==0.6.3
# via
# -c requirements/base.txt
@@ -607,6 +650,16 @@ pycparser==2.22
# via
# -c requirements/base.txt
# cffi
pydantic==2.11.7
# via
# fastmcp
# mcp
# openapi-pydantic
# pydantic-settings
pydantic-core==2.33.2
# via pydantic
pydantic-settings==2.10.1
# via mcp
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -642,6 +695,8 @@ pyparsing==3.2.3
# -c requirements/base.txt
# apache-superset
# matplotlib
pyperclip==1.9.0
# via fastmcp
pysocks==1.7.1
# via
# -c requirements/base.txt
@@ -649,8 +704,11 @@ pysocks==1.7.1
pytest==7.4.4
# via
# apache-superset
# pytest-asyncio
# pytest-cov
# pytest-mock
pytest-asyncio==0.23.8
# via apache-superset
pytest-cov==6.0.0
# via apache-superset
pytest-mock==3.10.0
@@ -674,12 +732,16 @@ python-dotenv==1.1.0
# via
# -c requirements/base.txt
# apache-superset
# fastmcp
# pydantic-settings
python-geohash==0.8.5
# via
# -c requirements/base.txt
# apache-superset
python-ldap==3.4.4
# via apache-superset
python-multipart==0.0.20
# via mcp
pytz==2025.2
# via
# -c requirements/base.txt
@@ -734,7 +796,12 @@ rfc3339-validator==0.1.4
rich==13.9.4
# via
# -c requirements/base.txt
# cyclopts
# fastmcp
# flask-limiter
# rich-rst
rich-rst==1.3.1
# via cyclopts
rpds-py==0.25.0
# via
# -c requirements/base.txt
@@ -779,6 +846,7 @@ slack-sdk==3.35.0
sniffio==1.3.1
# via
# -c requirements/base.txt
# anyio
# trio
sortedcontainers==2.4.0
# via
@@ -808,10 +876,14 @@ sqlglot==27.3.0
# apache-superset
sqloxide==0.1.51
# via apache-superset
sse-starlette==2.4.1
# via mcp
sshtunnel==0.4.0
# via
# -c requirements/base.txt
# apache-superset
starlette==0.47.2
# via mcp
statsd==4.0.1
# via apache-superset
tabulate==0.9.0
@@ -839,13 +911,23 @@ typing-extensions==4.14.0
# via
# -c requirements/base.txt
# alembic
# anyio
# apache-superset
# cattrs
# exceptiongroup
# limits
# pydantic
# pydantic-core
# pyopenssl
# referencing
# selenium
# shillelagh
# starlette
# typing-inspection
typing-inspection==0.4.1
# via
# pydantic
# pydantic-settings
tzdata==2025.2
# via
# -c requirements/base.txt
@@ -864,6 +946,8 @@ urllib3==2.5.0
# requests
# requests-cache
# selenium
uvicorn==0.35.0
# via mcp
vine==5.1.0
# via
# -c requirements/base.txt

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

@@ -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) {
@@ -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);
});
@@ -602,7 +547,6 @@ describe('Drill by modal', () => {
describe('Tier 2 charts', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 2');
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -129,7 +129,7 @@
"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",
@@ -208,7 +208,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",
@@ -243,7 +243,7 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/classnames": "^2.3.4",
"@types/classnames": "^2.2.10",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^29.5.14",
"@types/js-levenshtein": "^1.1.3",
@@ -331,7 +331,7 @@
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.20.3",
"tsx": "^4.19.2",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",

View File

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

View File

@@ -1,72 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { styled } from '@superset-ui/core';
import { Collapse } from '@superset-ui/core/components';
const { Panel } = Collapse;
const StyledCollapse = styled(Collapse)`
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
border: none;
background: transparent;
.ant-collapse-item {
border: 1px solid ${({ theme }: any) => theme.colors.grayscale.light2};
border-radius: ${({ theme }: any) => theme.borderRadius}px;
margin-bottom: ${({ theme }: any) => theme.gridUnit * 2}px;
.ant-collapse-header {
font-weight: ${({ theme }: any) => theme.typography.weights.bold};
background: ${({ theme }: any) => theme.colors.grayscale.light5};
border-radius: ${({ theme }: any) => theme.borderRadius}px
${({ theme }: any) => theme.borderRadius}px 0 0;
}
.ant-collapse-content {
background: white;
padding: ${({ theme }: any) => theme.gridUnit * 3}px;
}
}
`;
export interface ControlPanelSectionProps {
title: string;
children: React.ReactNode;
expanded?: boolean;
}
export const ControlPanelSection: React.FC<ControlPanelSectionProps> = ({
title,
children,
expanded = false,
}) => {
const [activeKey, setActiveKey] = useState(expanded ? ['1'] : []);
return (
<StyledCollapse
activeKey={activeKey}
onChange={keys => setActiveKey(keys as string[])}
>
<Panel header={title} key="1">
{children}
</Panel>
</StyledCollapse>
);
};

View File

@@ -1,42 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { styled } from '@superset-ui/core';
const StyledRow = styled.div`
display: flex;
gap: ${({ theme }: any) => theme.gridUnit * 3}px;
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
& > * {
flex: 1;
}
.control-wrapper {
min-width: 0; // Allow flex items to shrink
}
`;
export interface ControlRowProps {
children: React.ReactNode;
}
export const ControlRow: React.FC<ControlRowProps> = ({ children }) => (
<StyledRow>{children}</StyledRow>
);

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 React from 'react';
import { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import AdhocFilterControlOriginal from 'src/explore/components/controls/FilterControl/AdhocFilterControl/index';
import { ControlHeader } from '../ControlHeader';
export interface AdhocFilterControlProps {
name: string;
value?: any[];
onChange: (value: any[]) => void;
label?: string;
description?: string;
required?: boolean;
renderTrigger?: boolean;
}
/**
* Wrapper around the existing AdhocFilterControl that simplifies its API
*/
export const AdhocFilterControl: React.FC<AdhocFilterControlProps> = ({
name,
value = [],
onChange,
label,
description,
required,
renderTrigger,
}) => {
// Get datasource from Redux state
const datasource = useSelector<ExplorePageState>(
state => state.explore.datasource,
) as any;
const columns = datasource?.columns || [];
const savedMetrics = datasource?.metrics || [];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<AdhocFilterControlOriginal
name={name}
value={value}
onChange={onChange}
columns={columns}
savedMetrics={savedMetrics}
datasource={datasource}
/>
</div>
);
};

View File

@@ -1,39 +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 { ReactElement } from 'react';
// @ts-ignore
import CheckboxControlComponent from '../../../../../../../src/explore/components/controls/CheckboxControl';
export interface CheckboxControlProps {
value?: boolean;
onChange: (value: boolean) => void;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
hovered?: boolean;
[key: string]: any;
}
/**
* Checkbox control component
*/
export const CheckboxControl: React.FC<CheckboxControlProps> = (
props,
): ReactElement => <CheckboxControlComponent {...props} />;

View File

@@ -1,68 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { getCategoricalSchemeRegistry } from '@superset-ui/core';
import { SelectControl } from './SimpleSelectControl';
export interface ColorSchemeControlProps {
name: string;
value?: string;
onChange: (value: string) => void;
label?: string;
description?: string;
required?: boolean;
renderTrigger?: boolean;
clearable?: boolean;
}
/**
* Color scheme selector control
*/
export const ColorSchemeControl: React.FC<ColorSchemeControlProps> = ({
name,
value,
onChange,
label,
description,
required,
renderTrigger,
clearable = false,
}) => {
// Get available color schemes from the categorical scheme registry
const colorSchemes = getCategoricalSchemeRegistry().keys();
const choices: [string, string][] = colorSchemes.map((scheme: string) => [
scheme,
scheme,
]);
return (
<SelectControl
name={name}
value={value}
onChange={onChange as (value: string | string[]) => void}
label={label}
description={description}
required={required}
renderTrigger={renderTrigger}
choices={choices}
clearable={clearable}
multiple={false}
/>
);
};

View File

@@ -1,38 +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 { ReactElement } from 'react';
// @ts-ignore
import ControlComponent from '../../../../../../../src/explore/components/Control';
export interface ControlProps {
type?: string;
name: string;
value?: any;
actions?: any;
formData?: any;
renderTrigger?: boolean;
[key: string]: any;
}
/**
* Generic control wrapper component
*/
export const Control: React.FC<ControlProps> = (props): ReactElement => (
<ControlComponent {...props} />
);

View File

@@ -1,94 +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 { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import { DndColumnSelect as DndColumnSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import { ControlHeader } from '../ControlHeader';
export interface DndColumnSelectProps {
name: string;
value?: any;
onChange: (value: any) => void;
label?: string;
description?: string;
multi?: boolean;
required?: boolean;
renderTrigger?: boolean;
canDelete?: boolean;
ghostButtonText?: string;
isTemporal?: boolean;
}
/**
* Wrapper around the existing DndColumnSelect that simplifies its API
*/
export const DndColumnSelect: React.FC<DndColumnSelectProps> = ({
name,
value,
onChange,
label,
description,
multi = false,
required,
renderTrigger,
canDelete = true,
ghostButtonText,
isTemporal,
}) => {
// Get columns from Redux state
const columns = useSelector<ExplorePageState>(
state => state.explore.datasource?.columns || [],
) as any[];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<DndColumnSelectControl
name={name}
value={value}
onChange={onChange}
options={columns}
multi={multi}
canDelete={canDelete}
ghostButtonText={
ghostButtonText || (multi ? 'Drop columns here' : 'Drop column here')
}
isTemporal={isTemporal}
type="DndColumnSelect"
actions={
{
setControlValue: (controlName: string, value: any) => ({
type: 'SET_CONTROL_VALUE',
controlName,
value,
validationErrors: undefined,
}),
} as any
}
/>
</div>
);
};

View File

@@ -1,83 +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 { ReactElement } from 'react';
// @ts-ignore
import { DndFilterSelect as DndFilterSelectControl } from '../../../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import { Datasource } from '../../types';
export interface DndFilterSelectProps {
value?: any[];
onChange: (value: any[]) => void;
datasource?: Datasource;
columns?: any[];
formData?: any;
savedMetrics?: any[];
selectedMetrics?: any[];
name?: string;
actions?: any;
type?: string;
[key: string]: any;
}
/**
* Wrapper around the existing DndFilterSelect that simplifies its API
*/
export const DndFilterSelect: React.FC<DndFilterSelectProps> = ({
value = [],
onChange,
datasource,
columns = [],
formData = {},
savedMetrics = [],
selectedMetrics = [],
name = 'adhoc_filters',
actions,
type = 'DndFilterSelect',
...restProps
}): ReactElement => {
// Handle the case where onChange needs to be wrapped for actions.setControlValue
const handleChange = (val: any) => {
if (actions?.setControlValue) {
actions.setControlValue(name, val);
} else if (onChange) {
onChange(val);
}
};
// For compatibility with the original component
const componentProps = {
value,
onChange: handleChange,
datasource,
columns,
formData,
name,
savedMetrics,
selectedMetrics,
type,
actions,
...restProps,
};
return (
<div className="filter-select-wrapper">
<DndFilterSelectControl {...componentProps} />
</div>
);
};

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 { useSelector } from 'react-redux';
import { ExplorePageState } from 'src/explore/types';
import { DndMetricSelect as DndMetricSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
import { ControlHeader } from '../ControlHeader';
export interface DndMetricSelectProps {
name: string;
value?: any;
onChange: (value: any) => void;
label?: string;
description?: string;
multi?: boolean;
required?: boolean;
renderTrigger?: boolean;
canDelete?: boolean;
ghostButtonText?: string;
}
/**
* Wrapper around the existing DndMetricSelect that simplifies its API
*/
export const DndMetricSelect: React.FC<DndMetricSelectProps> = ({
name,
value,
onChange,
label,
description,
multi = false,
required,
renderTrigger,
canDelete = true,
ghostButtonText,
}) => {
// Get datasource from Redux state
const datasource = useSelector<ExplorePageState>(
state => state.explore.datasource,
) as any;
const columns = datasource?.columns || [];
const savedMetrics = datasource?.metrics || [];
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<DndMetricSelectControl
name={name}
value={value}
onChange={onChange}
columns={columns}
savedMetrics={savedMetrics}
multi={multi}
canDelete={canDelete}
ghostButtonText={
ghostButtonText || (multi ? 'Drop metrics here' : 'Drop metric here')
}
type="DndMetricSelect"
actions={
{
setControlValue: (controlName: string, value: any) => ({
type: 'SET_CONTROL_VALUE',
controlName,
value,
validationErrors: undefined,
}),
} as any
}
/>
</div>
);
};

View File

@@ -1,43 +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 { ReactElement } from 'react';
// @ts-ignore
import SelectControlComponent from '../../../../../../../src/explore/components/controls/SelectControl';
export interface SelectControlProps {
value?: any;
onChange: (value: any) => void;
choices?: Array<[string | number, string]>;
clearable?: boolean;
multi?: boolean;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
hovered?: boolean;
placeholder?: string;
[key: string]: any;
}
/**
* Select control component
*/
export const SelectControl: React.FC<SelectControlProps> = (
props,
): ReactElement => <SelectControlComponent {...props} />;

View File

@@ -1,80 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Select } from '@superset-ui/core/components';
import { ControlHeader } from '../ControlHeader';
export interface SelectControlProps {
name: string;
value?: string | string[];
onChange: (value: string | string[]) => void;
label?: string;
description?: string;
placeholder?: string;
disabled?: boolean;
choices: Array<[string, string]>;
clearable?: boolean;
multiple?: boolean;
renderTrigger?: boolean;
required?: boolean;
}
export const SelectControl: React.FC<SelectControlProps> = ({
name,
value,
onChange,
label,
description,
placeholder,
disabled,
choices,
clearable = true,
multiple = false,
renderTrigger,
required,
}) => {
const options = choices.map(([val, label]) => ({
value: val,
label,
}));
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<Select
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
options={options}
allowClear={clearable}
mode={multiple ? 'multiple' : undefined}
css={{ width: '100%' }}
/>
</div>
);
};

View File

@@ -1,94 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { Input } from '@superset-ui/core/components';
import { ControlHeader } from '../ControlHeader';
export interface TextControlProps {
name: string;
value?: string | number;
onChange: (value: string | number) => void;
label?: string;
description?: string;
placeholder?: string;
disabled?: boolean;
isInt?: boolean;
isFloat?: boolean;
min?: number;
max?: number;
renderTrigger?: boolean;
required?: boolean;
}
export const TextControl: React.FC<TextControlProps> = ({
name,
value,
onChange,
label,
description,
placeholder,
disabled,
isInt,
isFloat,
min,
max,
renderTrigger,
required,
}) => {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
let newValue: string | number = e.target.value;
if (isInt) {
newValue = parseInt(newValue, 10) || 0;
} else if (isFloat) {
newValue = parseFloat(newValue) || 0;
}
onChange(newValue);
},
[onChange, isInt, isFloat],
);
const inputType = isInt || isFloat ? 'number' : 'text';
return (
<div className="control-wrapper">
{label && (
<ControlHeader
label={label}
description={description}
renderTrigger={renderTrigger}
required={required}
/>
)}
<Input
name={name}
type={inputType}
value={value ?? ''}
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
min={min}
max={max}
css={{ width: '100%' }}
/>
</div>
);
};

View File

@@ -1,41 +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 { ReactElement } from 'react';
// @ts-ignore
import SliderControlComponent from '../../../../../../../src/explore/components/controls/SliderControl';
export interface SliderControlProps {
value?: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
label?: string;
description?: string;
disabled?: boolean;
renderTrigger?: boolean;
[key: string]: any;
}
/**
* Slider control component
*/
export const SliderControl: React.FC<SliderControlProps> = (
props,
): ReactElement => <SliderControlComponent {...props} />;

View File

@@ -1,39 +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 { ReactElement } from 'react';
// @ts-ignore
import TextControlComponent from '../../../../../../../src/explore/components/controls/TextControl';
export interface TextControlProps {
value?: string | number;
onChange: (value: string | number) => void;
placeholder?: string;
isInt?: boolean;
isFloat?: boolean;
disabled?: boolean;
controlId?: string;
[key: string]: any;
}
/**
* Text input control component
*/
export const TextControl: React.FC<TextControlProps> = (
props,
): ReactElement => <TextControlComponent {...props} />;

View File

@@ -1,39 +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.
*/
// Layout components
export { ControlPanelSection } from './ControlPanelSection';
export { ControlRow } from './ControlRow';
export { ControlHeader } from './ControlHeader';
// Control components
export { TextControl as SimpleTextControl } from './controls/SimpleTextControl';
export { SelectControl as SimpleSelectControl } from './controls/SimpleSelectControl';
// Wrapper controls for new simplified API
export { DndColumnSelect } from './controls/DndColumnSelectWrapper';
export { DndMetricSelect } from './controls/DndMetricSelectWrapper';
export { DndFilterSelect } from './controls/DndFilterSelectWrapper';
export { AdhocFilterControl } from './controls/AdhocFilterControlWrapper';
export { ColorSchemeControl as SimpleColorSchemeControl } from './controls/ColorSchemeControlWrapper';
export { TextControl } from './controls/TextControlWrapper';
export { CheckboxControl } from './controls/CheckboxControlWrapper';
export { SelectControl } from './controls/SelectControlWrapper';
export { SliderControl } from './controls/SliderControlWrapper';
export { Control } from './controls/ControlWrapper';

View File

@@ -1,81 +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 { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { QueryFormData } from '@superset-ui/core';
// Define minimal types to avoid circular dependencies
interface ExploreState {
form_data: QueryFormData;
[key: string]: any;
}
interface RootState {
explore: ExploreState;
[key: string]: any;
}
/**
* Hook to access and update form data from Redux store
* Provides a simple interface for control components
*
* NOTE: This hook assumes the Redux store structure used by Superset's explore view.
* The actual setControlValue action should be provided by the app via context or props.
*/
export function useFormData() {
const dispatch = useDispatch();
// Get form data from Redux
const formData = useSelector<RootState, QueryFormData>(state => {
console.log('useFormData - state.explore:', state.explore);
return state.explore?.form_data || {};
});
// Update a single control value
// This is a placeholder - the actual action should be injected
const updateControl = useCallback(
(controlName: string, value: any) => {
console.log('updateControl:', controlName, value);
// In production, this would dispatch the actual setControlValue action
// For now, we'll dispatch a generic action
dispatch({
type: 'SET_CONTROL_VALUE',
controlName,
value,
});
},
[dispatch],
);
// Update multiple controls at once
const updateControls = useCallback(
(updates: Partial<QueryFormData>) => {
Object.entries(updates).forEach(([key, value]) => {
updateControl(key, value);
});
},
[updateControl],
);
return {
formData,
updateControl,
updateControls,
};
}

View File

@@ -21,7 +21,6 @@ import * as sectionsModule from './sections';
export * from './utils';
export * from './constants';
export * from './operators';
export * from './hooks/useFormData';
// can't do `export * as sections from './sections'`, babel-transformer will fail
export const sections = sectionsModule;
@@ -33,26 +32,7 @@ export * from './components/Dropdown';
export * from './components/Menu';
export * from './components/MetricOption';
export * from './components/ControlHeader';
export * from './components';
// Export individual control components for easier access
export {
DndColumnSelect,
DndMetricSelect,
DndFilterSelect,
TextControl,
CheckboxControl,
SelectControl,
SliderControl,
Control,
} from './components';
export * from './shared-controls';
export {
GranularityControl,
RadioButtonControl,
ReactControlPanel,
} from './shared-controls/components';
// Export all from shared-controls/components which includes inline control functions
export * from './shared-controls/components';
export * from './types';
export * from './fixtures';

View File

@@ -18,20 +18,6 @@
*/
import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig, ControlSetRow } from '../types';
import {
AdhocFiltersControl,
GroupByControl,
GroupOthersWhenLimitReachedControl,
LimitControl,
MetricsControl,
OrderDescControl,
RowLimitControl,
ShowEmptyColumnsControl,
TimeGrainSqlaControl,
TimeLimitMetricControl,
TruncateMetricControl,
XAxisControl,
} from '../shared-controls/components/SharedControlComponents';
import {
contributionModeControl,
xAxisForceCategoricalControl,
@@ -40,34 +26,30 @@ import {
} from '../shared-controls';
const controlsWithoutXAxis: ControlSetRow[] = [
[MetricsControl()],
[GroupByControl()],
['metrics'],
['groupby'],
[contributionModeControl],
[AdhocFiltersControl()],
[LimitControl(), GroupOthersWhenLimitReachedControl()],
[TimeLimitMetricControl()],
[OrderDescControl()],
[RowLimitControl()],
[TruncateMetricControl()],
[ShowEmptyColumnsControl()],
['adhoc_filters'],
['limit', 'group_others_when_limit_reached'],
['timeseries_limit_metric'],
['order_desc'],
['row_limit'],
['truncate_metric'],
['show_empty_columns'],
];
export const echartsTimeSeriesQuery: ControlPanelSectionConfig = {
label: t('Query'),
expanded: true,
controlSetRows: [
[XAxisControl()],
[TimeGrainSqlaControl()],
...controlsWithoutXAxis,
],
controlSetRows: [['x_axis'], ['time_grain_sqla'], ...controlsWithoutXAxis],
};
export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
label: t('Query'),
expanded: true,
controlSetRows: [
[XAxisControl()],
[TimeGrainSqlaControl()],
['x_axis'],
['time_grain_sqla'],
[xAxisForceCategoricalControl],
[xAxisSortControl],
[xAxisSortAscControl],

View File

@@ -18,15 +18,6 @@
*/
import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig } from '../types';
import {
GranularityControl,
GranularitySqlaControl,
TimeGrainSqlaControl,
TimeRangeControl,
DatasourceControl,
VizTypeControl,
ColorSchemeControl,
} from '../shared-controls/components/SharedControlComponents';
// A few standard controls sections that are used internally.
// Not recommended for use in third-party plugins.
@@ -40,10 +31,10 @@ const baseTimeSection = {
export const legacyTimeseriesTime: ControlPanelSectionConfig = {
...baseTimeSection,
controlSetRows: [
[GranularityControl()],
[GranularitySqlaControl()],
[TimeGrainSqlaControl()],
[TimeRangeControl()],
['granularity'],
['granularity_sqla'],
['time_grain_sqla'],
['time_range'],
],
};
@@ -51,8 +42,8 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
label: t('Datasource & Chart Type'),
expanded: true,
controlSetRows: [
[DatasourceControl()],
[VizTypeControl()],
['datasource'],
['viz_type'],
[
{
name: 'slice_id',
@@ -100,7 +91,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
export const colorScheme: ControlPanelSectionConfig = {
label: t('Color Scheme'),
controlSetRows: [[ColorSchemeControl()]],
controlSetRows: [['color_scheme']],
};
export const annotations: ControlPanelSectionConfig = {

View File

@@ -1,252 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Input, Select, Switch, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface AxisControlSectionProps {
axis: 'x' | 'y';
showTitle?: boolean;
showFormat?: boolean;
showRotation?: boolean;
showBounds?: boolean;
showLogarithmic?: boolean;
showMinorTicks?: boolean;
showTruncate?: boolean;
timeFormat?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const D3_FORMAT_OPTIONS = [
['SMART_NUMBER', t('Adaptive formatting')],
['~g', t('Original value')],
['d', t('Signed integer')],
['.1f', t('1 decimal place')],
['.2f', t('2 decimal places')],
['.3f', t('3 decimal places')],
['+,', t('Positive integer')],
['$,.2f', t('Currency (2 decimals)')],
[',.0%', t('Percentage')],
['.1%', t('Percentage (1 decimal)')],
];
const D3_TIME_FORMAT_OPTIONS = [
['smart_date', t('Adaptive formatting')],
['%Y-%m-%d', t('2023-01-01')],
['%Y-%m-%d %H:%M', t('2023-01-01 10:30')],
['%m/%d/%Y', t('01/01/2023')],
['%d/%m/%Y', t('01/01/2023')],
['%Y', t('2023')],
['%B %Y', t('January 2023')],
['%b %Y', t('Jan 2023')],
['%B %-d, %Y', t('January 1, 2023')],
];
const ROTATION_OPTIONS = [
[0, '0°'],
[45, '45°'],
[90, '90°'],
[-45, '-45°'],
[-90, '-90°'],
];
export const AxisControlSection: FC<AxisControlSectionProps> = ({
axis,
showTitle = true,
showFormat = true,
showRotation = false,
showBounds = false,
showLogarithmic = false,
showMinorTicks = false,
showTruncate = false,
timeFormat = false,
values = {},
onChange = () => {},
}) => {
const isXAxis = axis === 'x';
const axisUpper = axis.toUpperCase();
const titleKey = `${axis}_axis_title`;
const formatKey = timeFormat
? `${axis}_axis_time_format`
: `${axis}_axis_format`;
const rotationKey = `${axis}_axis_label_rotation`;
const boundsMinKey = `${axis}_axis_bounds_min`;
const boundsMaxKey = `${axis}_axis_bounds_max`;
const logScaleKey = `log_scale`;
const minorTicksKey = `${axis}_axis_minor_ticks`;
const truncateKey = `truncate_${axis}axis`;
const truncateLabelsKey = `${axis}_axis_truncate_labels`;
return (
<div className="axis-control-section">
{showTitle && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Title`)}</label>
<Input
value={values[titleKey] || ''}
onChange={e => onChange(titleKey, e.target.value)}
placeholder={t(`Enter ${axis} axis title`)}
/>
<small className="text-muted">
{t(
'Overrides the axis title derived from the metric or column name',
)}
</small>
</Col>
</Row>
)}
{showFormat && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Format`)}</label>
<Select
value={
values[formatKey] ||
(timeFormat ? 'smart_date' : 'SMART_NUMBER')
}
onChange={value => onChange(formatKey, value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a format')}
options={(timeFormat
? D3_TIME_FORMAT_OPTIONS
: D3_FORMAT_OPTIONS
).map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{timeFormat
? t('D3 time format for x axis')
: t('D3 format for axis values')}
</small>
</Col>
</Row>
)}
{showRotation && isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Rotation')}</label>
<Select
value={values[rotationKey] || 0}
onChange={value => onChange(rotationKey, value)}
style={{ width: '100%' }}
options={ROTATION_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Rotation angle for axis labels')}
</small>
</Col>
</Row>
)}
{showBounds && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t(`${axisUpper} Axis Bounds`)}</label>
<div style={{ display: 'flex', gap: 8 }}>
<InputNumber
value={values[boundsMinKey]}
onChange={value => onChange(boundsMinKey, value)}
placeholder={t('Min')}
style={{ flex: 1 }}
/>
<InputNumber
value={values[boundsMaxKey]}
onChange={value => onChange(boundsMaxKey, value)}
placeholder={t('Max')}
style={{ flex: 1 }}
/>
</div>
<small className="text-muted">
{t('Bounds for axis values. Leave empty for automatic scaling.')}
</small>
</Col>
</Row>
)}
{showLogarithmic && !isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values[logScaleKey] || false}
onChange={checked => onChange(logScaleKey, checked)}
/>
{t('Logarithmic Scale')}
</label>
<small className="text-muted">
{t('Use a logarithmic scale for the Y-axis')}
</small>
</Col>
</Row>
)}
{showMinorTicks && !isXAxis && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values[minorTicksKey] || false}
onChange={checked => onChange(minorTicksKey, checked)}
/>
{t('Show Minor Ticks')}
</label>
<small className="text-muted">
{t('Show minor grid lines on the axis')}
</small>
</Col>
</Row>
)}
{showTruncate && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={
values[truncateKey] || values[truncateLabelsKey] || false
}
onChange={checked => {
onChange(truncateKey, checked);
onChange(truncateLabelsKey, checked);
}}
/>
{t(`Truncate ${axisUpper} Axis Labels`)}
</label>
<small className="text-muted">
{t('Truncate long axis labels to prevent overlap')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default AxisControlSection;

View File

@@ -1,634 +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 { ReactElement } from 'react';
import type { CustomControlItem, ControlValueValidator } from '../../types';
// Base control props that all controls share
interface BaseControlProps {
name: string;
label?: ReactElement | string;
description?: string;
default?: any;
renderTrigger?: boolean;
validators?: ControlValueValidator[];
warning?: string;
error?: string;
mapStateToProps?: (state: any, control: any) => any;
visibility?: (props: any) => boolean;
value?: any;
onChange?: (value: any) => void;
}
// Use the existing CustomControlItem type instead of creating a duplicate
// This ensures type compatibility with the rest of the codebase
export type ControlComponentConfig = CustomControlItem;
// CheckboxControl Component
interface CheckboxControlProps extends BaseControlProps {
default?: boolean;
}
export const CheckboxControl = (
props: CheckboxControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'CheckboxControl',
label: props.label,
description: props.description,
default: props.default ?? false,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SelectControl Component
interface SelectControlProps extends BaseControlProps {
choices?: Array<[string, string]> | (() => Array<[string, string]>);
clearable?: boolean;
freeForm?: boolean;
multi?: boolean;
placeholder?: string;
optionRenderer?: (option: any) => ReactElement;
valueRenderer?: (value: any) => ReactElement;
valueKey?: string;
labelKey?: string;
}
export const SelectControl = (
props: SelectControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectControl',
label: props.label,
description: props.description,
choices: props.choices ?? [],
clearable: props.clearable ?? true,
freeForm: props.freeForm ?? false,
multi: props.multi ?? false,
default: props.default,
renderTrigger: props.renderTrigger ?? false,
placeholder: props.placeholder,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
validators: props.validators,
warning: props.warning,
error: props.error,
optionRenderer: props.optionRenderer,
valueRenderer: props.valueRenderer,
valueKey: props.valueKey,
labelKey: props.labelKey,
},
});
// TextControl Component
interface TextControlProps extends BaseControlProps {
placeholder?: string;
disabled?: boolean;
isInt?: boolean;
isFloat?: boolean;
}
export const TextControl = (
props: TextControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
default: props.default ?? '',
renderTrigger: props.renderTrigger ?? false,
disabled: props.disabled ?? false,
isInt: props.isInt ?? false,
isFloat: props.isFloat ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// TextAreaControl Component
interface TextAreaControlProps extends BaseControlProps {
placeholder?: string;
rows?: number;
language?: 'json' | 'html' | 'sql' | 'markdown' | 'javascript';
offerEditInModal?: boolean;
disabled?: boolean;
}
export const TextAreaControl = (
props: TextAreaControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextAreaControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
rows: props.rows ?? 3,
language: props.language,
offerEditInModal: props.offerEditInModal ?? true,
default: props.default ?? '',
renderTrigger: props.renderTrigger ?? false,
disabled: props.disabled ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SliderControl Component
interface SliderControlProps extends BaseControlProps {
min?: number;
max?: number;
step?: number;
default?: number;
}
export const SliderControl = (
props: SliderControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SliderControl',
label: props.label,
description: props.description,
min: props.min ?? 0,
max: props.max ?? 100,
step: props.step ?? 1,
default: props.default ?? 0,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// RadioButtonControl Component
interface RadioButtonControlProps extends BaseControlProps {
options?: Array<[string, string | ReactElement]>;
default?: string;
}
export const RadioButtonControl = (
props: RadioButtonControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'RadioButtonControl',
label: props.label,
description: props.description,
options: props.options ?? [],
default: props.default,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// NumberControl Component
interface NumberControlProps extends BaseControlProps {
min?: number;
max?: number;
default?: number;
placeholder?: string;
}
export const NumberControl = (
props: NumberControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'TextControl',
label: props.label,
description: props.description,
placeholder: props.placeholder,
default: props.default,
renderTrigger: props.renderTrigger ?? false,
isFloat: true,
controlHeader: {
label: props.label,
description: props.description,
},
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
min: props.min,
max: props.max,
},
});
// ColorPickerControl Component
interface ColorPickerControlProps extends BaseControlProps {
default?: { r: number; g: number; b: number; a?: number };
}
export const ColorPickerControl = (
props: ColorPickerControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColorPickerControl',
label: props.label,
description: props.description,
default: props.default ?? { r: 0, g: 122, b: 135, a: 1 },
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// DateFilterControl Component
interface DateFilterControlProps extends BaseControlProps {
default?: string;
}
export const DateFilterControl = (
props: DateFilterControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'DateFilterControl',
label: props.label,
description: props.description,
default: props.default,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
renderTrigger: props.renderTrigger,
},
});
// BoundsControl Component
interface BoundsControlProps extends BaseControlProps {
default?: [number | null, number | null];
min?: number;
max?: number;
}
export const BoundsControl = (
props: BoundsControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'BoundsControl',
label: props.label,
description: props.description,
default: props.default ?? [null, null],
min: props.min,
max: props.max,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// SwitchControl Component
interface SwitchControlProps extends BaseControlProps {
default?: boolean;
}
export const SwitchControl = (
props: SwitchControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'CheckboxControl',
label: props.label,
description: props.description,
default: props.default ?? false,
renderTrigger: props.renderTrigger ?? false,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// HiddenControl Component (for hidden fields)
interface HiddenControlProps {
name: string;
value?: any;
default?: any;
}
export const HiddenControl = (
props: HiddenControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'HiddenControl',
default: props.default,
value: props.value,
renderTrigger: false,
visible: false,
},
});
// MetricsControl Component
interface MetricsControlProps extends BaseControlProps {
multi?: boolean;
clearable?: boolean;
savedMetrics?: any[];
columns?: any[];
datasourceType?: string;
}
export const MetricsControl = (
props: MetricsControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'MetricsControl',
label: props.label,
description: props.description,
multi: props.multi ?? true,
clearable: props.clearable ?? true,
validators: props.validators ?? [],
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
columns: state.datasource?.columns || [],
savedMetrics: state.datasource?.metrics || [],
datasourceType: state.datasource?.type,
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
savedMetrics: props.savedMetrics,
columns: props.columns,
datasourceType: props.datasourceType,
},
});
// GroupByControl Component
interface GroupByControlProps extends BaseControlProps {
multi?: boolean;
clearable?: boolean;
columns?: any[];
}
export const GroupByControl = (
props: GroupByControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectControl',
label: props.label,
description: props.description,
multi: props.multi ?? true,
clearable: props.clearable ?? true,
validators: props.validators ?? [],
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
choices: state.datasource?.columns || [],
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
columns: props.columns,
},
});
// AdhocFilterControl Component
interface AdhocFilterControlProps extends BaseControlProps {
columns?: any[];
savedMetrics?: any[];
datasourceType?: string;
}
export const AdhocFilterControl = (
props: AdhocFilterControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'AdhocFilterControl',
label: props.label,
description: props.description,
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
columns: state.datasource?.columns || [],
savedMetrics: state.datasource?.metrics || [],
datasourceType: state.datasource?.type,
})),
default: props.default,
renderTrigger: props.renderTrigger,
validators: props.validators,
warning: props.warning,
error: props.error,
visibility: props.visibility,
columns: props.columns,
savedMetrics: props.savedMetrics,
datasourceType: props.datasourceType,
},
});
// SpatialControl Component
interface SpatialControlProps extends BaseControlProps {
choices?: any[];
}
export const SpatialControl = (
props: SpatialControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SpatialControl',
label: props.label,
description: props.description,
validators: props.validators,
mapStateToProps:
props.mapStateToProps ||
((state: any) => ({
choices: state.datasource?.columns || [],
})),
default: props.default,
renderTrigger: props.renderTrigger,
warning: props.warning,
error: props.error,
visibility: props.visibility,
},
});
// ColorSchemeControl Component
interface ColorSchemeControlProps extends BaseControlProps {
choices?: (() => Array<[string, string]>) | Array<[string, string]>;
schemes?: () => any;
isLinear?: boolean;
}
export const ColorSchemeControl = (
props: ColorSchemeControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColorSchemeControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
choices: props.choices,
schemes: props.schemes,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
isLinear: props.isLinear,
},
});
// SelectAsyncControl Component
interface SelectAsyncControlProps extends BaseControlProps {
dataEndpoint?: string;
multi?: boolean;
mutator?: (data: any) => any;
placeholder?: string;
onAsyncErrorMessage?: string;
cacheOptions?: boolean;
}
export const SelectAsyncControl = (
props: SelectAsyncControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'SelectAsyncControl',
label: props.label,
description: props.description,
default: props.default,
dataEndpoint: props.dataEndpoint,
multi: props.multi ?? false,
mutator: props.mutator,
placeholder: props.placeholder,
onAsyncErrorMessage: props.onAsyncErrorMessage,
cacheOptions: props.cacheOptions ?? true,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
renderTrigger: props.renderTrigger,
},
});
// ContourControl Component
interface ContourControlProps extends BaseControlProps {
renderTrigger?: boolean;
choices?: Array<[string, string]>;
}
export const ContourControl = (
props: ContourControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ContourControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
choices: props.choices,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// ColumnConfigControl Component
interface ColumnConfigControlProps extends BaseControlProps {
renderTrigger?: boolean;
}
export const ColumnConfigControl = (
props: ColumnConfigControlProps,
): ControlComponentConfig => ({
name: props.name,
config: {
type: 'ColumnConfigControl',
label: props.label,
description: props.description,
default: props.default,
renderTrigger: props.renderTrigger ?? true,
validators: props.validators,
warning: props.warning,
error: props.error,
mapStateToProps: props.mapStateToProps,
visibility: props.visibility,
},
});
// Export all components
export default {
CheckboxControl,
SelectControl,
TextControl,
TextAreaControl,
SliderControl,
RadioButtonControl,
NumberControl,
ColorPickerControl,
DateFilterControl,
BoundsControl,
SwitchControl,
HiddenControl,
MetricsControl,
GroupByControl,
AdhocFilterControl,
SpatialControl,
ColorSchemeControl,
SelectAsyncControl,
ContourControl,
ColumnConfigControl,
};

View File

@@ -1,154 +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 { ReactNode, FC } from 'react';
import { Row, Col, Collapse } from '@superset-ui/core/components';
/**
* Props for control panel sections
*/
export interface ControlSectionProps {
label?: ReactNode;
description?: ReactNode;
expanded?: boolean;
children: ReactNode;
}
/**
* A collapsible section in the control panel
*/
export const ControlSection: FC<ControlSectionProps> = ({
label,
description,
expanded = true,
children,
}) => {
if (!label) {
// No label means no collapsible wrapper
return <>{children}</>;
}
return (
<Collapse defaultActiveKey={expanded ? ['1'] : []} ghost>
<Collapse.Panel
header={
<span>
{label}
{description && (
<span style={{ marginLeft: 8, fontSize: '0.85em', opacity: 0.7 }}>
{description}
</span>
)}
</span>
}
key="1"
>
{children}
</Collapse.Panel>
</Collapse>
);
};
/**
* Props for control row - uses Ant Design grid
*/
export interface ControlRowProps {
children: ReactNode;
gutter?: number | [number, number];
}
/**
* A row of controls using Ant Design's grid system
* Automatically distributes controls evenly across columns
*/
export const ControlPanelRow: FC<ControlRowProps> = ({
children,
gutter = [16, 16],
}) => {
const childArray = Array.isArray(children) ? children : [children];
const validChildren = childArray.filter(
child => child !== null && child !== undefined,
);
const colSpan =
validChildren.length > 0 ? Math.floor(24 / validChildren.length) : 24;
return (
<Row gutter={gutter} style={{ marginBottom: 16 }}>
{validChildren.map((child, index) => (
<Col key={index} span={colSpan}>
{child}
</Col>
))}
</Row>
);
};
/**
* Props for the main control panel layout
*/
export interface ControlPanelLayoutProps {
children: ReactNode;
}
/**
* Main control panel layout container
*/
export const ControlPanelLayout: FC<ControlPanelLayoutProps> = ({
children,
}) => (
<div className="control-panel-layout" style={{ padding: '16px 0' }}>
{children}
</div>
);
/**
* Helper function to create a full-width single control row
*/
export const SingleControlRow: FC<{ children: ReactNode }> = ({ children }) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={24}>{children}</Col>
</Row>
);
/**
* Helper function to create a two-column control row
*/
export const TwoColumnRow: FC<{ left: ReactNode; right: ReactNode }> = ({
left,
right,
}) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={12}>{left}</Col>
<Col span={12}>{right}</Col>
</Row>
);
/**
* Helper function to create a three-column control row
*/
export const ThreeColumnRow: FC<{
left: ReactNode;
center: ReactNode;
right: ReactNode;
}> = ({ left, center, right }) => (
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={8}>{left}</Col>
<Col span={8}>{center}</Col>
<Col span={8}>{right}</Col>
</Row>
);

View File

@@ -1,360 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Switch, Select, Input, Slider } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface DeckGLControlsSectionProps {
layerType?:
| 'scatter'
| 'polygon'
| 'path'
| 'heatmap'
| 'hex'
| 'grid'
| 'screengrid'
| 'contour'
| 'geojson'
| 'arc';
showViewport?: boolean;
showMapStyle?: boolean;
showColorScheme?: boolean;
showLegend?: boolean;
showTooltip?: boolean;
showFilters?: boolean;
showAnimation?: boolean;
show3D?: boolean;
showMultiplier?: boolean;
showPointRadius?: boolean;
showLineWidth?: boolean;
showFillColor?: boolean;
showStrokeColor?: boolean;
showOpacity?: boolean;
showCoverage?: boolean;
showElevation?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const DeckGLControlsSection: FC<DeckGLControlsSectionProps> = ({
layerType = 'scatter',
showViewport = true,
showMapStyle = true,
showColorScheme = true,
showLegend = true,
showTooltip = true,
showFilters = true,
showAnimation = false,
show3D = false,
showMultiplier = false,
showPointRadius = false,
showLineWidth = false,
showFillColor = false,
showStrokeColor = false,
showOpacity = true,
showCoverage = false,
showElevation = false,
values = {},
onChange = () => {},
}) => (
<div className="deckgl-controls-section">
{/* Map Style */}
{showMapStyle && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Map Style')}</label>
<Select
value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'}
onChange={value => onChange('mapbox_style', value)}
style={{ width: '100%' }}
options={[
{
value: 'mapbox://styles/mapbox/streets-v11',
label: t('Streets'),
},
{ value: 'mapbox://styles/mapbox/light-v9', label: t('Light') },
{ value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') },
{
value: 'mapbox://styles/mapbox/satellite-v9',
label: t('Satellite'),
},
{
value: 'mapbox://styles/mapbox/outdoors-v11',
label: t('Outdoors'),
},
]}
/>
<small className="text-muted">
{t('Base map style for the visualization')}
</small>
</Col>
</Row>
)}
{/* Viewport */}
{showViewport && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Zoom')}</label>
<Slider
value={values.zoom || 11}
onChange={value => onChange('zoom', value)}
min={0}
max={22}
step={0.1}
marks={{ 0: '0', 11: '11', 22: '22' }}
/>
<small className="text-muted">{t('Map zoom level')}</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.autozoom || true}
onChange={checked => onChange('autozoom', checked)}
/>
{t('Auto Zoom')}
</label>
<small className="text-muted">
{t('Automatically zoom to fit data bounds')}
</small>
</Col>
</Row>
</>
)}
{/* Point/Shape Size Controls */}
{showPointRadius && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Point Radius')}</label>
<Slider
value={values.point_radius_fixed?.value || 1000}
onChange={value =>
onChange('point_radius_fixed', { type: 'fix', value })
}
min={1}
max={10000}
step={10}
/>
<small className="text-muted">
{t('Fixed radius for points in meters')}
</small>
</Col>
</Row>
)}
{showLineWidth && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Line Width')}</label>
<Slider
value={values.line_width || 1}
onChange={value => onChange('line_width', value)}
min={1}
max={50}
step={1}
/>
<small className="text-muted">{t('Width of lines in pixels')}</small>
</Col>
</Row>
)}
{/* 3D Controls */}
{show3D && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.extruded || false}
onChange={checked => onChange('extruded', checked)}
/>
{t('3D')}
</label>
<small className="text-muted">{t('Show data in 3D')}</small>
</Col>
</Row>
{values.extruded && showElevation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Elevation')}</label>
<Slider
value={values.elevation || 0.1}
onChange={value => onChange('elevation', value)}
min={0}
max={1}
step={0.01}
/>
<small className="text-muted">
{t('Elevation multiplier for 3D rendering')}
</small>
</Col>
</Row>
)}
</>
)}
{/* Opacity */}
{showOpacity && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Opacity')}</label>
<Slider
value={values.opacity || 80}
onChange={value => onChange('opacity', value)}
min={0}
max={100}
step={1}
marks={{ 0: '0%', 50: '50%', 100: '100%' }}
/>
<small className="text-muted">{t('Layer opacity')}</small>
</Col>
</Row>
)}
{/* Coverage (for hex, grid) */}
{showCoverage && (layerType === 'hex' || layerType === 'grid') && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Coverage')}</label>
<Slider
value={values.coverage || 1}
onChange={value => onChange('coverage', value)}
min={0}
max={1}
step={0.01}
/>
<small className="text-muted">{t('Cell coverage radius')}</small>
</Col>
</Row>
)}
{/* Legend */}
{showLegend && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Legend Position')}</label>
<Select
value={values.legend_position || 'top_right'}
onChange={value => onChange('legend_position', value)}
style={{ width: '100%' }}
options={[
{ value: 'top_left', label: t('Top left') },
{ value: 'top_right', label: t('Top right') },
{ value: 'bottom_left', label: t('Bottom left') },
{ value: 'bottom_right', label: t('Bottom right') },
]}
/>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Legend Format')}</label>
<Input
value={values.legend_format || ''}
onChange={e => onChange('legend_format', e.target.value)}
placeholder=".3s"
/>
<small className="text-muted">
{t('D3 number format for legend')}
</small>
</Col>
</Row>
</>
)}
{/* Filters */}
{showFilters && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.filter_nulls || true}
onChange={checked => onChange('filter_nulls', checked)}
/>
{t('Filter Nulls')}
</label>
<small className="text-muted">
{t('Filter out null values from data')}
</small>
</Col>
</Row>
)}
{/* Tooltip */}
{showTooltip && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Tooltip')}</label>
<Input.TextArea
value={values.js_tooltip || ''}
onChange={e => onChange('js_tooltip', e.target.value)}
placeholder={t('JavaScript tooltip generator')}
rows={3}
/>
<small className="text-muted">
{t('JavaScript code for custom tooltip')}
</small>
</Col>
</Row>
)}
{/* Animation */}
{showAnimation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.animation || false}
onChange={checked => onChange('animation', checked)}
/>
{t('Animate')}
</label>
<small className="text-muted">
{t('Animate visualization over time')}
</small>
</Col>
</Row>
)}
{/* Multiplier for some visualizations */}
{showMultiplier && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Multiplier')}</label>
<Slider
value={values.multiplier || 1}
onChange={value => onChange('multiplier', value)}
min={0.01}
max={10}
step={0.01}
/>
<small className="text-muted">{t('Value multiplier')}</small>
</Col>
</Row>
)}
</div>
);
export default DeckGLControlsSection;

View File

@@ -1,336 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Switch, Select, Input, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface FilterControlsSectionProps {
filterType: 'select' | 'range' | 'time' | 'time_column' | 'time_grain';
showMultiple?: boolean;
showSearch?: boolean;
showParentFilter?: boolean;
showDefaultValue?: boolean;
showInverseSelection?: boolean;
showDateFilter?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const FilterControlsSection: FC<FilterControlsSectionProps> = ({
filterType,
showMultiple = true,
showSearch = true,
showParentFilter = true,
showDefaultValue = true,
showInverseSelection = false,
showDateFilter = false,
values = {},
onChange = () => {},
}) => {
const isSelect = filterType === 'select';
const isRange = filterType === 'range';
const isTime = filterType === 'time';
const isTimeColumn = filterType === 'time_column';
const isTimeGrain = filterType === 'time_grain';
return (
<div className="filter-controls-section">
{/* Multiple Selection */}
{showMultiple && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.multiSelect || false}
onChange={checked => onChange('multiSelect', checked)}
/>
{t('Multiple Select')}
</label>
<small className="text-muted">
{t('Allow selecting multiple values')}
</small>
</Col>
</Row>
)}
{/* Search */}
{showSearch && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.enableEmptyFilter || false}
onChange={checked => onChange('enableEmptyFilter', checked)}
/>
{t('Enable Empty Filter')}
</label>
<small className="text-muted">
{t('Allow empty filter values')}
</small>
</Col>
</Row>
)}
{/* Inverse Selection */}
{showInverseSelection && isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.inverseSelection || false}
onChange={checked => onChange('inverseSelection', checked)}
/>
{t('Inverse Selection')}
</label>
<small className="text-muted">
{t('Exclude selected values instead of including them')}
</small>
</Col>
</Row>
)}
{/* Parent Filter */}
{showParentFilter && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.parentFilter || false}
onChange={checked => onChange('parentFilter', checked)}
/>
{t('Parent Filter')}
</label>
<small className="text-muted">
{t('Filter is dependent on another filter')}
</small>
</Col>
</Row>
)}
{/* Default Value */}
{showDefaultValue && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Default Value')}</label>
{isSelect ? (
<Input
value={values.defaultValue || ''}
onChange={e => onChange('defaultValue', e.target.value)}
placeholder={t('Enter default value')}
/>
) : isRange ? (
<div style={{ display: 'flex', gap: 8 }}>
<InputNumber
value={values.defaultValueMin}
onChange={value => onChange('defaultValueMin', value)}
placeholder={t('Min')}
style={{ flex: 1 }}
/>
<InputNumber
value={values.defaultValueMax}
onChange={value => onChange('defaultValueMax', value)}
placeholder={t('Max')}
style={{ flex: 1 }}
/>
</div>
) : (
<Input
value={values.defaultValue || ''}
onChange={e => onChange('defaultValue', e.target.value)}
placeholder={t('Enter default value')}
/>
)}
<small className="text-muted">
{t('Default value to use when filter is first loaded')}
</small>
</Col>
</Row>
)}
{/* Sort Options for Select */}
{isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Sort Filter Values')}</label>
<Select
value={values.sortFilter || false}
onChange={value => onChange('sortFilter', value)}
style={{ width: '100%' }}
options={[
{ value: false, label: t('No Sort') },
{ value: true, label: t('Sort Ascending') },
{ value: 'desc', label: t('Sort Descending') },
]}
/>
<small className="text-muted">
{t('Sort filter values alphabetically')}
</small>
</Col>
</Row>
)}
{/* Search for Select Filter */}
{isSelect && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.searchAllOptions || false}
onChange={checked => onChange('searchAllOptions', checked)}
/>
{t('Search All Options')}
</label>
<small className="text-muted">
{t('Search all filter options, not just displayed ones')}
</small>
</Col>
</Row>
)}
{/* Range Options */}
{isRange && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Min Value')}</label>
<InputNumber
value={values.rangeMin}
onChange={value => onChange('rangeMin', value)}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t('Minimum value for the range')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Max Value')}</label>
<InputNumber
value={values.rangeMax}
onChange={value => onChange('rangeMax', value)}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t('Maximum value for the range')}
</small>
</Col>
</Row>
</>
)}
{/* Time Options */}
{(isTime || isTimeColumn) && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.defaultToFirstValue || false}
onChange={checked => onChange('defaultToFirstValue', checked)}
/>
{t('Default to First Value')}
</label>
<small className="text-muted">
{t('Default to the first available time value')}
</small>
</Col>
</Row>
)}
{/* Time Grain Options */}
{isTimeGrain && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Default Time Grain')}</label>
<Select
value={values.defaultTimeGrain || 'day'}
onChange={value => onChange('defaultTimeGrain', value)}
style={{ width: '100%' }}
options={[
{ value: 'minute', label: t('Minute') },
{ value: 'hour', label: t('Hour') },
{ value: 'day', label: t('Day') },
{ value: 'week', label: t('Week') },
{ value: 'month', label: t('Month') },
{ value: 'quarter', label: t('Quarter') },
{ value: 'year', label: t('Year') },
]}
/>
<small className="text-muted">
{t('Default time granularity')}
</small>
</Col>
</Row>
)}
{/* UI Configuration */}
<h4 style={{ marginTop: 24, marginBottom: 16 }}>
{t('UI Configuration')}
</h4>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.instant_filtering || true}
onChange={checked => onChange('instant_filtering', checked)}
/>
{t('Instant Filtering')}
</label>
<small className="text-muted">
{t('Apply filters instantly as they change')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_apply || false}
onChange={checked => onChange('show_apply', checked)}
/>
{t('Show Apply Button')}
</label>
<small className="text-muted">
{t('Show an apply button for the filter')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_clear || true}
onChange={checked => onChange('show_clear', checked)}
/>
{t('Show Clear Button')}
</label>
<small className="text-muted">
{t('Show a clear button for the filter')}
</small>
</Col>
</Row>
</div>
);
};
export default FilterControlsSection;

View File

@@ -1,241 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Select } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface FormatControlGroupProps {
showNumber?: boolean;
showCurrency?: boolean;
showDate?: boolean;
showPercentage?: boolean;
numberFormatLabel?: string;
currencyFormatLabel?: string;
dateFormatLabel?: string;
percentageFormatLabel?: string;
customFormatOptions?: Array<[string, string]>;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
export const D3_FORMAT_OPTIONS = [
['SMART_NUMBER', t('Adaptive formatting')],
['~g', t('Original value')],
['d', t('Signed integer')],
['.0f', t('Integer')],
['.1f', t('1 decimal place')],
['.2f', t('2 decimal places')],
['.3f', t('3 decimal places')],
['.4f', t('4 decimal places')],
['.5f', t('5 decimal places')],
['+,', t('Positive integer')],
['+,.0f', t('Positive number')],
['+,.1f', t('Positive (1 decimal)')],
['+,.2f', t('Positive (2 decimals)')],
[',.0f', t('Number (no decimals)')],
[',.1f', t('Number (1 decimal)')],
[',.2f', t('Number (2 decimals)')],
[',.3f', t('Number (3 decimals)')],
['.0%', t('Percentage')],
['.1%', t('Percentage (1 decimal)')],
['.2%', t('Percentage (2 decimals)')],
['.3%', t('Percentage (3 decimals)')],
[',.0%', t('Percentage with thousands')],
['.1s', t('SI notation')],
['.2s', t('SI notation (2 decimals)')],
['.3s', t('SI notation (3 decimals)')],
['$,.0f', t('Currency (no decimals)')],
['$,.1f', t('Currency (1 decimal)')],
['$,.2f', t('Currency (2 decimals)')],
['$,.3f', t('Currency (3 decimals)')],
];
export const D3_TIME_FORMAT_OPTIONS = [
['smart_date', t('Adaptive formatting')],
['%Y-%m-%d', t('YYYY-MM-DD')],
['%Y-%m-%d %H:%M', t('YYYY-MM-DD HH:MM')],
['%Y-%m-%d %H:%M:%S', t('YYYY-MM-DD HH:MM:SS')],
['%Y/%m/%d', t('YYYY/MM/DD')],
['%m/%d/%Y', t('MM/DD/YYYY')],
['%d/%m/%Y', t('DD/MM/YYYY')],
['%d.%m.%Y', t('DD.MM.YYYY')],
['%Y', t('Year (YYYY)')],
['%B %Y', t('Month Year (January 2023)')],
['%b %Y', t('Month Year (Jan 2023)')],
['%B', t('Month (January)')],
['%b', t('Month (Jan)')],
['%B %-d, %Y', t('Month Day, Year')],
['%b %-d, %Y', t('Mon Day, Year')],
['%a', t('Day of week (short)')],
['%A', t('Day of week (full)')],
['%H:%M', t('Time (24-hour)')],
['%I:%M %p', t('Time (12-hour)')],
['%H:%M:%S', t('Time with seconds')],
];
const CURRENCY_OPTIONS = [
{ value: 'USD', label: 'USD ($)' },
{ value: 'EUR', label: 'EUR (€)' },
{ value: 'GBP', label: 'GBP (£)' },
{ value: 'JPY', label: 'JPY (¥)' },
{ value: 'CNY', label: 'CNY (¥)' },
{ value: 'INR', label: 'INR (₹)' },
{ value: 'CAD', label: 'CAD ($)' },
{ value: 'AUD', label: 'AUD ($)' },
{ value: 'CHF', label: 'CHF (Fr)' },
{ value: 'SEK', label: 'SEK (kr)' },
{ value: 'NOK', label: 'NOK (kr)' },
{ value: 'DKK', label: 'DKK (kr)' },
{ value: 'KRW', label: 'KRW (₩)' },
{ value: 'BRL', label: 'BRL (R$)' },
{ value: 'MXN', label: 'MXN ($)' },
{ value: 'RUB', label: 'RUB (₽)' },
];
const FormatControlGroup: FC<FormatControlGroupProps> = ({
showNumber = true,
showCurrency = false,
showDate = false,
showPercentage = false,
numberFormatLabel = t('Number format'),
currencyFormatLabel = t('Currency'),
dateFormatLabel = t('Date format'),
percentageFormatLabel = t('Percentage format'),
customFormatOptions = [],
values = {},
onChange = () => {},
}) => {
const formatOptions =
customFormatOptions.length > 0 ? customFormatOptions : D3_FORMAT_OPTIONS;
return (
<div className="format-control-group">
{showNumber && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{numberFormatLabel}</label>
<Select
value={values.number_format || 'SMART_NUMBER'}
onChange={value => onChange('number_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={formatOptions.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 format string for numbers. See ')}
<a
href="https://github.com/d3/d3-format/blob/main/README.md#format"
target="_blank"
rel="noopener noreferrer"
>
{t('D3 format docs')}
</a>
{t(' for details.')}
</small>
</Col>
</Row>
)}
{showCurrency && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{currencyFormatLabel}</label>
<Select
value={values.currency_format || 'USD'}
onChange={value => onChange('currency_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select currency')}
options={CURRENCY_OPTIONS}
/>
<small className="text-muted">
{t('Currency to use for formatting')}
</small>
</Col>
</Row>
)}
{showDate && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{dateFormatLabel}</label>
<Select
value={values.date_format || 'smart_date'}
onChange={value => onChange('date_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 time format string. See ')}
<a
href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format"
target="_blank"
rel="noopener noreferrer"
>
{t('D3 time format docs')}
</a>
{t(' for details.')}
</small>
</Col>
</Row>
)}
{showPercentage && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{percentageFormatLabel}</label>
<Select
value={values.percentage_format || '.0%'}
onChange={value => onChange('percentage_format', value)}
style={{ width: '100%' }}
showSearch
placeholder={t('Select or type a custom format')}
options={[
['.0%', t('0%')],
['.1%', t('0.1%')],
['.2%', t('0.12%')],
['.3%', t('0.123%')],
[',.0%', t('1,234%')],
[',.1%', t('1,234.5%')],
].map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('D3 format for percentages')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default FormatControlGroup;

View File

@@ -1,131 +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 { FC, useMemo } from 'react';
import { t } from '@superset-ui/core';
import { Select } from '@superset-ui/core/components';
import { ControlComponentProps, ColumnMeta } from '../../types';
import { ControlHeader } from '../../components/ControlHeader';
export interface GranularityControlValue {
column_name: string;
type?: string;
is_dttm?: boolean;
}
export interface GranularityControlProps
extends ControlComponentProps<GranularityControlValue | string> {
columns?: ColumnMeta[];
datasource?: {
columns?: ColumnMeta[];
verbose_map?: Record<string, string>;
};
clearable?: boolean;
temporalColumnsOnly?: boolean;
}
const GranularityControl: FC<GranularityControlProps> = ({
value,
onChange,
columns = [],
datasource,
clearable = false,
temporalColumnsOnly = true,
name,
label,
description,
validationErrors,
renderTrigger,
...props
}) => {
const allColumns = useMemo(() => {
const cols = columns.length > 0 ? columns : datasource?.columns || [];
if (temporalColumnsOnly) {
return cols.filter(col => col.is_dttm);
}
return cols;
}, [columns, datasource?.columns, temporalColumnsOnly]);
const options = useMemo(
() =>
allColumns.map(col => ({
value: col.column_name,
label:
datasource?.verbose_map?.[col.column_name] ||
col.verbose_name ||
col.column_name,
})),
[allColumns, datasource?.verbose_map],
);
const currentValue = useMemo(() => {
if (typeof value === 'string') {
return value;
}
return value?.column_name;
}, [value]);
const handleChange = (newValue: string | undefined) => {
if (onChange) {
if (!newValue && clearable) {
onChange(null as any);
} else if (newValue) {
const column = allColumns.find(col => col.column_name === newValue);
if (column) {
onChange({
column_name: column.column_name,
type: column.type,
is_dttm: column.is_dttm,
});
}
}
}
};
return (
<div>
<ControlHeader
name={name}
label={label || t('Time Column')}
description={
description ||
t(
'The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' +
'column in the table. Also note that the ' +
'filter below is applied against this column or ' +
'expression',
)
}
validationErrors={validationErrors}
renderTrigger={renderTrigger}
/>
<Select
value={currentValue}
onChange={handleChange}
options={options}
placeholder={t('Select a temporal column')}
allowClear={clearable}
showSearch
css={{ width: '100%' }}
/>
</div>
);
};
export default GranularityControl;

View File

@@ -1,268 +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 { CustomControlItem } from '../../types';
/**
* Helper function to create a SelectControl configuration
*/
export const SelectControl = (config: {
name: string;
label: string;
default?: any;
choices?: any[][] | (() => any[][]) | any[];
description?: string;
freeForm?: boolean;
clearable?: boolean;
multiple?: boolean;
validators?: any[];
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SelectControl',
...config,
},
});
/**
* Helper function to create a TextControl configuration
*/
export const TextControl = (config: {
name: string;
label: string;
default?: any;
description?: string;
isInt?: boolean;
isFloat?: boolean;
validators?: any[];
renderTrigger?: boolean;
placeholder?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'TextControl',
...config,
},
});
/**
* Helper function to create a CheckboxControl configuration
*/
export const CheckboxControl = (config: {
name: string;
label: string;
default?: boolean;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'CheckboxControl',
...config,
},
});
/**
* Helper function to create a SliderControl configuration
*/
export const SliderControl = (config: {
name: string;
label: string;
default?: number;
min?: number;
max?: number;
step?: number;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SliderControl',
...config,
},
});
/**
* Helper function to create a RadioButtonControl configuration
*/
export const RadioButtonControl = (config: {
name: string;
label: string;
default?: any;
options?: any[][] | any[];
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'RadioButtonControl',
...config,
},
});
/**
* Helper function to create a BoundsControl configuration
*/
export const BoundsControl = (config: {
name: string;
label: string;
default?: [number | null, number | null];
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'BoundsControl',
...config,
},
});
/**
* Helper function to create a ColorPickerControl configuration
*/
export const ColorPickerControl = (config: {
name: string;
label: string;
default?: { r: number; g: number; b: number; a: number };
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'ColorPickerControl',
...config,
},
});
/**
* Helper function to create a DateFilterControl configuration
*/
export const DateFilterControl = (config: {
name: string;
label: string;
default?: string;
description?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'DateFilterControl',
...config,
},
});
/**
* Helper function to create a SwitchControl configuration
*/
export const SwitchControl = (config: {
name: string;
label: string;
default?: boolean;
description?: string;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SwitchControl',
...config,
},
});
/**
* Helper function to create a HiddenControl configuration
*/
export const HiddenControl = (config: {
name: string;
default?: any;
initialValue?: any;
description?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'HiddenControl',
...config,
},
});
/**
* Helper function to create a SpatialControl configuration
*/
export const SpatialControl = (config: {
name: string;
label: string;
description?: string;
validators?: any[];
mapStateToProps?: (state: any) => any;
renderTrigger?: boolean;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'SpatialControl',
...config,
},
});
/**
* Helper function to create a ContourControl configuration
*/
export const ContourControl = (config: {
name: string;
label: string;
description?: string;
renderTrigger?: boolean;
mapStateToProps?: (state: any) => any;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'ContourControl',
...config,
},
});
/**
* Helper function to create a TextAreaControl configuration
*/
export const TextAreaControl = (config: {
name: string;
label: string;
default?: string;
description?: string;
renderTrigger?: boolean;
rows?: number;
placeholder?: string;
[key: string]: any;
}): CustomControlItem => ({
name: config.name,
config: {
type: 'TextAreaControl',
...config,
},
});

View File

@@ -1,241 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Select, Switch, InputNumber, Input } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface LabelControlGroupProps {
chartType?: 'pie' | 'sunburst' | 'treemap' | 'funnel' | 'gauge';
showLabelType?: boolean;
showTemplate?: boolean;
showThreshold?: boolean;
showOutside?: boolean;
showLabelLine?: boolean;
showRotation?: boolean;
showUpperLabels?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const LABEL_TYPE_OPTIONS = [
['key', t('Category Name')],
['value', t('Value')],
['percent', t('Percentage')],
['key_value', t('Category and Value')],
['key_percent', t('Category and Percentage')],
['key_value_percent', t('Category, Value and Percentage')],
['value_percent', t('Value and Percentage')],
['template', t('Template')],
];
const LABEL_ROTATION_OPTIONS = [
['0', t('Horizontal')],
['45', t('45°')],
['90', t('Vertical')],
['-45', t('-45°')],
];
const LabelControlGroup: FC<LabelControlGroupProps> = ({
chartType = 'pie',
showLabelType = true,
showTemplate = true,
showThreshold = true,
showOutside = false,
showLabelLine = false,
showRotation = false,
showUpperLabels = false,
values = {},
onChange = () => {},
}) => {
const showLabels = values.show_labels ?? true;
const labelType = values.label_type || 'key';
return (
<div className="label-control-group">
{/* Show Labels Toggle */}
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={showLabels}
onChange={checked => onChange('show_labels', checked)}
/>
{t('Show Labels')}
</label>
<small className="text-muted">
{t('Whether to display the labels')}
</small>
</Col>
</Row>
{showLabels && (
<>
{/* Label Type */}
{showLabelType && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Type')}</label>
<Select
value={labelType}
onChange={value => onChange('label_type', value)}
style={{ width: '100%' }}
options={LABEL_TYPE_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('What should be shown on the label?')}
</small>
</Col>
</Row>
)}
{/* Label Template */}
{showTemplate && labelType === 'template' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Template')}</label>
<Input.TextArea
value={values.label_template || ''}
onChange={e => onChange('label_template', e.target.value)}
placeholder="{name}: {value} ({percent}%)"
rows={3}
/>
<small className="text-muted">
{t(
'Format data labels. Use variables: {name}, {value}, {percent}. \\n represents a new line.',
)}
</small>
</Col>
</Row>
)}
{/* Label Threshold */}
{showThreshold && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Threshold')}</label>
<InputNumber
value={values.show_labels_threshold ?? 5}
onChange={value => onChange('show_labels_threshold', value)}
min={0}
max={100}
step={0.5}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
<small className="text-muted">
{t(
'Minimum threshold in percentage points for showing labels',
)}
</small>
</Col>
</Row>
)}
{/* Labels Outside (Pie specific) */}
{showOutside && chartType === 'pie' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.labels_outside || false}
onChange={checked => onChange('labels_outside', checked)}
/>
{t('Put labels outside')}
</label>
<small className="text-muted">
{t('Put the labels outside of the pie?')}
</small>
</Col>
</Row>
)}
{/* Label Line (Pie specific) */}
{showLabelLine && chartType === 'pie' && values.labels_outside && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.label_line || false}
onChange={checked => onChange('label_line', checked)}
/>
{t('Label Line')}
</label>
<small className="text-muted">
{t('Draw a line from the label to the slice')}
</small>
</Col>
</Row>
)}
{/* Label Rotation */}
{showRotation && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Label Rotation')}</label>
<Select
value={values.label_rotation || '0'}
onChange={value => onChange('label_rotation', value)}
style={{ width: '100%' }}
options={LABEL_ROTATION_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Rotation angle of labels')}
</small>
</Col>
</Row>
)}
{/* Show Upper Labels (Treemap specific) */}
{showUpperLabels && chartType === 'treemap' && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
>
<Switch
checked={values.show_upper_labels || false}
onChange={checked => onChange('show_upper_labels', checked)}
/>
{t('Show Upper Labels')}
</label>
<small className="text-muted">
{t('Show labels for parent nodes')}
</small>
</Col>
</Row>
)}
</>
)}
</div>
);
};
export default LabelControlGroup;

View File

@@ -1,133 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Switch, Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface MarkerControlGroupProps {
enabledLabel?: string;
sizeLabel?: string;
maxSize?: number;
minSize?: number;
defaultSize?: number;
values?: {
markerEnabled?: boolean;
markerSize?: number;
};
onChange?: (name: string, value: any) => void;
disabled?: boolean;
}
const MarkerControlGroup: FC<MarkerControlGroupProps> = ({
enabledLabel = t('Show markers'),
sizeLabel = t('Marker size'),
maxSize = 20,
minSize = 0,
defaultSize = 6,
values = {},
onChange = () => {},
disabled = false,
}) => {
const markerEnabled = values.markerEnabled ?? false;
const markerSize = values.markerSize ?? defaultSize;
const handleEnabledChange = (checked: boolean) => {
onChange('markerEnabled', checked);
if (checked && !values.markerSize) {
onChange('markerSize', defaultSize);
}
};
const handleSizeChange = (value: number) => {
onChange('markerSize', value);
};
const handleInputChange = (value: number | null) => {
if (value !== null) {
const clampedValue = Math.max(minSize, Math.min(maxSize, value));
onChange('markerSize', clampedValue);
}
};
return (
<div className="marker-control-group">
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={markerEnabled}
onChange={handleEnabledChange}
disabled={disabled}
/>
{enabledLabel}
</label>
<small className="text-muted">
{t('Draw markers on data points for better visibility')}
</small>
</Col>
</Row>
{markerEnabled && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'block', marginBottom: 8 }}>
{sizeLabel}
</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={minSize}
max={maxSize}
step={1}
value={markerSize}
onChange={handleSizeChange}
disabled={disabled || !markerEnabled}
marks={{
[minSize]: minSize.toString(),
[Math.floor(maxSize / 2)]: Math.floor(
maxSize / 2,
).toString(),
[maxSize]: maxSize.toString(),
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={minSize}
max={maxSize}
step={1}
value={markerSize}
onChange={handleInputChange}
disabled={disabled || !markerEnabled}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Size of the markers in pixels')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default MarkerControlGroup;

View File

@@ -1,114 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface OpacityControlProps {
name?: string;
label?: string;
description?: string;
min?: number;
max?: number;
step?: number;
value?: number;
onChange?: (value: number) => void;
disabled?: boolean;
marks?: Record<number, string>;
}
const OpacityControl: FC<OpacityControlProps> = ({
name = 'opacity',
label = t('Opacity'),
description = t('Opacity of the elements'),
min = 0,
max = 1,
step = 0.1,
value = 0.8,
onChange = () => {},
disabled = false,
marks,
}) => {
const defaultMarks = marks || {
0: '0%',
0.25: '25%',
0.5: '50%',
0.75: '75%',
1: '100%',
};
const handleSliderChange = (val: number) => {
onChange(val);
};
const handleInputChange = (val: number | null) => {
if (val !== null) {
const clampedValue = Math.max(min, Math.min(max, val));
onChange(clampedValue);
}
};
const percentageValue = Math.round(value * 100);
return (
<div className="opacity-control" data-name={name}>
<label style={{ display: 'block', marginBottom: 8 }}>{label}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={min}
max={max}
step={step}
value={value}
onChange={handleSliderChange}
marks={defaultMarks}
disabled={disabled}
tooltip={{
formatter: val => `${Math.round((val as number) * 100)}%`,
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={min * 100}
max={max * 100}
step={step * 100}
value={percentageValue}
onChange={val => handleInputChange(val !== null ? val / 100 : null)}
formatter={val => `${val}%`}
parser={val => Number((val as string).replace('%', ''))}
disabled={disabled}
style={{ width: '100%' }}
/>
</Col>
</Row>
{description && (
<small
className="text-muted"
style={{ display: 'block', marginTop: 4 }}
>
{description}
</small>
)}
</div>
);
};
export default OpacityControl;

View File

@@ -1,175 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Select, Switch, Slider, InputNumber } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
export interface PieShapeControlProps {
showDonut?: boolean;
showRoseType?: boolean;
showRadius?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const ROSE_TYPE_OPTIONS = [
['area', t('Area')],
['radius', t('Radius')],
[null, t('None')],
];
const PieShapeControl: FC<PieShapeControlProps> = ({
showDonut = true,
showRoseType = true,
showRadius = true,
values = {},
onChange = () => {},
}) => {
const isDonut = values.donut || false;
const innerRadius = values.innerRadius || 30;
const outerRadius = values.outerRadius || 70;
return (
<div className="pie-shape-control">
{/* Donut Toggle */}
{showDonut && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={isDonut}
onChange={checked => onChange('donut', checked)}
/>
{t('Donut')}
</label>
<small className="text-muted">
{t('Do you want a donut or a pie?')}
</small>
</Col>
</Row>
)}
{/* Inner Radius (for Donut) */}
{showRadius && isDonut && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Inner Radius')}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={0}
max={100}
step={1}
value={innerRadius}
onChange={value => onChange('innerRadius', value)}
marks={{
0: '0%',
50: '50%',
100: '100%',
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={0}
max={100}
step={1}
value={innerRadius}
onChange={value => onChange('innerRadius', value)}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Inner radius of donut hole')}
</small>
</Col>
</Row>
)}
{/* Outer Radius */}
{showRadius && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Outer Radius')}</label>
<Row gutter={16} align="middle">
<Col span={16}>
<Slider
min={0}
max={100}
step={1}
value={outerRadius}
onChange={value => onChange('outerRadius', value)}
marks={{
0: '0%',
50: '50%',
100: '100%',
}}
/>
</Col>
<Col span={8}>
<InputNumber
min={0}
max={100}
step={1}
value={outerRadius}
onChange={value => onChange('outerRadius', value)}
formatter={value => `${value}%`}
parser={value => Number((value as string).replace('%', ''))}
style={{ width: '100%' }}
/>
</Col>
</Row>
<small className="text-muted">
{t('Outer edge of the pie/donut')}
</small>
</Col>
</Row>
)}
{/* Rose Type (Nightingale Chart) */}
{showRoseType && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Rose Type')}</label>
<Select
value={values.roseType || null}
onChange={value => onChange('roseType', value)}
style={{ width: '100%' }}
allowClear
placeholder={t('None')}
options={ROSE_TYPE_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Whether to show as Nightingale chart (polar area chart)')}
</small>
</Col>
</Row>
)}
</div>
);
};
export default PieShapeControl;

View File

@@ -1,104 +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 { FC, ReactElement, useMemo } from 'react';
import { JsonValue } from '@superset-ui/core';
export interface ReactControlPanelSection {
key: string;
label: string;
description?: string;
expanded?: boolean;
render: (props: ControlPanelRenderProps) => ReactElement;
}
export interface ControlPanelRenderProps {
values: Record<string, JsonValue>;
onChange: (name: string, value: JsonValue) => void;
datasource?: any;
formData?: any;
validationErrors?: Record<string, string[]>;
}
export interface ReactControlPanelConfig {
sections: ReactControlPanelSection[];
onFormDataChange?: (formData: any) => any;
}
/**
* A wrapper component that allows rendering control panels with actual React components
* instead of configuration objects. This provides a stepping stone toward JSON-driven forms.
*/
export const ReactControlPanel: FC<{
config: ReactControlPanelConfig;
values: Record<string, JsonValue>;
onChange: (name: string, value: JsonValue) => void;
datasource?: any;
formData?: any;
validationErrors?: Record<string, string[]>;
}> = ({ config, values, onChange, datasource, formData, validationErrors }) => {
const renderProps: ControlPanelRenderProps = useMemo(
() => ({
values,
onChange,
datasource,
formData,
validationErrors,
}),
[values, onChange, datasource, formData, validationErrors],
);
return (
<div className="react-control-panel">
{config.sections.map(section => (
<div key={section.key} className="control-panel-section">
<h4>{section.label}</h4>
{section.description && (
<p className="section-description">{section.description}</p>
)}
{section.render(renderProps)}
</div>
))}
</div>
);
};
/**
* Helper function to create a control panel configuration that works with
* both the old and new systems
*/
export function createReactControlPanel(config: ReactControlPanelConfig): any {
return {
controlPanelSections: config.sections.map(section => ({
label: section.label,
description: section.description,
expanded: section.expanded ?? true,
controlSetRows: [
[
// Return a React element that will be rendered directly
<ReactControlPanel
key={section.key}
config={{ sections: [section] }}
values={{}}
onChange={() => {}}
/>,
],
],
})),
};
}

View File

@@ -1,306 +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 { CustomControlItem } from '../../types';
import sharedControls from '../sharedControls';
/**
* React component wrappers for shared controls.
* These replace string references like ['metrics'] with actual component calls.
*/
// Metrics controls
export const MetricsControl = (): CustomControlItem => ({
name: 'metrics',
config: sharedControls.metrics,
});
export const MetricControl = (): CustomControlItem => ({
name: 'metric',
config: sharedControls.metric,
});
export const SecondaryMetricControl = (): CustomControlItem => ({
name: 'secondary_metric',
config: sharedControls.secondary_metric,
});
export const Metric2Control = (): CustomControlItem => ({
name: 'metric_2',
config: sharedControls.metric_2,
});
export const TimeLimitMetricControl = (): CustomControlItem => ({
name: 'timeseries_limit_metric',
config: sharedControls.timeseries_limit_metric,
});
export const OrderByControl = (): CustomControlItem => ({
name: 'orderby',
config: sharedControls.orderby,
});
export const SeriesLimitMetricControl = (): CustomControlItem => ({
name: 'series_limit_metric',
config: sharedControls.series_limit_metric,
});
export const SortByMetricControl = (): CustomControlItem => ({
name: 'sort_by_metric',
config: sharedControls.sort_by_metric,
});
// Dimension controls
export const GroupByControl = (): CustomControlItem => ({
name: 'groupby',
config: sharedControls.groupby,
});
export const ColumnsControl = (): CustomControlItem => ({
name: 'columns',
config: sharedControls.columns,
});
// Note: These controls are not in sharedControls, using columns as fallback
export const AllColumnsControl = (): CustomControlItem => ({
name: 'all_columns',
config: sharedControls.columns || {},
});
export const AllColumnsXControl = (): CustomControlItem => ({
name: 'all_columns_x',
config: sharedControls.columns || {},
});
export const AllColumnsYControl = (): CustomControlItem => ({
name: 'all_columns_y',
config: sharedControls.columns || {},
});
export const SeriesControl = (): CustomControlItem => ({
name: 'series',
config: sharedControls.series,
});
export const EntityControl = (): CustomControlItem => ({
name: 'entity',
config: sharedControls.entity,
});
export const XControl = (): CustomControlItem => ({
name: 'x',
config: sharedControls.x,
});
export const YControl = (): CustomControlItem => ({
name: 'y',
config: sharedControls.y,
});
// Note: sort_by is not in sharedControls, using a default config
export const SortByControl = (): CustomControlItem => ({
name: 'sort_by',
config: {
type: 'SelectControl',
label: 'Sort By',
description: 'Sort by column',
},
});
export const SizeControl = (): CustomControlItem => ({
name: 'size',
config: sharedControls.size,
});
export const XAxisControl = (): CustomControlItem => ({
name: 'x_axis',
config: sharedControls.x_axis,
});
// Filter controls
export const AdhocFiltersControl = (): CustomControlItem => ({
name: 'adhoc_filters',
config: sharedControls.adhoc_filters,
});
export const TimeRangeControl = (): CustomControlItem => ({
name: 'time_range',
config: sharedControls.time_range,
});
export const TimeGrainSqlaControl = (): CustomControlItem => ({
name: 'time_grain_sqla',
config: sharedControls.time_grain_sqla,
});
export const GranularityControl = (): CustomControlItem => ({
name: 'granularity',
config: sharedControls.granularity,
});
export const GranularitySqlaControl = (): CustomControlItem => ({
name: 'granularity_sqla',
config: sharedControls.granularity_sqla,
});
// Limit controls
export const RowLimitControl = (): CustomControlItem => ({
name: 'row_limit',
config: sharedControls.row_limit,
});
export const LimitControl = (): CustomControlItem => ({
name: 'limit',
config: sharedControls.limit,
});
export const GroupOthersWhenLimitReachedControl = (): CustomControlItem => ({
name: 'group_others_when_limit_reached',
config: sharedControls.group_others_when_limit_reached,
});
export const SeriesLimitControl = (): CustomControlItem => ({
name: 'series_limit',
config: sharedControls.series_limit,
});
// Sort controls
export const OrderDescControl = (): CustomControlItem => ({
name: 'order_desc',
config: sharedControls.order_desc,
});
export const OrderByColsControl = (): CustomControlItem => ({
name: 'order_by_cols',
config: sharedControls.order_by_cols,
});
// Color controls
export const ColorSchemeControl = (): CustomControlItem => ({
name: 'color_scheme',
config: sharedControls.color_scheme,
});
export const LinearColorSchemeControl = (): CustomControlItem => ({
name: 'linear_color_scheme',
config: sharedControls.linear_color_scheme,
});
export const ColorPickerControl = (): CustomControlItem => ({
name: 'color_picker',
config: sharedControls.color_picker,
});
export const TimeShiftColorControl = (): CustomControlItem => ({
name: 'time_shift_color',
config: sharedControls.time_shift_color,
});
export const CurrencyFormatControl = (): CustomControlItem => ({
name: 'currency_format',
config: sharedControls.currency_format,
});
export const TruncateMetricControl = (): CustomControlItem => ({
name: 'truncate_metric',
config: sharedControls.truncate_metric,
});
export const ShowEmptyColumnsControl = (): CustomControlItem => ({
name: 'show_empty_columns',
config: sharedControls.show_empty_columns,
});
// Other controls
export const ZoomableControl = (): CustomControlItem => ({
name: 'zoomable',
config: sharedControls.zoomable,
});
export const DatasourceControl = (): CustomControlItem => ({
name: 'datasource',
config: sharedControls.datasource,
});
export const VizTypeControl = (): CustomControlItem => ({
name: 'viz_type',
config: sharedControls.viz_type,
});
// Tooltip controls
export const TooltipColumnsControl = (): CustomControlItem => ({
name: 'tooltip_columns',
config: sharedControls.tooltip_columns,
});
export const TooltipMetricsControl = (): CustomControlItem => ({
name: 'tooltip_metrics',
config: sharedControls.tooltip_metrics,
});
// Format controls
export const YAxisFormatControl = (): CustomControlItem => ({
name: 'y_axis_format',
config: sharedControls.y_axis_format,
});
export const XAxisTimeFormatControl = (): CustomControlItem => ({
name: 'x_axis_time_format',
config: sharedControls.x_axis_time_format,
});
// Hidden controls
export const TemporalColumnsLookupControl = (): CustomControlItem => ({
name: 'temporal_columns_lookup',
config: sharedControls.temporal_columns_lookup,
});
// Inline controls - for creating custom controls with overrides
export const InlineTextControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'TextControl',
...overrides,
},
});
export const InlineSelectControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'SelectControl',
...overrides,
},
});
export const InlineCheckboxControl = (
name: string,
overrides?: Partial<CustomControlItem['config']>,
): CustomControlItem => ({
name,
config: {
type: 'CheckboxControl',
...overrides,
},
});

View File

@@ -1,272 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Switch, Select, Input } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
import FormatControlGroup from './FormatControlGroup';
export interface TableControlsSectionProps {
variant?: 'table' | 'pivot' | 'ag-grid';
showPagination?: boolean;
showCellBars?: boolean;
showTotals?: boolean;
showConditionalFormatting?: boolean;
showTimestampFormat?: boolean;
showAllowHtml?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const TableControlsSection: FC<TableControlsSectionProps> = ({
variant = 'table',
showPagination = false,
showCellBars = false,
showTotals = false,
showConditionalFormatting = true,
showTimestampFormat = false,
showAllowHtml = true,
values = {},
onChange = () => {},
}) => {
const isPivot = variant === 'pivot';
return (
<div className="table-controls-section">
{/* Pagination Controls */}
{showPagination && !isPivot && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.server_pagination || false}
onChange={checked => onChange('server_pagination', checked)}
/>
{t('Server Pagination')}
</label>
<small className="text-muted">
{t('Enable server-side pagination for large datasets')}
</small>
</Col>
</Row>
{values.server_pagination && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Page Length')}</label>
<Select
value={values.server_page_length || 10}
onChange={value => onChange('server_page_length', value)}
style={{ width: '100%' }}
options={[
{ value: 10, label: '10' },
{ value: 25, label: '25' },
{ value: 50, label: '50' },
{ value: 100, label: '100' },
{ value: 200, label: '200' },
]}
/>
<small className="text-muted">
{t('Number of rows per page')}
</small>
</Col>
</Row>
)}
</>
)}
{/* Cell Bars */}
{showCellBars && !isPivot && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_cell_bars || false}
onChange={checked => onChange('show_cell_bars', checked)}
/>
{t('Show Cell Bars')}
</label>
<small className="text-muted">
{t('Display mini bar charts in numeric columns')}
</small>
</Col>
</Row>
)}
{/* Totals */}
{showTotals && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.show_totals || values.rowTotals || false}
onChange={checked => {
if (isPivot) {
onChange('rowTotals', checked);
onChange('colTotals', checked);
} else {
onChange('show_totals', checked);
}
}}
/>
{t('Show Totals')}
</label>
<small className="text-muted">
{isPivot
? t('Show row and column totals')
: t('Show total row at bottom')}
</small>
</Col>
</Row>
)}
{/* Subtotals for Pivot */}
{isPivot && (
<>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.rowSubTotals || false}
onChange={checked => onChange('rowSubTotals', checked)}
/>
{t('Show Row Subtotals')}
</label>
<small className="text-muted">
{t('Show subtotals for row groups')}
</small>
</Col>
</Row>
{values.rowSubTotals && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Subtotal Position')}</label>
<Select
value={values.rowSubtotalPosition || 'bottom'}
onChange={value => onChange('rowSubtotalPosition', value)}
style={{ width: '100%' }}
options={[
{ value: 'top', label: t('Top') },
{ value: 'bottom', label: t('Bottom') },
]}
/>
</Col>
</Row>
)}
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.colSubTotals || false}
onChange={checked => onChange('colSubTotals', checked)}
/>
{t('Show Column Subtotals')}
</label>
<small className="text-muted">
{t('Show subtotals for column groups')}
</small>
</Col>
</Row>
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.transposePivot || false}
onChange={checked => onChange('transposePivot', checked)}
/>
{t('Transpose Pivot')}
</label>
<small className="text-muted">{t('Swap rows and columns')}</small>
</Col>
</Row>
</>
)}
{/* Conditional Formatting */}
{showConditionalFormatting && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Conditional Formatting')}</label>
<Input.TextArea
value={values.conditional_formatting || ''}
onChange={e => onChange('conditional_formatting', e.target.value)}
placeholder={t('Enter conditional formatting rules as JSON')}
rows={4}
/>
<small className="text-muted">
{t('Apply conditional color formatting to cells')}
</small>
</Col>
</Row>
)}
{/* Timestamp Format */}
{showTimestampFormat && !isPivot && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label>{t('Timestamp Format')}</label>
<Input
value={values.table_timestamp_format || ''}
onChange={e => onChange('table_timestamp_format', e.target.value)}
placeholder="%Y-%m-%d %H:%M:%S"
/>
<small className="text-muted">
{t('D3 time format for timestamp columns')}
</small>
</Col>
</Row>
)}
{/* Allow HTML */}
{showAllowHtml && (
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
<Col span={24}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch
checked={values.allow_render_html || false}
onChange={checked => onChange('allow_render_html', checked)}
/>
{t('Allow HTML')}
</label>
<small className="text-muted">
{t(
'Render HTML content in cells (security warning: only enable for trusted data)',
)}
</small>
</Col>
</Row>
)}
{/* Format Controls */}
<div style={{ marginTop: 24 }}>
<h4>{t('Value Formats')}</h4>
<FormatControlGroup
showNumber
showCurrency
showPercentage={isPivot}
showDate={!isPivot}
values={values}
onChange={onChange}
/>
</div>
</div>
);
};
export default TableControlsSection;

View File

@@ -1,237 +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 { FC } from 'react';
import { t } from '@superset-ui/core';
import { Select, Radio } from 'antd';
import { Row, Col } from '@superset-ui/core/components';
import AxisControlSection from './AxisControlSection';
import FormatControlGroup from './FormatControlGroup';
import OpacityControl from './OpacityControl';
import MarkerControlGroup from './MarkerControlGroup';
export interface TimeseriesControlPanelProps {
variant: 'area' | 'bar' | 'line' | 'scatter' | 'smooth' | 'step';
showSeriesType?: boolean;
showStack?: boolean;
showArea?: boolean;
showMarkers?: boolean;
showOpacity?: boolean;
showOrientation?: boolean;
values?: Record<string, any>;
onChange?: (name: string, value: any) => void;
}
const SERIES_TYPE_OPTIONS: Record<string, Array<[string, string]>> = {
line: [
['line', t('Line')],
['scatter', t('Scatter')],
['smooth', t('Smooth')],
],
area: [
['line', t('Line')],
['smooth', t('Smooth Line')],
['start', t('Step - start')],
['middle', t('Step - middle')],
['end', t('Step - end')],
],
step: [
['start', t('Step - start')],
['middle', t('Step - middle')],
['end', t('Step - end')],
],
bar: [],
scatter: [],
smooth: [],
};
const STACK_OPTIONS = [
['stack', t('Stack')],
['stream', t('Stream')],
['expand', t('Expand')],
];
const TimeseriesControlPanel: FC<TimeseriesControlPanelProps> = ({
variant,
showSeriesType = true,
showStack = false,
showArea = false,
showMarkers = true,
showOpacity = false,
showOrientation = false,
values = {},
onChange = () => {},
}) => {
const hasAreaOptions = variant === 'area' || showArea;
const hasBarOptions = variant === 'bar';
const hasLineOptions = variant === 'line' || variant === 'smooth';
return (
<div className="timeseries-control-panel">
{/* Series Type Selection */}
{showSeriesType && SERIES_TYPE_OPTIONS[variant] && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Series Style')}</label>
<Select
value={
values.seriesType ||
(SERIES_TYPE_OPTIONS[variant][0]
? SERIES_TYPE_OPTIONS[variant][0][0]
: 'line')
}
onChange={value => onChange('seriesType', value)}
style={{ width: '100%' }}
options={SERIES_TYPE_OPTIONS[variant].map(
([value, label]: [string, string]) => ({
value,
label,
}),
)}
/>
<small className="text-muted">
{t('Series chart type (line, smooth, step, etc)')}
</small>
</Col>
</Row>
)}
{/* Stack Options */}
{showStack && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Stacking')}</label>
<Select
value={values.stack || null}
onChange={value => onChange('stack', value)}
style={{ width: '100%' }}
allowClear
placeholder={t('No stacking')}
options={STACK_OPTIONS.map(([value, label]) => ({
value,
label,
}))}
/>
<small className="text-muted">
{t('Stack series on top of each other')}
</small>
</Col>
</Row>
)}
{/* Bar Orientation */}
{showOrientation && hasBarOptions && (
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
<Col span={24}>
<label>{t('Bar Orientation')}</label>
<Radio.Group
value={values.orientation || 'vertical'}
onChange={e => onChange('orientation', e.target.value)}
>
<Radio value="vertical">{t('Vertical')}</Radio>
<Radio value="horizontal">{t('Horizontal')}</Radio>
</Radio.Group>
<small
className="text-muted"
style={{ display: 'block', marginTop: 8 }}
>
{t('Orientation of bar chart')}
</small>
</Col>
</Row>
)}
{/* Area Chart Options */}
{hasAreaOptions && (
<div style={{ marginBottom: 24 }}>
<h4>{t('Area Chart')}</h4>
<OpacityControl
name="opacity"
label={t('Area opacity')}
description={t(
'Opacity of area under the line. Set to 0 to disable area.',
)}
value={values.opacity || (variant === 'area' ? 0.2 : 0)}
onChange={value => onChange('opacity', value)}
/>
</div>
)}
{/* Line/Marker Options */}
{showMarkers && (hasLineOptions || variant === 'area') && (
<div style={{ marginBottom: 24 }}>
<h4>{t('Markers')}</h4>
<MarkerControlGroup
values={{
markerEnabled: values.markerEnabled || false,
markerSize: values.markerSize || 6,
}}
onChange={onChange}
/>
</div>
)}
{/* X Axis Controls */}
<div style={{ marginBottom: 24 }}>
<h4>{t('X Axis')}</h4>
<AxisControlSection
axis="x"
showTitle
showFormat
showRotation
showBounds={hasBarOptions}
showTruncate
timeFormat
values={values}
onChange={onChange}
/>
</div>
{/* Y Axis Controls */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Y Axis')}</h4>
<AxisControlSection
axis="y"
showTitle
showFormat
showBounds
showLogarithmic
showMinorTicks
showTruncate
values={values}
onChange={onChange}
/>
</div>
{/* Value Formats */}
<div style={{ marginBottom: 24 }}>
<h4>{t('Value Formats')}</h4>
<FormatControlGroup
showNumber
showCurrency={hasBarOptions}
showDate
showPercentage={showStack}
values={values}
onChange={onChange}
/>
</div>
</div>
);
};
export default TimeseriesControlPanel;

View File

@@ -16,42 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import RadioButtonControl from './RadioButtonControl';
export * from './RadioButtonControl';
export { default as RadioButtonControl } from './RadioButtonControl';
export { default as GranularityControl } from './GranularityControl';
export * from './ReactControlPanel';
export { default as AxisControlSection } from './AxisControlSection';
export { default as FormatControlGroup } from './FormatControlGroup';
export { default as OpacityControl } from './OpacityControl';
export { default as MarkerControlGroup } from './MarkerControlGroup';
export { default as TimeseriesControlPanel } from './TimeseriesControlPanel';
export { default as LabelControlGroup } from './LabelControlGroup';
export { default as PieShapeControl } from './PieShapeControl';
export { default as TableControlsSection } from './TableControlsSection';
export { default as FilterControlsSection } from './FilterControlsSection';
export { default as DeckGLControlsSection } from './DeckGLControlsSection';
// Export ControlComponents with specific names
// ColorPickerControl from ControlComponents takes props, renamed to avoid conflict
export {
CheckboxControl,
NumberControl,
SelectControl,
SliderControl,
SwitchControl,
TextAreaControl,
TextControl,
ColorPickerControl as ColorPickerControlWithProps,
type ControlComponentConfig,
} from './ControlComponents';
// Export all SharedControlComponents which replace string references
// ColorPickerControl from here does NOT take props - it's for the shared 'color_picker' control
export * from './SharedControlComponents';
// Export React control panel
export { ReactControlPanel } from './ReactControlPanel';
// Export control panel layout components
export * from './ControlPanelLayout';
// Inline control functions are exported from SharedControlComponents
/**
* Shared chart controls. Can be referred via string shortcuts in chart control
* configs.
*/
export default {
RadioButtonControl,
};

View File

@@ -17,8 +17,8 @@
* under the License.
*/
export { default as sharedControls } from './sharedControls';
// sharedControlComponents is deprecated - import components directly instead
// export { default as sharedControlComponents } from './components';
// React control components
export { default as sharedControlComponents } from './components';
export { aggregationControl } from './customControls';
export * from './components';
export * from './customControls';

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'> = {

View File

@@ -32,7 +32,7 @@ import type {
QueryFormMetric,
QueryResponse,
} from '@superset-ui/core';
import { sharedControls } from './shared-controls';
import { sharedControls, sharedControlComponents } from './shared-controls';
export type { Metric } from '@superset-ui/core';
export type { ControlComponentProps } from './shared-controls/components/types';
@@ -45,6 +45,8 @@ interface Action {
interface AnyAction extends Action, AnyDict {}
export type SharedControls = typeof sharedControls;
export type SharedControlAlias = keyof typeof sharedControls;
export type SharedControlComponents = typeof sharedControlComponents;
/** ----------------------------------------------
* Input data/props while rendering
@@ -183,7 +185,7 @@ export type InternalControlType =
| 'Select'
| 'Slider'
| 'Input'
| 'RadioButtonControl';
| keyof SharedControlComponents; // expanded in `expandControlConfig`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ControlType = InternalControlType | ComponentType<any>;
@@ -357,7 +359,7 @@ export type SharedSectionAlias =
| 'NVD3TimeSeries';
export interface OverrideSharedControlItem<
A extends keyof SharedControls = keyof SharedControls,
A extends SharedControlAlias = SharedControlAlias,
> {
name: A;
override: Partial<SharedControls[A]>;
@@ -380,16 +382,16 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
// interfere with other ControlSetItem types
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
// All controls must be React components or control configuration objects
export type ControlSetItem = OverrideSharedControlItem | ExpandedControlItem;
export type ControlSetItem =
| SharedControlAlias
| OverrideSharedControlItem
| ExpandedControlItem;
export type ControlSetRow = ControlSetItem[];
// Ref:
// - superset-frontend/src/explore/components/ControlPanelsContainer.jsx
// - superset-frontend/src/explore/components/ControlPanelSection.jsx
// DEPRECATED: Legacy control panel types - use JsonFormsControlPanelConfig instead
// These are kept temporarily for backward compatibility during migration
export interface ControlPanelSectionConfig {
label?: ReactNode;
description?: ReactNode;
@@ -426,7 +428,6 @@ export const isStandardizedFormData = (
Array.isArray(formData.standardizedFormData.controls.metrics) &&
Array.isArray(formData.standardizedFormData.controls.columns);
// DEPRECATED: Use JsonFormsControlPanelConfig from './types/jsonForms' instead
export interface ControlPanelConfig {
controlPanelSections: (ControlPanelSectionConfig | null)[];
controlOverrides?: ControlOverrides;
@@ -436,7 +437,7 @@ export interface ControlPanelConfig {
}
export type ControlOverrides = {
[P in keyof SharedControls]?: Partial<SharedControls[P]>;
[P in SharedControlAlias]?: Partial<SharedControls[P]>;
};
export type SectionOverrides = {

View File

@@ -1,20 +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.
*/
// Type exports placeholder

View File

@@ -17,15 +17,28 @@
* under the License.
*/
import { isValidElement, ReactElement } from 'react';
import { sharedControls } from '../shared-controls';
import { sharedControls, sharedControlComponents } from '../shared-controls';
import {
ControlType,
ControlSetItem,
ExpandedControlItem,
ControlOverrides,
} from '../types';
export function expandControlType(controlType: ControlType) {
if (
typeof controlType === 'string' &&
controlType in sharedControlComponents
) {
return sharedControlComponents[
controlType as keyof typeof sharedControlComponents
];
}
return controlType;
}
/**
* Expand a control config item to full config in the format of
* Expand a shorthand control config item to full config in the format of
* {
* name: ...,
* config: {
@@ -33,31 +46,26 @@ import {
* ...
* }
* }
*
* Note: String references to shared controls are no longer supported.
* All controls must be React components or control configuration objects.
*/
export function expandControlConfig(
control: ControlSetItem,
controlOverrides: ControlOverrides = {},
): ExpandedControlItem {
// one of the named shared controls
if (typeof control === 'string' && control in sharedControls) {
const name = control;
return {
name,
config: {
...sharedControls[name],
...controlOverrides[name],
},
};
}
// JSX/React element or NULL
if (!control || isValidElement(control)) {
if (!control || typeof control === 'string' || isValidElement(control)) {
return control as ReactElement;
}
// Check if it's a modern panel component (function with isModernPanel flag)
if (typeof control === 'function' && (control as any).isModernPanel) {
console.log('expandControlConfig - Found modern panel, returning as-is');
return control as any;
}
// String controls are no longer supported - they must be migrated to React components
if (typeof control === 'string') {
throw new Error(
`String control reference "${control}" is not supported. ` +
`Use the corresponding React component from @superset-ui/chart-controls instead. ` +
`For example, replace ['metrics'] with [MetricsControl()].`,
);
}
// already fully expanded control config, e.g.
// {
// name: 'metric',
@@ -66,7 +74,13 @@ export function expandControlConfig(
// }
// }
if ('name' in control && 'config' in control) {
return control;
return {
...control,
config: {
...control.config,
type: expandControlType(control.config.type as ControlType),
},
};
}
// apply overrides with shared controls
if ('override' in control && control.name in sharedControls) {

View File

@@ -1,330 +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 {
MetricsControl,
MetricControl,
GroupByControl,
ColumnsControl,
AdhocFiltersControl,
LimitControl,
RowLimitControl,
OrderByControl,
OrderDescControl,
SeriesControl,
EntityControl,
XControl,
YControl,
SizeControl,
ColorSchemeControl,
LinearColorSchemeControl,
ColorPickerControl,
TimeRangeControl,
GranularitySqlaControl,
TimeGrainSqlaControl,
DatasourceControl,
VizTypeControl,
XAxisControl,
YAxisFormatControl,
XAxisTimeFormatControl,
ZoomableControl,
SortByMetricControl,
CurrencyFormatControl,
TooltipColumnsControl,
TooltipMetricsControl,
sharedControls,
} from '../../src';
describe('SharedControlComponents', () => {
describe('React Component Controls', () => {
it('should return proper control items for metrics controls', () => {
const metricsControl = MetricsControl();
expect(metricsControl).toEqual({
name: 'metrics',
config: sharedControls.metrics,
});
const metricControl = MetricControl();
expect(metricControl).toEqual({
name: 'metric',
config: sharedControls.metric,
});
});
it('should return proper control items for dimension controls', () => {
const groupByControl = GroupByControl();
expect(groupByControl).toEqual({
name: 'groupby',
config: sharedControls.groupby,
});
const columnsControl = ColumnsControl();
expect(columnsControl).toEqual({
name: 'columns',
config: sharedControls.columns,
});
const seriesControl = SeriesControl();
expect(seriesControl).toEqual({
name: 'series',
config: sharedControls.series,
});
const entityControl = EntityControl();
expect(entityControl).toEqual({
name: 'entity',
config: sharedControls.entity,
});
});
it('should return proper control items for filter controls', () => {
const adhocFiltersControl = AdhocFiltersControl();
expect(adhocFiltersControl).toEqual({
name: 'adhoc_filters',
config: sharedControls.adhoc_filters,
});
});
it('should return proper control items for limit controls', () => {
const limitControl = LimitControl();
expect(limitControl).toEqual({
name: 'limit',
config: sharedControls.limit,
});
const rowLimitControl = RowLimitControl();
expect(rowLimitControl).toEqual({
name: 'row_limit',
config: sharedControls.row_limit,
});
});
it('should return proper control items for sort controls', () => {
const orderByControl = OrderByControl();
expect(orderByControl).toEqual({
name: 'orderby',
config: sharedControls.orderby,
});
const orderDescControl = OrderDescControl();
expect(orderDescControl).toEqual({
name: 'order_desc',
config: sharedControls.order_desc,
});
const sortByMetricControl = SortByMetricControl();
expect(sortByMetricControl).toEqual({
name: 'sort_by_metric',
config: sharedControls.sort_by_metric,
});
});
it('should return proper control items for axis controls', () => {
const xControl = XControl();
expect(xControl).toEqual({
name: 'x',
config: sharedControls.x,
});
const yControl = YControl();
expect(yControl).toEqual({
name: 'y',
config: sharedControls.y,
});
const xAxisControl = XAxisControl();
expect(xAxisControl).toEqual({
name: 'x_axis',
config: sharedControls.x_axis,
});
// Note: YAxisControl doesn't exist, YControl is reused for y axis
const yControl2 = YControl();
expect(yControl2).toEqual({
name: 'y',
config: sharedControls.y,
});
});
it('should return proper control items for formatting controls', () => {
const yAxisFormatControl = YAxisFormatControl();
expect(yAxisFormatControl).toEqual({
name: 'y_axis_format',
config: sharedControls.y_axis_format,
});
const xAxisTimeFormatControl = XAxisTimeFormatControl();
expect(xAxisTimeFormatControl).toEqual({
name: 'x_axis_time_format',
config: sharedControls.x_axis_time_format,
});
const currencyFormatControl = CurrencyFormatControl();
expect(currencyFormatControl).toEqual({
name: 'currency_format',
config: sharedControls.currency_format,
});
});
it('should return proper control items for color controls', () => {
const colorSchemeControl = ColorSchemeControl();
expect(colorSchemeControl).toEqual({
name: 'color_scheme',
config: sharedControls.color_scheme,
});
const linearColorSchemeControl = LinearColorSchemeControl();
expect(linearColorSchemeControl).toEqual({
name: 'linear_color_scheme',
config: sharedControls.linear_color_scheme,
});
const colorPickerControl = ColorPickerControl();
expect(colorPickerControl).toEqual({
name: 'color_picker',
config: sharedControls.color_picker,
});
});
it('should return proper control items for time controls', () => {
const timeRangeControl = TimeRangeControl();
expect(timeRangeControl).toEqual({
name: 'time_range',
config: sharedControls.time_range,
});
const granularitySqlaControl = GranularitySqlaControl();
expect(granularitySqlaControl).toEqual({
name: 'granularity_sqla',
config: sharedControls.granularity_sqla,
});
const timeGrainSqlaControl = TimeGrainSqlaControl();
expect(timeGrainSqlaControl).toEqual({
name: 'time_grain_sqla',
config: sharedControls.time_grain_sqla,
});
});
it('should return proper control items for datasource controls', () => {
const datasourceControl = DatasourceControl();
expect(datasourceControl).toEqual({
name: 'datasource',
config: sharedControls.datasource,
});
const vizTypeControl = VizTypeControl();
expect(vizTypeControl).toEqual({
name: 'viz_type',
config: sharedControls.viz_type,
});
});
it('should return proper control items for tooltip controls', () => {
const tooltipColumnsControl = TooltipColumnsControl();
expect(tooltipColumnsControl).toEqual({
name: 'tooltip_columns',
config: sharedControls.tooltip_columns,
});
const tooltipMetricsControl = TooltipMetricsControl();
expect(tooltipMetricsControl).toEqual({
name: 'tooltip_metrics',
config: sharedControls.tooltip_metrics,
});
});
it('should return proper control items for other controls', () => {
const sizeControl = SizeControl();
expect(sizeControl).toEqual({
name: 'size',
config: sharedControls.size,
});
const zoomableControl = ZoomableControl();
expect(zoomableControl).toEqual({
name: 'zoomable',
config: sharedControls.zoomable,
});
});
});
describe('Control compatibility', () => {
it('should be usable in control panel configurations', () => {
// Simulate a control panel configuration
const controlPanel = {
controlPanelSections: [
{
label: 'Query',
expanded: true,
controlSetRows: [
[MetricsControl()],
[GroupByControl()],
[AdhocFiltersControl()],
[LimitControl(), OrderDescControl()],
[RowLimitControl()],
],
},
{
label: 'Chart Options',
expanded: true,
controlSetRows: [[ColorSchemeControl()], [YAxisFormatControl()]],
},
],
};
// Verify structure
expect(controlPanel.controlPanelSections).toHaveLength(2);
// Verify first section
const querySection = controlPanel.controlPanelSections[0];
expect(querySection.controlSetRows[0][0]).toHaveProperty(
'name',
'metrics',
);
expect(querySection.controlSetRows[1][0]).toHaveProperty(
'name',
'groupby',
);
expect(querySection.controlSetRows[2][0]).toHaveProperty(
'name',
'adhoc_filters',
);
expect(querySection.controlSetRows[3][0]).toHaveProperty('name', 'limit');
expect(querySection.controlSetRows[3][1]).toHaveProperty(
'name',
'order_desc',
);
expect(querySection.controlSetRows[4][0]).toHaveProperty(
'name',
'row_limit',
);
// Verify second section
const optionsSection = controlPanel.controlPanelSections[1];
expect(optionsSection.controlSetRows[0][0]).toHaveProperty(
'name',
'color_scheme',
);
expect(optionsSection.controlSetRows[1][0]).toHaveProperty(
'name',
'y_axis_format',
);
});
});
});

View File

@@ -20,19 +20,15 @@ import {
expandControlConfig,
sharedControls,
CustomControlItem,
sharedControlComponents,
} from '../../src';
describe('expandControlConfig()', () => {
it('throws error when string control is passed', () => {
expect(() => expandControlConfig('metrics' as any)).toThrow(
'String control reference "metrics" is not supported',
);
expect(() => expandControlConfig('groupby' as any)).toThrow(
'String control reference "groupby" is not supported',
);
expect(() => expandControlConfig('columns' as any)).toThrow(
'String control reference "columns" is not supported',
);
it('expands shared control alias', () => {
expect(expandControlConfig('metrics')).toEqual({
name: 'metrics',
config: sharedControls.metrics,
});
});
it('expands control with overrides', () => {
@@ -73,7 +69,7 @@ describe('expandControlConfig()', () => {
};
expect(
(expandControlConfig(input) as CustomControlItem).config.type,
).toEqual('RadioButtonControl');
).toEqual(sharedControlComponents.RadioButtonControl);
});
it('leave NULL and ReactElement untouched', () => {
@@ -82,6 +78,11 @@ describe('expandControlConfig()', () => {
expect(expandControlConfig(input)).toBe(input);
});
it('leave unknown text untouched', () => {
const input = 'superset-ui';
expect(expandControlConfig(input as never)).toBe(input);
});
it('return null for invalid configs', () => {
expect(
expandControlConfig({ type: 'SelectControl', label: 'Hello' } as never),

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

@@ -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

@@ -76,7 +76,6 @@ export { CronPicker, type CronError } from './CronPicker';
export * from './DatePicker';
export { DeleteModal, type DeleteModalProps } from './DeleteModal';
export { Divider, type DividerProps } from './Divider';
export { Drawer, type DrawerProps } from './Drawer';
export {
Dropdown,
MenuDotsDropdown,

View File

@@ -49,21 +49,6 @@ describe('isProbablyHTML', () => {
const trickyText = 'a <= 10 and b > 10';
expect(isProbablyHTML(trickyText)).toBe(false);
});
it('should return false for strings with angle brackets that are not HTML', () => {
// Test case from issue #25561
expect(isProbablyHTML('<abcdef:12345>')).toBe(false);
// Other similar cases
expect(isProbablyHTML('<foo:bar>')).toBe(false);
expect(isProbablyHTML('<123>')).toBe(false);
expect(isProbablyHTML('<test@example.com>')).toBe(false);
expect(isProbablyHTML('<custom-element>')).toBe(false);
// Mathematical expressions
expect(isProbablyHTML('if x < 5 and y > 10')).toBe(false);
expect(isProbablyHTML('price < $100')).toBe(false);
});
});
describe('sanitizeHtmlIfNeeded', () => {

View File

@@ -68,87 +68,9 @@ export function isProbablyHTML(text: string) {
return true;
}
// Check if the string contains common HTML patterns
if (!hasHtmlTagPattern(text)) {
return false;
}
const parser = new DOMParser();
const doc = parser.parseFromString(cleanedStr, 'text/html');
// Check if parsing created actual HTML elements (not just text nodes)
const elements = Array.from(doc.body.childNodes).filter(
node => node.nodeType === 1,
) as Element[];
// If no elements were created, it's not HTML
if (elements.length === 0) {
return false;
}
// Check if the elements are known HTML tags (not custom/unknown tags)
// This prevents strings like "<abcdef:12345>" from being treated as HTML
return elements.some(element => {
const tagName = element.tagName.toLowerCase();
// List of common HTML tags we want to recognize
const knownHtmlTags = [
'div',
'span',
'p',
'a',
'b',
'i',
'u',
'em',
'strong',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'table',
'tr',
'td',
'th',
'tbody',
'thead',
'tfoot',
'ul',
'ol',
'li',
'img',
'br',
'hr',
'pre',
'code',
'blockquote',
'section',
'article',
'nav',
'header',
'footer',
'form',
'input',
'button',
'select',
'option',
'textarea',
'label',
'fieldset',
'legend',
'video',
'audio',
'canvas',
'iframe',
'script',
'style',
'link',
'meta',
'title',
];
return knownHtmlTags.includes(tagName);
});
return Array.from(doc.body.childNodes).some(({ nodeType }) => nodeType === 1);
}
export function sanitizeHtmlIfNeeded(htmlString: string) {

View File

@@ -43,7 +43,7 @@
"@storybook/types": "8.4.7",
"@types/react-loadable": "^5.5.11",
"core-js": "3.40.0",
"gh-pages": "^6.3.0",
"gh-pages": "^6.2.0",
"jquery": "^3.7.1",
"memoize-one": "^5.2.1",
"react": "^17.0.2",

View File

@@ -22,15 +22,6 @@ import {
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
getStandardizedControls,
AdhocFiltersControl,
GranularitySqlaControl,
LinearColorSchemeControl,
MetricsControl,
TimeRangeControl,
YAxisFormatControl,
InlineSelectControl,
InlineTextControl,
InlineCheckboxControl,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
@@ -39,43 +30,51 @@ const config: ControlPanelConfig = {
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [[GranularitySqlaControl()], [TimeRangeControl()]],
controlSetRows: [['granularity_sqla'], ['time_range']],
},
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
InlineSelectControl('domain_granularity', {
label: t('Domain'),
default: 'month',
choices: [
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
['year', t('year')],
],
description: t('The time unit used for the grouping of blocks'),
}),
InlineSelectControl('subdomain_granularity', {
label: t('Subdomain'),
default: 'day',
choices: [
['min', t('min')],
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
],
description: t(
'The time unit for each block. Should be a smaller unit than ' +
'domain_granularity. Should be larger or equal to Time Grain',
),
}),
{
name: 'domain_granularity',
config: {
type: 'SelectControl',
label: t('Domain'),
default: 'month',
choices: [
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
['year', t('year')],
],
description: t('The time unit used for the grouping of blocks'),
},
},
{
name: 'subdomain_granularity',
config: {
type: 'SelectControl',
label: t('Subdomain'),
default: 'day',
choices: [
['min', t('min')],
['hour', t('hour')],
['day', t('day')],
['week', t('week')],
['month', t('month')],
],
description: t(
'The time unit for each block. Should be a smaller unit than ' +
'domain_granularity. Should be larger or equal to Time Grain',
),
},
},
],
[MetricsControl()],
[AdhocFiltersControl()],
['metrics'],
['adhoc_filters'],
],
},
{
@@ -83,77 +82,109 @@ const config: ControlPanelConfig = {
expanded: true,
tabOverride: 'customize',
controlSetRows: [
[LinearColorSchemeControl()],
['linear_color_scheme'],
[
InlineTextControl('cell_size', {
label: t('Cell Size'),
default: 10,
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
description: t('The size of the square cell, in pixels'),
}),
InlineTextControl('cell_padding', {
label: t('Cell Padding'),
default: 2,
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
description: t('The distance between cells, in pixels'),
}),
{
name: 'cell_size',
config: {
type: 'TextControl',
isInt: true,
default: 10,
validators: [legacyValidateInteger],
renderTrigger: true,
label: t('Cell Size'),
description: t('The size of the square cell, in pixels'),
},
},
{
name: 'cell_padding',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 2,
label: t('Cell Padding'),
description: t('The distance between cells, in pixels'),
},
},
],
[
InlineTextControl('cell_radius', {
label: t('Cell Radius'),
default: 0,
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
description: t('The pixel radius'),
}),
InlineTextControl('steps', {
label: t('Color Steps'),
default: 10,
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
description: t('The number color "steps"'),
}),
{
name: 'cell_radius',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 0,
label: t('Cell Radius'),
description: t('The pixel radius'),
},
},
{
name: 'steps',
config: {
type: 'TextControl',
isInt: true,
validators: [legacyValidateInteger],
renderTrigger: true,
default: 10,
label: t('Color Steps'),
description: t('The number color "steps"'),
},
},
],
[
YAxisFormatControl(),
InlineSelectControl('x_axis_time_format', {
label: t('Time Format'),
default: 'smart_date',
freeForm: true,
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
}),
'y_axis_format',
{
name: 'x_axis_time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Time Format'),
renderTrigger: true,
default: 'smart_date',
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
],
[
InlineCheckboxControl('show_legend', {
label: t('Legend'),
default: true,
renderTrigger: true,
description: t('Whether to display the legend (toggles)'),
}),
InlineCheckboxControl('show_values', {
label: t('Show Values'),
default: false,
renderTrigger: true,
description: t(
'Whether to display the numerical values within the cells',
),
}),
{
name: 'show_legend',
config: {
type: 'CheckboxControl',
label: t('Legend'),
renderTrigger: true,
default: true,
description: t('Whether to display the legend (toggles)'),
},
},
{
name: 'show_values',
config: {
type: 'CheckboxControl',
label: t('Show Values'),
renderTrigger: true,
default: false,
description: t(
'Whether to display the numerical values within the cells',
),
},
},
],
[
InlineCheckboxControl('show_metric_name', {
label: t('Show Metric Names'),
default: true,
renderTrigger: true,
description: t('Whether to display the metric name as a title'),
}),
{
name: 'show_metric_name',
config: {
type: 'CheckboxControl',
label: t('Show Metric Names'),
renderTrigger: true,
default: true,
description: t('Whether to display the metric name as a title'),
},
},
null,
],
],

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