Compare commits

...

40 Commits

Author SHA1 Message Date
GitHub Action
f3fb775a17 chore(🦾): bump python python-dotenv 1.1.0 -> 1.1.1 2025-07-28 19:12:51 +00:00
Maxime Beauchemin
604d72cc98 feat: introducing a docker-compose-light.yml for lighter development (#34324) 2025-07-28 09:27:07 -07:00
Enzo Martellucci
913e068113 style(FastVizSwitcher): Adjust padding for FastVizSwitcher selector (#34317) 2025-07-28 14:39:10 +03:00
Geido
1a4e2173f5 fix(NavBar): Add brand text back (#34318) 2025-07-28 12:19:14 +03:00
Ian McEwen
c49789167b style(chart): restyle table pagination (#34311) 2025-07-27 19:39:10 -07:00
Maxime Beauchemin
1be2287b3a feat(timeseries): enhance 'Series Limit' to support grouping the long tail (#34308) 2025-07-25 16:26:32 -07:00
Maxime Beauchemin
e741a3167f feat: add a theme CRUD page to manage themes (#34182)
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-25 13:26:41 -07:00
Michael S. Molina
5f11f9097a fix: Charts list is displaying empty dataset names when there's no schema (#34315) 2025-07-25 14:07:50 -03:00
Jan Suleiman
8783579aa8 fix(cartodiagram): add missing locales for rendering echarts (#34268) 2025-07-25 09:59:28 -07:00
Evan Rusackas
c25b4221f8 fix(npm): more reliable execution of npm run update-maps (#34305) 2025-07-25 13:48:05 -03:00
Pius Iniobong
9c771fb2ba fix: preserve correct column order when table layout is changed with time comparison enabled (#34300) 2025-07-25 15:31:33 +03:00
sha174n
7f44992c4b fix: enhance disallowed SQL functions list for improved security (#33084) 2025-07-24 16:36:32 -07:00
Beto Dealmeida
8df5860826 chore: bump sqlglot to latest version (27.3.0) (#34302) 2025-07-24 15:38:29 -07:00
Beto Dealmeida
b794b192d1 fix: return 422 on invalid SQL (#34303) 2025-07-24 16:40:56 -04:00
Maxime Beauchemin
3177131d52 feat: re-order CRUD list view action buttons (#34294) 2025-07-24 12:46:34 -07:00
Enzo Martellucci
89bf77b5c9 fix(theming): Fix visual regressions from theming P7 (#34237) 2025-07-24 19:57:50 +02:00
Maxime Beauchemin
30e5684006 fix: address numerous long-standing console errors (python & web) (#34299) 2025-07-24 09:50:26 -07:00
Maxime Beauchemin
3f8472ca7b chore: move some rules from ruff -> pylint (#34292) 2025-07-24 09:40:49 -07:00
Beto Dealmeida
efa8cb6fa4 chore: improve sqlglot parsing (#34270) 2025-07-24 10:50:59 -04:00
Beto Dealmeida
ab59b7e9b0 feat: make SupersetClient retry on 502-504 (#34290)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-24 10:46:50 -04:00
Vitor Avila
c99843b13a fix: Hide View in SQL Lab for users without access (#34293) 2025-07-24 10:45:31 -03:00
Fardin Mustaque
da55a6c94a fix(chart-download): ensure full table or handlebar chart is captured in image export (#34233) 2025-07-24 15:47:44 +03:00
LisaHusband
7a1c056374 fix(charting): correctly categorize numeric columns with NULL values (#34213) 2025-07-24 15:46:58 +03:00
Michael S. Molina
1e5a4e9bdc fix: Saved queries list break if one query can't be parsed (#34289) 2025-07-24 08:30:04 -03:00
Đỗ Trọng Hải
9b88527883 chore: remove supposedly dev dep html-webpack-plugin from lockfile (#34288) 2025-07-24 15:53:16 +07:00
dependabot[bot]
800c1639ec chore(deps-dev): bump prettier from 3.5.3 to 3.6.2 in /superset-frontend (#33997) 2025-07-24 09:38:00 +07:00
Ahmed Habeeb
43775e9373 fix(sqllab_export): manually encode CSV output to support utf-8-sig (#34235) 2025-07-23 18:44:56 -07:00
Maxime Beauchemin
9099b0f00d fix: fix the pre-commit hook for tsc (#34275)
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-23 13:21:54 -07:00
dependabot[bot]
77ffe65773 chore(deps): bump axios from 1.10.0 to 1.11.0 in /docs (#34285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 00:06:53 +07:00
Damian Pendrak
32f8f33a4f fix(deckgl): fix deck.gl color breakpoints Control (#34244) 2025-07-23 19:25:29 +03:00
Enzo Martellucci
710c277681 style(Button): Vertically align icons across all buttons (#34067) 2025-07-23 19:24:55 +03:00
Michael S. Molina
11324607d0 fix: Bulk select is not respecting the TAGGING_SYSTEM feature flag (#34282) 2025-07-23 11:33:06 -03:00
Mehmet Salih Yavuz
9c6271136d fix(theming): Visual regressions p2 (#34279) 2025-07-23 16:14:06 +02:00
Maxime Beauchemin
c444eed63e chore(docker): use editable mode in docker images (#34146) 2025-07-22 16:14:31 -07:00
Mehmet Salih Yavuz
1df5e59fdf fix(theming): Theming visual fixes (#34253) 2025-07-23 01:55:32 +03:00
Maxime Beauchemin
77f66e7434 fix: build issues on master with 'npm run dev' (#34272) 2025-07-22 15:55:18 -07:00
Maxime Beauchemin
2c81eb6c39 feat(docker): do not include chromium (headless browser) by default in Dockerfile (#34258) 2025-07-22 13:12:55 -07:00
Maxime Beauchemin
09c4afc894 feat: introduce comprehensive LLM context guides for AI-powered development (#34194)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-22 12:22:13 -07:00
JUST.in DO IT
229d92590a fix: Matching errorType on superset api error with SupersetError (#34261) 2025-07-22 11:51:42 -07:00
Kamil Gabryjelski
f4f516c64c fix: Missing ownState and isCached props in Chart.jsx (#34259) 2025-07-22 18:58:28 +02:00
280 changed files with 10112 additions and 2058 deletions

View File

@@ -0,0 +1,125 @@
---
description: Apache Superset development standards and guidelines for Cursor IDE
globs: ["**/*.py", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.sql", "**/*.md"]
alwaysApply: true
---
# Apache Superset Development Standards for Cursor IDE
Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
**These migrations are actively happening - avoid deprecated patterns:**
### Frontend Modernization
- **NO `any` types** - Use proper TypeScript types
- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
- **NO Enzyme** - Use React Testing Library/Jest (Enzyme fully removed)
- **Use @superset-ui/core** - Don't import Ant Design directly
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over Cypress end-to-end tests
- **Cypress is last resort** - Actively moving away from Cypress
- **Use Jest + React Testing Library** for component testing
### Backend Type Safety
- **Add type hints** - All new Python code needs proper typing
- **MyPy compliance** - Run `pre-commit run mypy` to validate
- **SQLAlchemy typing** - Use proper model annotations
## Code Standards
### TypeScript Frontend
- **NO `any` types** - Use proper TypeScript
- **Functional components** with hooks
- **@superset-ui/core** for UI components (not direct antd)
- **Jest** for testing (NO Enzyme)
- **Redux** for global state, hooks for local
### Python Backend
- **Type hints required** for all new code
- **MyPy compliant** - run `pre-commit run mypy`
- **SQLAlchemy models** with proper typing
- **pytest** for testing
### Apache License Headers
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
## Key Directory Structure
```
superset/
├── superset/ # Python backend (Flask, SQLAlchemy)
│ ├── views/api/ # REST API endpoints
│ ├── models/ # Database models
│ └── connectors/ # Database connections
├── superset-frontend/src/ # React TypeScript frontend
│ ├── components/ # Reusable components
│ ├── explore/ # Chart builder
│ ├── dashboard/ # Dashboard interface
│ └── SqlLab/ # SQL editor
├── superset-frontend/packages/
│ └── superset-ui-core/ # UI component library (USE THIS)
├── tests/ # Python/integration tests
├── docs/ # Documentation (UPDATE FOR CHANGES)
└── UPDATING.md # Breaking changes log
```
## Architecture Patterns
### Dataset-Centric Approach
Charts built from enriched datasets containing:
- Dimension columns with labels/descriptions
- Predefined metrics as SQL expressions
- Self-service analytics within defined contexts
### Security & Features
- **RBAC**: Role-based access via Flask-AppBuilder
- **Feature flags**: Control feature rollouts
- **Row-level security**: SQL-based data access control
## Test Utilities
### Python Test Helpers
- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
- **`@with_config`** - Config mocking decorator
- **`@with_feature_flags`** - Feature flag testing
- **`login_as()`, `login_as_admin()`** - Authentication helpers
- **`create_dashboard()`, `create_slice()`** - Data setup utilities
### TypeScript Test Helpers
- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
- **`createWrapper()`** - Redux/Router/Theme wrapper
- **`selectOption()`** - Select component helper
- **React Testing Library** - NO Enzyme (removed)
## Pre-commit Validation
**Use pre-commit hooks for quality validation:**
```bash
# Install hooks
pre-commit install
# Quick validation (faster than --all-files)
pre-commit run # Staged files only
pre-commit run mypy # Python type checking
pre-commit run prettier # Code formatting
pre-commit run eslint # Frontend linting
```
## Development Guidelines
- **Documentation**: Update docs/ for any user-facing changes
- **Breaking Changes**: Add to UPDATING.md
- **Docstrings**: Required for new functions/classes
- **Follow existing patterns**: Mimic code style, use existing libraries and utilities
- **Type Safety**: This codebase is actively modernizing toward full TypeScript and type safety
- **Always run `pre-commit run`** to validate changes before committing
---
**Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.

1
.github/copilot-instructions.md vendored Symbolic link
View File

@@ -0,0 +1 @@
../LLMS.md

View File

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

4
.gitignore vendored
View File

@@ -127,5 +127,7 @@ docker/*local*
# Jest test report
test-report.html
superset/static/stats/statistics.html
.aider*
# LLM-related
CLAUDE.local.md
.aider*

View File

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

View File

@@ -76,3 +76,11 @@ ydb.svg
erd.puml
erd.svg
intro_header.txt
# for LLMs
llm-context.md
LLMS.md
CLAUDE.md
CURSOR.md
GEMINI.md
GPT.md

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
LLMS.md

View File

@@ -167,7 +167,7 @@ RUN mkdir -p \
&& touch superset/static/version_info.json
# Install Playwright and optionally setup headless browsers
ARG INCLUDE_CHROMIUM="true"
ARG INCLUDE_CHROMIUM="false"
ARG INCLUDE_FIREFOX="false"
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
if [ "$INCLUDE_CHROMIUM" = "true" ] || [ "$INCLUDE_FIREFOX" = "true" ]; then \
@@ -223,7 +223,7 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
/app/docker/pip-install.sh --requires-build-essential -r requirements/base.txt
# Install the superset package
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install .
uv pip install -e .
RUN python -m compileall /app/superset
USER superset
@@ -246,7 +246,7 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
/app/docker/pip-install.sh --requires-build-essential -r requirements/development.txt
# Install the superset package
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install .
uv pip install -e .
RUN uv pip install .[postgres]
RUN python -m compileall /app/superset

1
GEMINI.md Symbolic link
View File

@@ -0,0 +1 @@
LLMS.md

1
GPT.md Symbolic link
View File

@@ -0,0 +1 @@
LLMS.md

191
LLMS.md Normal file
View File

@@ -0,0 +1,191 @@
# LLM Context Guide for Apache Superset
Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
**These migrations are actively happening - avoid deprecated patterns:**
### Frontend Modernization
- **NO `any` types** - Use proper TypeScript types
- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
- **Use @superset-ui/core** - Don't import Ant Design directly
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over Cypress end-to-end tests
- **Cypress is last resort** - Actively moving away from Cypress
- **Use Jest + React Testing Library** for component testing
### Backend Type Safety
- **Add type hints** - All new Python code needs proper typing
- **MyPy compliance** - Run `pre-commit run mypy` to validate
- **SQLAlchemy typing** - Use proper model annotations
### UUID Migration
- **Prefer UUIDs over auto-incrementing IDs** - New models should use UUID primary keys
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
## Key Directories
```
superset/
├── superset/ # Python backend (Flask, SQLAlchemy)
│ ├── views/api/ # REST API endpoints
│ ├── models/ # Database models
│ └── connectors/ # Database connections
├── superset-frontend/src/ # React TypeScript frontend
│ ├── components/ # Reusable components
│ ├── explore/ # Chart builder
│ ├── dashboard/ # Dashboard interface
│ └── SqlLab/ # SQL editor
├── superset-frontend/packages/
│ └── superset-ui-core/ # UI component library (USE THIS)
├── tests/ # Python/integration tests
├── docs/ # Documentation (UPDATE FOR CHANGES)
└── UPDATING.md # Breaking changes log
```
## Code Standards
### TypeScript Frontend
- **Avoid `any` types** - Use proper TypeScript, reuse existing types
- **Functional components** with hooks
- **@superset-ui/core** for UI components (not direct antd)
- **Jest** for testing (NO Enzyme)
- **Redux** for global state where it exists, hooks for local
### Python Backend
- **Type hints required** for all new code
- **MyPy compliant** - run `pre-commit run mypy`
- **SQLAlchemy models** with proper typing
- **pytest** for testing
### Apache License Headers
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
## Documentation Requirements
- **docs/**: Update for any user-facing changes
- **UPDATING.md**: Add breaking changes here
- **Docstrings**: Required for new functions/classes
## Architecture Patterns
### Security & Features
- **RBAC**: Role-based access via Flask-AppBuilder
- **Feature flags**: Control feature rollouts
- **Row-level security**: SQL-based data access control
## Test Utilities
### Python Test Helpers
- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
- **`@with_config`** - Config mocking decorator
- **`@with_feature_flags`** - Feature flag testing
- **`login_as()`, `login_as_admin()`** - Authentication helpers
- **`create_dashboard()`, `create_slice()`** - Data setup utilities
### TypeScript Test Helpers
- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
- **`createWrapper()`** - Redux/Router/Theme wrapper
- **`selectOption()`** - Select component helper
- **React Testing Library** - NO Enzyme (removed)
### Test Database Patterns
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
- **API tests**: Update expected columns when adding new model fields
### Running Tests
```bash
# Frontend
npm run test # All tests
npm run test -- filename.test.tsx # Single file
# Backend
pytest # All tests
pytest tests/unit_tests/specific_test.py # Single file
pytest tests/unit_tests/ # Directory
# If pytest fails with database/setup issues, ask the user to run test environment setup
```
## Environment Validation
**Quick Setup Check (run this first):**
```bash
# Verify Superset is running
curl -f http://localhost:8088/health || echo "❌ Setup required - see https://superset.apache.org/docs/contributing/development#working-with-llms"
```
**If health checks fail:**
"It appears you aren't set up properly. Please refer to the [Working with LLMs](https://superset.apache.org/docs/contributing/development#working-with-llms) section in the development docs for setup instructions."
**Key Project Files:**
- `superset-frontend/package.json` - Frontend build scripts (`npm run dev` on port 9000, `npm run test`, `npm run lint`)
- `pyproject.toml` - Python tooling (ruff, mypy configs)
- `requirements/` folder - Python dependencies (base.txt, development.txt)
## SQLAlchemy Query Best Practices
- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
- **Example**: `~Model.is_active` instead of `Model.is_active == False`
## Pre-commit Validation
**Use pre-commit hooks for quality validation:**
```bash
# Install hooks
pre-commit install
# IMPORTANT: Stage your changes first!
git add . # Pre-commit only checks staged files
# Quick validation (faster than --all-files)
pre-commit run # Staged files only
pre-commit run mypy # Python type checking
pre-commit run prettier # Code formatting
pre-commit run eslint # Frontend linting
```
**Important pre-commit usage notes:**
- **Stage files first**: Run `git add .` before `pre-commit run` to check only changed files (much faster)
- **Virtual environment**: Activate your Python virtual environment before running pre-commit
```bash
# Common virtual environment locations (yours may differ):
source .venv/bin/activate # if using .venv
source venv/bin/activate # if using venv
source ~/venvs/superset/bin/activate # if using a central location
```
If you get a "command not found" error, ask the user which virtual environment to activate
- **Auto-fixes**: Some hooks auto-fix issues (e.g., trailing whitespace). Re-run after fixes are applied
## Common File Patterns
### API Structure
- **`/api.py`** - REST endpoints with decorators and OpenAPI docstrings
- **`/schemas.py`** - Marshmallow validation schemas for OpenAPI spec
- **`/commands/`** - Business logic classes with @transaction() decorators
- **`/models/`** - SQLAlchemy database models
- **OpenAPI docs**: Auto-generated at `/swagger/v1` from docstrings and schemas
### Migration Files
- **Location**: `superset/migrations/versions/`
- **Naming**: `YYYY-MM-DD_HH-MM_hash_description.py`
- **Utilities**: Use helpers from `superset.migrations.shared.utils` for database compatibility
- **Pattern**: Import utilities instead of raw SQLAlchemy operations
## Platform-Specific Instructions
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
---
**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.

View File

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

View File

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

157
docker-compose-light.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,44 +10,85 @@ version: 1
apache-superset>=6.0
:::
Superset now rides on **Ant Design v5s token-based theming**.
Superset now rides on **Ant Design v5's token-based theming**.
Every Antd token works, plus a handful of Superset-specific ones for charts and dashboard chrome.
## 1 — Create a theme
## Managing Themes via CRUD Interface
1. Open the official [Ant Design Theme Editor](https://ant.design/theme-editor)
2. Design your palette, typography, and component overrides.
3. Open the `CONFIG` modal and paste the JSON.
Superset now includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
### Creating a New Theme
1. Navigate to **Settings > Themes** in the Superset interface
2. Click **+ Theme** to create a new theme
3. Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to design your theme:
- Design your palette, typography, and component overrides
- Open the `CONFIG` modal and copy the JSON configuration
4. Paste the JSON into the theme definition field in Superset
5. Give your theme a descriptive name and save
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
## 2 — Apply it instance-wide
### Applying Themes to Dashboards
Once created, themes can be applied to individual dashboards:
- Edit any dashboard and select your custom theme from the theme dropdown
- Each dashboard can have its own theme, allowing for branded or context-specific styling
## Alternative: Instance-wide Configuration
For system-wide theming, you can configure default themes via Python configuration:
### Setting Default Themes
```python
# superset_config.py
THEME = {
# Paste your JSON theme definition here
# Default theme (light mode)
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
"colorSuccess": "#5ac189",
# ... your theme JSON configuration
}
}
# Dark theme configuration
THEME_DARK = {
"algorithm": "dark",
"token": {
"colorPrimary": "#2893B3",
# ... your dark theme overrides
}
}
# Theme behavior settings
THEME_SETTINGS = {
"enforced": False, # If True, forces default theme always
"allowSwitching": True, # Allow users to switch between themes
"allowOSPreference": True, # Auto-detect system theme preference
}
```
Restart Superset to apply changes
### Copying Themes from CRUD Interface
## 3 — Tweak live in the app (beta)
To use a theme created via the CRUD interface as your system default:
Set the feature flag in your `superset_config`
```python
DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
{{ ... }}
THEME_ALLOW_THEME_EDITOR_BETA = True,
}
```
1. Navigate to **Settings > Themes** and edit your desired theme
2. Copy the complete JSON configuration from the theme definition field
3. Paste it directly into your `superset_config.py` as shown above
- Enables a JSON editor panel inside Superset as a new icon in the navbar
- Intended for testing/design and rapid in-context iteration
- End-user theme switching & preferences coming later
Restart Superset to apply changes.
## 4 — Potential Next Steps
## Theme Development Workflow
- CRUD UI for managing multiple themes
- Per-dashboard & per-workspace theme assignment
- User-selectable theme preferences
1. **Design**: Use the [Ant Design Theme Editor](https://ant.design/theme-editor) to iterate on your design
2. **Test**: Create themes in Superset's CRUD interface for testing
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
## 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

View File

@@ -194,6 +194,48 @@ You can also run the pre-commit checks manually in various ways:
Replace `<hook_id>` with the ID of the specific hook you want to run. You can find the list
of available hooks in the `.pre-commit-config.yaml` file.
## Working with LLMs
### Environment Setup
Ensure Docker Compose is running before starting LLM sessions:
```bash
docker compose up
```
Validate your environment:
```bash
curl -f http://localhost:8088/health && echo "✅ Superset ready"
```
### LLM Session Best Practices
- Always validate environment setup first using the health checks above
- Use focused validation commands: `pre-commit run` (not `--all-files`)
- **Read [LLMS.md](https://github.com/apache/superset/blob/master/LLMS.md) first** - Contains comprehensive development guidelines, coding standards, and critical refactor information
- **Check platform-specific files** when available:
- `CLAUDE.md` - For Claude/Anthropic tools
- `CURSOR.md` - For Cursor editor
- `GEMINI.md` - For Google Gemini tools
- `GPT.md` - For OpenAI/ChatGPT tools
- Follow the TypeScript migration guidelines and avoid deprecated patterns listed in LLMS.md
### Key Development Commands
```bash
# Frontend development
cd superset-frontend
npm run dev # Development server on http://localhost:9000
npm run test # Run all tests
npm run test -- filename.test.tsx # Run single test file
npm run lint # Linting and type checking
# Backend validation
pre-commit run mypy # Type checking
pytest # Run all tests
pytest tests/unit_tests/specific_test.py # Run single test file
pytest tests/unit_tests/ # Run all tests in directory
```
For detailed development context, environment setup, and coding guidelines, see [LLMS.md](https://github.com/apache/superset/blob/master/LLMS.md).
## Alternatives to `docker compose`
:::caution

View File

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

View File

@@ -65,5 +65,6 @@
"last 1 firefox version",
"last 1 safari version"
]
}
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -4320,12 +4320,12 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54"
integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==
version "1.11.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
form-data "^4.0.4"
proxy-from-env "^1.1.0"
babel-loader@^9.2.1:
@@ -6653,7 +6653,7 @@ form-data-encoder@^2.1.2:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.0:
form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==

View File

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

View File

@@ -11,7 +11,9 @@ apispec==6.6.1
apsw==3.50.1.0
# via shillelagh
async-timeout==4.0.3
# via -r requirements/base.in
# via
# -r requirements/base.in
# redis
attrs==25.3.0
# via
# cattrs
@@ -97,6 +99,11 @@ email-validator==2.2.0
# via flask-appbuilder
et-xmlfile==2.0.0
# via openpyxl
exceptiongroup==1.3.0
# via
# cattrs
# trio
# trio-websocket
flask==2.3.3
# via
# apache-superset (pyproject.toml)
@@ -154,6 +161,7 @@ greenlet==3.1.1
# via
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==23.0.0
# via apache-superset (pyproject.toml)
h11==0.16.0
@@ -310,7 +318,7 @@ python-dateutil==2.9.0.post0
# holidays
# pandas
# shillelagh
python-dotenv==1.1.0
python-dotenv==1.1.1
# via apache-superset (pyproject.toml)
python-geohash==0.8.5
# via apache-superset (pyproject.toml)
@@ -378,7 +386,7 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
sqlglot==26.28.1
sqlglot==27.3.0
# via apache-superset (pyproject.toml)
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
@@ -395,9 +403,11 @@ typing-extensions==4.14.0
# apache-superset (pyproject.toml)
# alembic
# cattrs
# exceptiongroup
# limits
# pyopenssl
# referencing
# rich
# selenium
# shillelagh
tzdata==2025.2

View File

@@ -20,6 +20,10 @@ apsw==3.50.1.0
# shillelagh
astroid==3.3.10
# via pylint
async-timeout==4.0.3
# via
# -c requirements/base.txt
# redis
attrs==25.3.0
# via
# -c requirements/base.txt
@@ -176,6 +180,13 @@ et-xmlfile==2.0.0
# via
# -c requirements/base.txt
# openpyxl
exceptiongroup==1.3.0
# via
# -c requirements/base.txt
# cattrs
# pytest
# trio
# trio-websocket
filelock==3.12.2
# via virtualenv
flask==2.3.3
@@ -313,6 +324,7 @@ greenlet==3.1.1
# apache-superset
# gevent
# shillelagh
# sqlalchemy
grpcio==1.71.0
# via
# apache-superset
@@ -670,7 +682,7 @@ python-dateutil==2.9.0.post0
# pyhive
# shillelagh
# trino
python-dotenv==1.1.0
python-dotenv==1.1.1
# via
# -c requirements/base.txt
# apache-superset
@@ -802,7 +814,7 @@ sqlalchemy-utils==0.38.3
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
sqlglot==26.28.1
sqlglot==27.3.0
# via
# -c requirements/base.txt
# apache-superset
@@ -818,6 +830,11 @@ tabulate==0.9.0
# via
# -c requirements/base.txt
# apache-superset
tomli==2.2.1
# via
# coverage
# pylint
# pytest
tomlkit==0.13.3
# via pylint
tqdm==4.67.1
@@ -840,10 +857,13 @@ typing-extensions==4.14.0
# -c requirements/base.txt
# alembic
# apache-superset
# astroid
# cattrs
# exceptiongroup
# limits
# pyopenssl
# referencing
# rich
# selenium
# shillelagh
tzdata==2025.2

View File

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

View File

@@ -54,7 +54,7 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
interceptV1ChartData();
}
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)')
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
.should('be.visible')
.find("[role='menu'] [role='menuitem']")
.contains(/^Drill by$/)
@@ -529,7 +529,7 @@ describe('Drill by modal', () => {
]);
});
it('Bar Chart', () => {
it.skip('Bar Chart', () => {
testEchart('echarts_timeseries_bar', 'Bar Chart', [
[85, 94],
[490, 68],
@@ -612,7 +612,7 @@ describe('Drill by modal', () => {
]);
});
it('Mixed Chart', () => {
it.skip('Mixed Chart', () => {
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
// click 'boy'
cy.wrap($canvas).scrollIntoView();

View File

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

View File

@@ -77,7 +77,6 @@
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^130.0.0",
"html-webpack-plugin": "^5.6.3",
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
@@ -249,7 +248,7 @@
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
"po2json": "^0.4.5",
"prettier": "3.5.3",
"prettier": "3.6.2",
"prettier-plugin-packagejson": "^2.5.3",
"process": "^0.11.10",
"react-resizable": "^3.0.5",
@@ -276,7 +275,7 @@
"webpack-visualizer-plugin2": "^1.2.0"
},
"engines": {
"node": "^20.16.0",
"node": "^20.18.1",
"npm": "^10.8.1"
},
"peerDependencies": {
@@ -46219,9 +46218,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"devOptional": true,
"license": "MIT",
"bin": {

View File

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

View File

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

View File

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

View File

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

View File

@@ -283,6 +283,19 @@ const series_limit: SharedControlConfig<'SelectControl'> = {
),
};
const group_others_when_limit_reached: SharedControlConfig<'CheckboxControl'> =
{
type: 'CheckboxControl',
label: t('Group remaining as "Others"'),
default: false,
description: t(
'Groups remaining series into an "Others" category when series limit is reached. ' +
'This prevents incomplete time series data from being displayed.',
),
visibility: ({ form_data }: { form_data: any }) =>
Boolean(form_data?.limit || form_data?.series_limit),
};
const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> =
{
type: 'SelectControl',
@@ -446,6 +459,7 @@ export default {
time_shift_color,
series_columns: dndColumnsControl,
series_limit,
group_others_when_limit_reached,
series_limit_metric: dndSortByControl,
legacy_order_by: dndSortByControl,
truncate_metric,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,17 +31,13 @@ const StyledDiv = styled.div`
}
`;
const DescriptionContainer = styled.div`
line-height: ${({ theme }) => theme.sizeUnit * 4}px;
padding-top: 16px;
`;
export function DeleteModal({
description,
onConfirm,
onHide,
open,
title,
name,
}: DeleteModalProps) {
const [disableChange, setDisableChange] = useState(true);
const [confirmation, setConfirmation] = useState<string>('');
@@ -81,12 +77,13 @@ export function DeleteModal({
onHide={hide}
onHandledPrimaryAction={confirm}
primaryButtonName={t('Delete')}
primaryButtonType="danger"
primaryButtonStyle="danger"
show={open}
name={name}
title={title}
centered
>
<DescriptionContainer>{description}</DescriptionContainer>
{description}
<StyledDiv>
<FormLabel htmlFor="delete">
{t('Type "%s" to confirm', t('DELETE'))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ export const InteractiveModal = (props: ModalProps) => (
InteractiveModal.args = {
disablePrimaryButton: false,
primaryButtonName: 'Danger',
primaryButtonType: 'danger',
primaryButtonStyle: 'danger',
show: true,
title: "I'm a modal!",
resizable: false,

View File

@@ -77,7 +77,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
.ant-modal-header {
flex: 0 0 auto;
border-radius: ${theme.borderRadius}px ${theme.borderRadius}px 0 0;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
.ant-modal-title {
font-weight: ${theme.fontWeightStrong};
@@ -121,7 +121,8 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
.ant-modal-body {
flex: 0 1 auto;
padding: ${theme.sizeUnit * 4}px;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
overflow: auto;
${!resizable && height && `height: ${height};`}
}
@@ -208,7 +209,7 @@ const CustomModal = ({
onHide,
onHandledPrimaryAction,
primaryButtonName = t('OK'),
primaryButtonType = 'primary',
primaryButtonStyle = 'primary',
show,
name,
title,
@@ -261,7 +262,7 @@ const CustomModal = ({
</Button>,
<Button
key="submit"
buttonStyle={primaryButtonType}
buttonStyle={primaryButtonStyle}
disabled={disablePrimaryButton}
tooltip={primaryTooltipMessage}
loading={primaryButtonLoading}
@@ -332,7 +333,7 @@ const CustomModal = ({
}
footer={!hideFooter ? modalFooter : null}
hideFooter={hideFooter}
wrapProps={{ 'data-test': `${name || title}-modal`, ...wrapProps }}
wrapProps={{ 'data-test': `${name || 'antd'}-modal`, ...wrapProps }}
modalRender={modal =>
resizable || draggable ? (
<Draggable

View File

@@ -20,6 +20,7 @@ import type { CSSProperties, ReactNode } from 'react';
import type { ModalFuncProps } from 'antd';
import type { ResizableProps } from 're-resizable';
import type { DraggableProps } from 'react-draggable';
import { ButtonStyle } from '../Button/types';
export interface ModalProps {
className?: string;
@@ -30,7 +31,7 @@ export interface ModalProps {
onHide: () => void;
onHandledPrimaryAction?: () => void;
primaryButtonName?: string;
primaryButtonType?: 'primary' | 'danger';
primaryButtonStyle?: ButtonStyle;
show: boolean;
name?: string;
title: ReactNode;

View File

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

View File

@@ -1,150 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Modal, Tooltip, Flex, Select } from 'antd';
import { Button, JsonEditor } from '@superset-ui/core/components';
import {
themeObject,
exampleThemes,
SerializableThemeConfig,
Theme,
AnyThemeConfig,
} from '@superset-ui/core';
import { useState } from 'react';
import { Icons } from '@superset-ui/core/components/Icons';
interface ThemeEditorProps {
tooltipTitle?: string;
modalTitle?: string;
theme?: Theme;
setTheme?: (config: AnyThemeConfig) => void;
}
const ThemeEditor: React.FC<ThemeEditorProps> = ({
tooltipTitle = 'Edit Theme',
modalTitle = 'Theme Editor',
theme,
setTheme,
}) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const jsonTheme =
JSON.stringify(theme?.toSerializedConfig(), null, 2) || '{}';
const [jsonMetadata, setJsonMetadata] = useState<string>(jsonTheme);
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
// Get theme names for the Select options
const themeOptions: { value: string; label: string }[] = Object.keys(
exampleThemes,
).map(key => ({
value: key,
label: key,
}));
const handleOpenModal = (): void => {
setIsModalOpen(true);
setJsonMetadata(JSON.stringify(theme?.toSerializedConfig(), null, 2));
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
const handleSave = (): void => {
try {
const parsedTheme = JSON.parse(jsonMetadata);
setTheme?.(parsedTheme);
setIsModalOpen(false);
} catch (error) {
console.error('Invalid JSON in theme editor:', error);
alert('Error parsing JSON. Please check your input.');
}
};
const handleThemeChange = (value: string): void => {
setSelectedTheme(value);
// When a theme is selected, update the JSON editor with the theme definition
const themeData = exampleThemes[value] || ({} as SerializableThemeConfig);
setJsonMetadata(JSON.stringify(themeData, null, 2));
};
return (
<>
<Tooltip title={tooltipTitle} placement="bottom">
<Button
buttonStyle="link"
icon={
<Icons.BgColorsOutlined
iconSize="l"
iconColor={themeObject.theme.colorPrimary}
/>
}
onClick={handleOpenModal}
aria-label="Edit theme"
size="large"
/>
</Tooltip>
<Modal
title={modalTitle}
open={isModalOpen}
onCancel={handleCancel}
width={800}
centered
styles={{
body: {
padding: '24px',
},
}}
footer={
<Flex justify="end" gap="small">
<Button onClick={handleCancel} buttonStyle="secondary">
Cancel
</Button>
<Button type="primary" onClick={handleSave}>
Apply Theme
</Button>
</Flex>
}
>
<Flex vertical gap="middle">
<div>
Select a theme template:
<Select
placeholder="Choose a theme"
style={{ width: '100%', marginTop: '8px' }}
options={themeOptions}
onChange={handleThemeChange}
value={selectedTheme}
/>
</div>
<JsonEditor
showLoadingForImport
name="json_metadata"
value={jsonMetadata}
onChange={setJsonMetadata}
tabSize={2}
width="100%"
height="200px"
wrapEnabled
/>
</Flex>
</Modal>
</>
);
};
export default ThemeEditor;

View File

@@ -16,22 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Tooltip } from 'antd';
import { Dropdown, Icons } from '@superset-ui/core/components';
import { t } from '@superset-ui/core';
import type { MenuItem } from '@superset-ui/core/components/Menu';
import { t, useTheme } from '@superset-ui/core';
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
export interface ThemeSelectProps {
setThemeMode: (newMode: ThemeMode) => void;
tooltipTitle?: string;
themeMode: ThemeMode;
hasLocalOverride?: boolean;
onClearLocalSettings?: () => void;
allowOSPreference?: boolean;
}
const ThemeSelect: React.FC<ThemeSelectProps> = ({
setThemeMode,
tooltipTitle = 'Select theme',
themeMode,
hasLocalOverride = false,
onClearLocalSettings,
allowOSPreference = true,
}) => {
const theme = useTheme();
const handleSelect = (mode: ThemeMode) => {
setThemeMode(mode);
};
@@ -43,36 +51,65 @@ const ThemeSelect: React.FC<ThemeSelectProps> = ({
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
};
// Use different icon when local theme is active
const triggerIcon = hasLocalOverride ? (
<Icons.FormatPainterOutlined style={{ color: theme.colorErrorText }} />
) : (
themeIconMap[themeMode] || <Icons.FormatPainterOutlined />
);
const menuItems: MenuItem[] = [
{
type: 'group',
label: t('Theme'),
},
{
key: ThemeMode.DEFAULT,
label: t('Light'),
icon: <Icons.SunOutlined />,
onClick: () => handleSelect(ThemeMode.DEFAULT),
},
{
key: ThemeMode.DARK,
label: t('Dark'),
icon: <Icons.MoonOutlined />,
onClick: () => handleSelect(ThemeMode.DARK),
},
...(allowOSPreference
? [
{
key: ThemeMode.SYSTEM,
label: t('Match system'),
icon: <Icons.FormatPainterOutlined />,
onClick: () => handleSelect(ThemeMode.SYSTEM),
},
]
: []),
];
// Add clear settings option only when there's a local theme active
if (onClearLocalSettings && hasLocalOverride) {
menuItems.push(
{ type: 'divider' } as MenuItem,
{
key: 'clear-local',
label: t('Clear local theme'),
icon: <Icons.ClearOutlined />,
onClick: onClearLocalSettings,
} as MenuItem,
);
}
return (
<Tooltip title={tooltipTitle} placement="bottom">
<Dropdown
menu={{
items: [
{
key: ThemeMode.DEFAULT,
label: t('Light'),
onClick: () => handleSelect(ThemeMode.DEFAULT),
icon: <Icons.SunOutlined />,
},
{
key: ThemeMode.DARK,
label: t('Dark'),
onClick: () => handleSelect(ThemeMode.DARK),
icon: <Icons.MoonOutlined />,
},
{
key: ThemeMode.SYSTEM,
label: t('Match system'),
onClick: () => handleSelect(ThemeMode.SYSTEM),
icon: <Icons.FormatPainterOutlined />,
},
],
}}
trigger={['click']}
>
{themeIconMap[themeMode] || <Icons.FormatPainterOutlined />}
</Dropdown>
</Tooltip>
<Dropdown
menu={{
items: menuItems,
selectedKeys: [themeMode],
}}
trigger={['hover']}
>
{triggerIcon}
</Dropdown>
);
};

View File

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

View File

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

View File

@@ -148,6 +148,7 @@ export {
Typography,
type TypographyProps,
type ParagraphProps,
type TitleProps,
} from './Typography';
export { Image, type ImageProps } from './Image';

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,3 +75,6 @@ export type {
ThemeControllerOptions,
ThemeContextType,
};
// Export theme utility functions
export * from './utils/themeUtils';

View File

@@ -411,6 +411,7 @@ export interface ThemeControllerOptions {
onChange?: (theme: Theme) => void;
canUpdateTheme?: () => boolean;
canUpdateMode?: () => boolean;
isGlobalContext?: boolean;
}
export interface ThemeContextType {
@@ -419,4 +420,12 @@ export interface ThemeContextType {
setTheme: (config: AnyThemeConfig) => void;
setThemeMode: (newMode: ThemeMode) => void;
resetTheme: () => void;
setTemporaryTheme: (config: AnyThemeConfig) => void;
clearLocalOverrides: () => void;
getCurrentCrudThemeId: () => string | null;
hasDevOverride: () => boolean;
canSetMode: () => boolean;
canSetTheme: () => boolean;
canDetectOSPreference: () => boolean;
createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
}

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import {
getSequentialSchemeRegistry,
CategoricalColorNamespace,
} from '@superset-ui/core';
import Datamap from 'datamaps/dist/datamaps.world.min';
import Datamap from 'datamaps/dist/datamaps.all.min';
import { ColorBy } from './utils';
const propTypes = {

View File

@@ -35,6 +35,7 @@ import {
spatial,
viewport,
} from '../../utilities/Shared_DeckGL';
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -53,7 +54,10 @@ const config: ControlPanelConfig = {
label: t('Map'),
controlSetRows: [
[mapboxStyle],
...generateDeckGLColorSchemeControls({}),
...generateDeckGLColorSchemeControls({
defaultSchemeType: COLOR_SCHEME_TYPES.categorical_palette,
disableCategoricalColumn: true,
}),
[viewport],
[autozoom],
[gridSize],

View File

@@ -37,4 +37,4 @@ export function formatSelectOptions(options: (string | number)[]) {
export const isColorSchemeTypeVisible = (
controls: ControlStateMapping,
colorSchemeType: ColorSchemeType,
) => controls.color_scheme_type.value === colorSchemeType;
) => controls.color_scheme_type?.value === colorSchemeType;

View File

@@ -40,7 +40,7 @@ import {
} from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import { type FunctionComponent } from 'react';
import { JsonObject, DataRecordValue, DataRecord } from '@superset-ui/core';
import { JsonObject, DataRecordValue, DataRecord, t } from '@superset-ui/core';
import { SearchOutlined } from '@ant-design/icons';
import { debounce, isEqual } from 'lodash';
import Pagination from './components/Pagination';
@@ -326,6 +326,79 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
suppressDragLeaveHidesColumns
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
localeText={{
// Pagination controls
next: t('Next'),
previous: t('Previous'),
page: t('Page'),
more: t('More'),
to: t('to'),
of: t('of'),
first: t('First'),
last: t('Last'),
loadingOoo: t('Loading...'),
// Set Filter
selectAll: t('Select All'),
searchOoo: t('Search...'),
blanks: t('Blanks'),
// Filter operations
filterOoo: t('Filter'),
applyFilter: t('Apply Filter'),
equals: t('Equals'),
notEqual: t('Not Equal'),
lessThan: t('Less Than'),
greaterThan: t('Greater Than'),
lessThanOrEqual: t('Less Than or Equal'),
greaterThanOrEqual: t('Greater Than or Equal'),
inRange: t('In Range'),
contains: t('Contains'),
notContains: t('Not Contains'),
startsWith: t('Starts With'),
endsWith: t('Ends With'),
// Logical conditions
andCondition: t('AND'),
orCondition: t('OR'),
// Panel and group labels
group: t('Group'),
columns: t('Columns'),
filters: t('Filters'),
valueColumns: t('Value Columns'),
pivotMode: t('Pivot Mode'),
groups: t('Groups'),
values: t('Values'),
pivots: t('Pivots'),
toolPanelButton: t('Tool Panel'),
// Enterprise menu items
pinColumn: t('Pin Column'),
valueAggregation: t('Value Aggregation'),
autosizeThiscolumn: t('Autosize This Column'),
autosizeAllColumns: t('Autosize All Columns'),
groupBy: t('Group By'),
ungroupBy: t('Ungroup By'),
resetColumns: t('Reset Columns'),
expandAll: t('Expand All'),
collapseAll: t('Collapse All'),
toolPanel: t('Tool Panel'),
export: t('Export'),
csvExport: t('CSV Export'),
excelExport: t('Excel Export'),
excelXmlExport: t('Excel XML Export'),
// Aggregation functions
sum: t('Sum'),
min: t('Min'),
max: t('Max'),
none: t('None'),
count: t('Count'),
average: t('Average'),
// Standard menu items
copy: t('Copy'),
copyWithHeaders: t('Copy with Headers'),
paste: t('Paste'),
// Column menu and sorting
sortAscending: t('Sort Ascending'),
sortDescending: t('Sort Descending'),
sortUnSort: t('Clear Sort'),
}}
context={{
onColumnHeaderClicked: handleColumnHeaderClick,
initialSortState: getInitialSortState(

View File

@@ -17,10 +17,6 @@
* under the License.
*/
import { formatSelectOptions } from '@superset-ui/chart-controls';
import { addLocaleData } from '@superset-ui/core';
import i18n from './i18n';
addLocaleData(i18n);
export const SERVER_PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
10, 20, 50, 100, 200,

View File

@@ -1,66 +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 { Locale } from '@superset-ui/core';
const en = {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Show'],
'page_size.all': ['All'],
'page_size.entries': ['entries'],
'table.previous_page': ['Previous'],
'table.next_page': ['Next'],
'search.num_records': ['%s record', '%s records...'],
};
const translations: Partial<Record<Locale, typeof en>> = {
en,
fr: {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Afficher'],
'page_size.all': ['tous'],
'page_size.entries': ['entrées'],
'table.previous_page': ['Précédent'],
'table.next_page': ['Suivante'],
'search.num_records': ['%s enregistrement', '%s enregistrements...'],
},
zh: {
'Query Mode': ['查询模式'],
Aggregate: ['分组聚合'],
'Raw Records': ['原始数据'],
'Emit Filter Events': ['关联看板过滤器'],
'Show Cell Bars': ['为指标添加条状图背景'],
'page_size.show': ['每页显示'],
'page_size.all': ['全部'],
'page_size.entries': ['条'],
'table.previous_page': ['上一页'],
'table.next_page': ['下一页'],
'search.num_records': ['%s条记录...'],
},
};
export default translations;

View File

@@ -35,8 +35,10 @@
},
"peerDependencies": {
"@ant-design/icons": "^5.2.6",
"@reduxjs/toolkit": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@types/react-redux": "*",
"geostyler": "^14.1.3",
"geostyler-data": "^1.0.0",
"geostyler-openlayers-parser": "^4.0.0",

View File

@@ -52,6 +52,8 @@ export class ChartLayer extends Layer {
theme: SupersetTheme;
locale: string;
/**
* Create a ChartLayer.
*
@@ -91,6 +93,10 @@ export class ChartLayer extends Layer {
this.theme = options.theme;
}
if (options.locale) {
this.locale = options.locale;
}
const spinner = document.createElement('img');
spinner.src = Loader;
spinner.style.position = 'relative';
@@ -183,6 +189,7 @@ export class ChartLayer extends Layer {
chartWidth,
chartHeight,
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, container);
@@ -218,6 +225,7 @@ export class ChartLayer extends Layer {
chartWidth,
chartHeight,
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, chart.htmlElement);

View File

@@ -16,8 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { configureStore } from '@reduxjs/toolkit';
import { getChartComponentRegistry, ThemeProvider } from '@superset-ui/core';
import { FC, useEffect, useState } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { ChartWrapperProps } from '../types';
export const ChartWrapper: FC<ChartWrapperProps> = ({
@@ -26,6 +28,7 @@ export const ChartWrapper: FC<ChartWrapperProps> = ({
height,
width,
chartConfig,
locale,
}) => {
const [Chart, setChart] = useState<any>();
@@ -39,13 +42,21 @@ export const ChartWrapper: FC<ChartWrapperProps> = ({
getChartFromRegistry(vizType);
}, [vizType]);
// Create a mock store that is needed by
// eCharts components to access the locale.
const mockStore = configureStore({
reducer: (state = { common: { locale } }) => state,
});
return (
<ThemeProvider theme={theme}>
{Chart === undefined ? (
<></>
) : (
<Chart {...chartConfig.properties} height={height} width={width} />
)}
<ReduxProvider store={mockStore}>
{Chart === undefined ? (
<></>
) : (
<Chart {...chartConfig.properties} height={height} width={width} />
)}
</ReduxProvider>
</ThemeProvider>
);
};

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import Point from 'ol/geom/Point';
import { View } from 'ol';
@@ -55,6 +56,8 @@ export const OlChartMap = (props: OlChartMapProps) => {
theme,
} = props;
const locale = useSelector((state: any) => state?.common?.locale);
const [currentChartConfigs, setCurrentChartConfigs] =
useState<ChartConfig>(chartConfigs);
const [currentMapView, setCurrentMapView] = useState<MapViewConfigs>(mapView);
@@ -360,6 +363,7 @@ export const OlChartMap = (props: OlChartMapProps) => {
onMouseOver: deactivateInteractions,
onMouseOut: activateInteractions,
theme,
locale,
});
olMap.addLayer(newChartLayer);
@@ -393,6 +397,7 @@ export const OlChartMap = (props: OlChartMapProps) => {
chartSize.values,
chartBackgroundColor,
chartBackgroundBorderRadius,
locale,
]);
return (

View File

@@ -195,6 +195,7 @@ export type ChartLayerOptions = {
map?: Map | null | undefined;
render?: RenderFunction | undefined;
properties?: { [x: string]: any } | undefined;
locale: string;
};
export type CartodiagramPluginConstructorOpts = {
@@ -207,4 +208,5 @@ export type ChartWrapperProps = {
width: number;
height: number;
chartConfig: ChartConfigFeature;
locale: string;
};

View File

@@ -36,6 +36,7 @@ export const createChartComponent = (
chartWidth: number,
chartHeight: number,
chartTheme: SupersetTheme,
chartLocale: string,
) => (
<ChartWrapper
vizType={chartVizType}
@@ -43,6 +44,7 @@ export const createChartComponent = (
width={chartWidth}
height={chartHeight}
theme={chartTheme}
locale={chartLocale}
/>
);

View File

@@ -24,6 +24,7 @@ describe('ChartLayer', () => {
it('creates div and loading mask', () => {
const options: ChartLayerOptions = {
chartVizType: 'pie',
locale: 'en',
};
const chartLayer = new ChartLayer(options);
@@ -34,6 +35,7 @@ describe('ChartLayer', () => {
it('can remove chart elements', () => {
const options: ChartLayerOptions = {
chartVizType: 'pie',
locale: 'en',
};
const chartLayer = new ChartLayer(options);
chartLayer.charts = [

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent, MouseEvent, createRef } from 'react';
import { useState, useEffect, useRef, MouseEvent } from 'react';
import {
t,
getNumberFormatter,
@@ -26,7 +26,7 @@ import {
BRAND_COLOR,
styled,
BinaryQueryObjectFilterClause,
themeObject,
useTheme,
} from '@superset-ui/core';
import Echart from '../components/Echart';
import { BigNumberVizProps } from './types';
@@ -44,82 +44,68 @@ const PROPORTION = {
TRENDLINE: 0.3,
};
type BigNumberVisState = {
elementsRendered: boolean;
recalculateTrigger: boolean;
};
function BigNumberVis({
className = '',
headerFormatter = defaultNumberFormatter,
formatTime = getTimeFormatter(SMART_DATE_VERBOSE_ID),
headerFontSize = PROPORTION.HEADER,
kickerFontSize = PROPORTION.KICKER,
metricNameFontSize = PROPORTION.METRIC_NAME,
showMetricName = true,
mainColor = BRAND_COLOR,
showTimestamp = false,
showTrendLine = false,
startYAxisAtZero = true,
subheader = '',
subheaderFontSize = PROPORTION.SUBHEADER,
subtitleFontSize = PROPORTION.SUBHEADER,
timeRangeFixed = false,
...props
}: BigNumberVizProps) {
const theme = useTheme();
class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
static defaultProps = {
className: '',
headerFormatter: defaultNumberFormatter,
formatTime: getTimeFormatter(SMART_DATE_VERBOSE_ID),
headerFontSize: PROPORTION.HEADER,
kickerFontSize: PROPORTION.KICKER,
metricNameFontSize: PROPORTION.METRIC_NAME,
showMetricName: true,
mainColor: BRAND_COLOR,
showTimestamp: false,
showTrendLine: false,
startYAxisAtZero: true,
subheader: '',
subheaderFontSize: PROPORTION.SUBHEADER,
timeRangeFixed: false,
};
// Convert state to hooks
const [elementsRendered, setElementsRendered] = useState(false);
// Create refs for each component to measure heights
metricNameRef = createRef<HTMLDivElement>();
const metricNameRef = useRef<HTMLDivElement>(null);
const kickerRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const subheaderRef = useRef<HTMLDivElement>(null);
const subtitleRef = useRef<HTMLDivElement>(null);
kickerRef = createRef<HTMLDivElement>();
headerRef = createRef<HTMLDivElement>();
subheaderRef = createRef<HTMLDivElement>();
subtitleRef = createRef<HTMLDivElement>();
state = {
elementsRendered: false,
recalculateTrigger: false,
};
componentDidMount() {
// Convert componentDidMount
useEffect(() => {
// Wait for elements to render and then calculate heights
setTimeout(() => {
this.setState({ elementsRendered: true });
const timeout = setTimeout(() => {
setElementsRendered(true);
}, 0);
}
return () => clearTimeout(timeout);
}, []);
componentDidUpdate(prevProps: BigNumberVizProps) {
if (
prevProps.height !== this.props.height ||
prevProps.showTrendLine !== this.props.showTrendLine
) {
this.setState(prevState => ({
recalculateTrigger: !prevState.recalculateTrigger,
}));
}
}
// Convert componentDidUpdate - trigger re-render when height or trendline changes
useEffect(() => {
// Re-render when height or showTrendLine changes
}, [props.height, showTrendLine]);
getClassName() {
const { className, showTrendLine, bigNumberFallback } = this.props;
const getClassName = () => {
const names = `superset-legacy-chart-big-number ${className} ${
bigNumberFallback ? 'is-fallback-value' : ''
props.bigNumberFallback ? 'is-fallback-value' : ''
}`;
if (showTrendLine) return names;
return `${names} no-trendline`;
}
};
createTemporaryContainer() {
const createTemporaryContainer = () => {
const container = document.createElement('div');
container.className = this.getClassName();
container.className = getClassName();
container.style.position = 'absolute'; // so it won't disrupt page layout
container.style.opacity = '0'; // and not visible
return container;
}
};
renderFallbackWarning() {
const { bigNumberFallback, formatTime, showTimestamp } = this.props;
const renderFallbackWarning = () => {
const { bigNumberFallback } = props;
if (!formatTime || !bigNumberFallback || showTimestamp) return null;
return (
<span
@@ -133,15 +119,15 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
{t('Not up to date')}
</span>
);
}
};
renderMetricName(maxHeight: number) {
const { metricName, width, showMetricName } = this.props;
const renderMetricName = (maxHeight: number) => {
const { metricName, width } = props;
if (!showMetricName || !metricName) return null;
const text = metricName;
const container = this.createTemporaryContainer();
const container = createTemporaryContainer();
document.body.append(container);
const fontSize = computeMaxFontSize({
text,
@@ -154,7 +140,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
return (
<div
ref={this.metricNameRef}
ref={metricNameRef}
className="metric-name"
style={{
fontSize,
@@ -164,10 +150,10 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
{text}
</div>
);
}
};
renderKicker(maxHeight: number) {
const { timestamp, showTimestamp, formatTime, width } = this.props;
const renderKicker = (maxHeight: number) => {
const { timestamp, width } = props;
if (
!formatTime ||
!showTimestamp ||
@@ -179,7 +165,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
const text = timestamp === null ? '' : formatTime(timestamp);
const container = this.createTemporaryContainer();
const container = createTemporaryContainer();
document.body.append(container);
const fontSize = computeMaxFontSize({
text,
@@ -192,7 +178,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
return (
<div
ref={this.kickerRef}
ref={kickerRef}
className="kicker"
style={{
fontSize,
@@ -202,18 +188,16 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
{text}
</div>
);
}
};
renderHeader(maxHeight: number) {
const { bigNumber, headerFormatter, width, colorThresholdFormatters } =
this.props;
const renderHeader = (maxHeight: number) => {
const { bigNumber, width, colorThresholdFormatters, onContextMenu } = props;
// @ts-ignore
const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber);
const hasThresholdColorFormatter =
Array.isArray(colorThresholdFormatters) &&
colorThresholdFormatters.length > 0;
const { theme } = themeObject;
let numberColor;
if (hasThresholdColorFormatter) {
@@ -229,7 +213,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
numberColor = theme.colorText;
}
const container = this.createTemporaryContainer();
const container = createTemporaryContainer();
document.body.append(container);
const fontSize = computeMaxFontSize({
text,
@@ -240,16 +224,16 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
});
container.remove();
const onContextMenu = (e: MouseEvent<HTMLDivElement>) => {
if (this.props.onContextMenu) {
const handleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
if (onContextMenu) {
e.preventDefault();
this.props.onContextMenu(e.nativeEvent.clientX, e.nativeEvent.clientY);
onContextMenu(e.nativeEvent.clientX, e.nativeEvent.clientY);
}
};
return (
<div
ref={this.headerRef}
ref={headerRef}
className="header-line"
style={{
display: 'flex',
@@ -258,21 +242,21 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
height: 'auto',
color: numberColor,
}}
onContextMenu={onContextMenu}
onContextMenu={handleContextMenu}
>
{text}
</div>
);
}
};
rendermetricComparisonSummary(maxHeight: number) {
const { subheader, width } = this.props;
const rendermetricComparisonSummary = (maxHeight: number) => {
const { width } = props;
let fontSize = 0;
const text = subheader;
if (text) {
const container = this.createTemporaryContainer();
const container = createTemporaryContainer();
document.body.append(container);
try {
fontSize = computeMaxFontSize({
@@ -288,7 +272,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
return (
<div
ref={this.subheaderRef}
ref={subheaderRef}
className="subheader-line"
style={{
fontSize,
@@ -300,10 +284,10 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
);
}
return null;
}
};
renderSubtitle(maxHeight: number) {
const { subtitle, width, bigNumber, bigNumberFallback } = this.props;
const renderSubtitle = (maxHeight: number) => {
const { subtitle, width, bigNumber, bigNumberFallback } = props;
let fontSize = 0;
const NO_DATA_OR_HASNT_LANDED = t(
@@ -320,7 +304,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
}
if (text) {
const container = this.createTemporaryContainer();
const container = createTemporaryContainer();
document.body.append(container);
fontSize = computeMaxFontSize({
text,
@@ -334,7 +318,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
return (
<>
<div
ref={this.subtitleRef}
ref={subtitleRef}
className="subtitle-line subheader-line"
style={{
fontSize: `${fontSize}px`,
@@ -347,10 +331,18 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
);
}
return null;
}
};
renderTrendline(maxHeight: number) {
const { width, trendLineData, echartOptions, refs } = this.props;
const renderTrendline = (maxHeight: number) => {
const {
width,
trendLineData,
echartOptions,
refs,
onContextMenu,
formData,
xValueFormatter,
} = props;
// if can't find any non-null values, no point rendering the trendline
if (!trendLineData?.some(d => d[1] !== null)) {
@@ -359,24 +351,22 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
const eventHandlers: EventHandlers = {
contextmenu: eventParams => {
if (this.props.onContextMenu) {
if (onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
if (data) {
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
drillToDetailFilters.push({
col: this.props.formData?.granularitySqla,
grain: this.props.formData?.timeGrainSqla,
col: formData?.granularitySqla,
grain: formData?.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: this.props.xValueFormatter?.(data[0]),
formattedVal: xValueFormatter?.(data[0]),
});
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
});
this.props.onContextMenu(
pointerEvent.clientX,
pointerEvent.clientY,
{ drillToDetail: drillToDetailFilters },
);
}
}
},
@@ -393,17 +383,17 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
/>
)
);
}
};
getTotalElementsHeight() {
const getTotalElementsHeight = () => {
const marginPerElement = 8; // theme.sizeUnit = 4, so margin-bottom = 8px
const refs = [
this.metricNameRef,
this.kickerRef,
this.headerRef,
this.subheaderRef,
this.subtitleRef,
metricNameRef,
kickerRef,
headerRef,
subheaderRef,
subtitleRef,
];
// Filter refs to only those with a current element
@@ -416,108 +406,94 @@ class BigNumberVis extends PureComponent<BigNumberVizProps, BigNumberVisState> {
}, 0);
return totalHeight;
}
};
shouldApplyOverflow(availableHeight: number) {
if (!this.state.elementsRendered) return false;
const totalHeight = this.getTotalElementsHeight();
const shouldApplyOverflow = (availableHeight: number) => {
if (!elementsRendered) return false;
const totalHeight = getTotalElementsHeight();
return totalHeight > availableHeight;
}
};
render() {
const {
showTrendLine,
height,
kickerFontSize,
headerFontSize,
subtitleFontSize,
metricNameFontSize,
subheaderFontSize,
} = this.props;
const className = this.getClassName();
const { height } = props;
const componentClassName = getClassName();
if (showTrendLine) {
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
const allTextHeight = height - chartHeight;
const shouldApplyOverflow = this.shouldApplyOverflow(allTextHeight);
if (showTrendLine) {
const chartHeight = Math.floor(PROPORTION.TRENDLINE * height);
const allTextHeight = height - chartHeight;
const overflow = shouldApplyOverflow(allTextHeight);
return (
<div className={className}>
<div
className="text-container"
style={{
height: allTextHeight,
...(shouldApplyOverflow
? {
display: 'block',
boxSizing: 'border-box',
overflowX: 'hidden',
overflowY: 'auto',
width: '100%',
}
: {}),
}}
>
{this.renderFallbackWarning()}
{this.renderMetricName(
Math.ceil(
(metricNameFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{this.renderKicker(
Math.ceil(
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{this.renderHeader(
Math.ceil(headerFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{this.rendermetricComparisonSummary(
Math.ceil(
subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{this.renderSubtitle(
Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
</div>
{this.renderTrendline(chartHeight)}
</div>
);
}
const shouldApplyOverflow = this.shouldApplyOverflow(height);
return (
<div
className={className}
style={{
height,
...(shouldApplyOverflow
? {
display: 'block',
boxSizing: 'border-box',
overflowX: 'hidden',
overflowY: 'auto',
width: '100%',
}
: {}),
}}
>
<div className="text-container">
{this.renderFallbackWarning()}
{this.renderMetricName((metricNameFontSize || 0) * height)}
{this.renderKicker((kickerFontSize || 0) * height)}
{this.renderHeader(Math.ceil(headerFontSize * height))}
{this.rendermetricComparisonSummary(
Math.ceil(subheaderFontSize * height),
<div className={componentClassName}>
<div
className="text-container"
style={{
height: allTextHeight,
...(overflow
? {
display: 'block',
boxSizing: 'border-box',
overflowX: 'hidden',
overflowY: 'auto',
width: '100%',
}
: {}),
}}
>
{renderFallbackWarning()}
{renderMetricName(
Math.ceil(
(metricNameFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{renderKicker(
Math.ceil(
(kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{renderHeader(
Math.ceil(headerFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{rendermetricComparisonSummary(
Math.ceil(subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{renderSubtitle(
Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
</div>
{renderTrendline(chartHeight)}
</div>
);
}
const overflow = shouldApplyOverflow(height);
return (
<div
className={componentClassName}
style={{
height,
...(overflow
? {
display: 'block',
boxSizing: 'border-box',
overflowX: 'hidden',
overflowY: 'auto',
width: '100%',
}
: {}),
}}
>
<div className="text-container">
{renderFallbackWarning()}
{renderMetricName((metricNameFontSize || 0) * height)}
{renderKicker((kickerFontSize || 0) * height)}
{renderHeader(Math.ceil(headerFontSize * height))}
{rendermetricComparisonSummary(Math.ceil(subheaderFontSize * height))}
{renderSubtitle(Math.ceil(subtitleFontSize * height))}
</div>
</div>
);
}
export default styled(BigNumberVis)`
const StyledBigNumberVis = styled(BigNumberVis)`
${({ theme }) => `
font-family: ${theme.fontFamily};
position: relative;
@@ -584,3 +560,5 @@ export default styled(BigNumberVis)`
}
`}
`;
export default StyledBigNumberVis;

View File

@@ -26,7 +26,6 @@ import {
getMetricLabel,
getNumberFormatter,
tooltipHtml,
themeObject,
} from '@superset-ui/core';
import { SankeyChartProps, SankeyTransformedProps } from './types';
import { Refs } from '../types';
@@ -40,7 +39,7 @@ export default function transformProps(
chartProps: SankeyChartProps,
): SankeyTransformedProps {
const refs: Refs = {};
const { formData, height, hooks, queriesData, width } = chartProps;
const { formData, height, hooks, queriesData, width, theme } = chartProps;
const { onLegendStateChanged } = hooks;
const { colorScheme, metric, source, target, sliceId } = formData;
const { data } = queriesData[0];
@@ -63,7 +62,6 @@ export default function transformProps(
value,
});
});
const { theme } = themeObject;
const seriesData: NonNullable<SankeySeriesOption['data']> = Array.from(
set,

View File

@@ -20,7 +20,6 @@ import {
getMetricLabel,
DataRecordValue,
tooltipHtml,
themeObject,
} from '@superset-ui/core';
import type { EChartsCoreOption } from 'echarts/core';
import type { TreeSeriesOption } from 'echarts/charts';
@@ -57,7 +56,7 @@ export function formatTooltip({
export default function transformProps(
chartProps: EchartsTreeChartProps,
): TreeTransformedProps {
const { width, height, formData, queriesData } = chartProps;
const { width, height, formData, queriesData, theme } = chartProps;
const refs: Refs = {};
const data: TreeDataRecord[] = queriesData[0].data || [];
@@ -182,7 +181,6 @@ export default function transformProps(
}
});
}
const { theme } = themeObject;
const series: TreeSeriesOption[] = [
{
type: 'tree',

View File

@@ -31,7 +31,7 @@ import { merge } from 'lodash';
import { useSelector } from 'react-redux';
import { styled, themeObject } from '@superset-ui/core';
import { styled, useTheme } from '@superset-ui/core';
import { use, init, EChartsType, registerLocale } from 'echarts/core';
import {
SankeyChart,
@@ -122,45 +122,6 @@ const loadLocale = async (locale: string) => {
return lang?.default;
};
const getTheme = (options: any) => {
const token = themeObject.theme;
const theme = {
textStyle: {
color: token.colorText,
fontFamily: token.fontFamily,
},
title: {
textStyle: { color: token.colorText },
},
legend: {
textStyle: { color: token.colorTextSecondary },
},
tooltip: {
backgroundColor: token.colorBgContainer,
textStyle: { color: token.colorText },
},
axisPointer: {
lineStyle: { color: token.colorPrimary },
label: { color: token.colorText },
},
} as any;
if (options?.xAxis) {
theme.xAxis = {
axisLine: { lineStyle: { color: token.colorSplit } },
axisLabel: { color: token.colorTextSecondary },
splitLine: { lineStyle: { color: token.colorSplit } },
};
}
if (options?.yAxis) {
theme.yAxis = {
axisLine: { lineStyle: { color: token.colorSplit } },
axisLabel: { color: token.colorTextSecondary },
splitLine: { lineStyle: { color: token.colorSplit } },
};
}
return theme;
};
function Echart(
{
width,
@@ -173,6 +134,7 @@ function Echart(
}: EchartsProps,
ref: Ref<EchartsHandler>,
) {
const theme = useTheme();
const divRef = useRef<HTMLDivElement>(null);
if (refs) {
// eslint-disable-next-line no-param-reassign
@@ -228,9 +190,48 @@ function Echart(
chartRef.current?.getZr().on(name, handler);
});
const getEchartsTheme = (options: any) => {
const antdTheme = theme;
const echartsTheme = {
textStyle: {
color: antdTheme.colorText,
fontFamily: antdTheme.fontFamily,
},
title: {
textStyle: { color: antdTheme.colorText },
},
legend: {
textStyle: { color: antdTheme.colorTextSecondary },
},
tooltip: {
backgroundColor: antdTheme.colorBgContainer,
textStyle: { color: antdTheme.colorText },
},
axisPointer: {
lineStyle: { color: antdTheme.colorPrimary },
label: { color: antdTheme.colorText },
},
} as any;
if (options?.xAxis) {
echartsTheme.xAxis = {
axisLine: { lineStyle: { color: antdTheme.colorSplit } },
axisLabel: { color: antdTheme.colorTextSecondary },
splitLine: { lineStyle: { color: antdTheme.colorSplit } },
};
}
if (options?.yAxis) {
echartsTheme.yAxis = {
axisLine: { lineStyle: { color: antdTheme.colorSplit } },
axisLabel: { color: antdTheme.colorTextSecondary },
splitLine: { lineStyle: { color: antdTheme.colorSplit } },
};
}
return echartsTheme;
};
const themedEchartOptions = merge(
{},
getTheme(echartOptions),
getEchartsTheme(echartOptions),
echartOptions,
);
chartRef.current?.setOption(themedEchartOptions, true);
@@ -238,7 +239,7 @@ function Echart(
// did mount
handleSizeChange({ width, height });
}
}, [didMount, echartOptions, eventHandlers, zrEventHandlers]);
}, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme]);
useEffect(() => () => chartRef.current?.dispose(), []);

View File

@@ -107,6 +107,7 @@ test('should compile query object A', () => {
series_columns: ['foo'],
series_limit: 5,
series_limit_metric: undefined,
group_others_when_limit_reached: false,
url_params: {},
custom_params: {},
custom_form_data: {},
@@ -166,6 +167,7 @@ test('should compile query object B', () => {
series_columns: [],
series_limit: 0,
series_limit_metric: undefined,
group_others_when_limit_reached: false,
url_params: {},
custom_params: {},
custom_form_data: {},

View File

@@ -149,7 +149,12 @@ export default styled.div`
.dt-pagination {
text-align: right;
/* use padding instead of margin so clientHeight can capture it */
padding-top: 0.5em;
padding: ${theme.paddingXXS}px 0px;
}
.dt-pagination .pagination > li {
display: inline;
margin: 0 ${theme.marginXXS}px;
}
.dt-pagination .pagination > li > a,
@@ -157,6 +162,8 @@ export default styled.div`
background-color: ${theme.colorBgBase};
color: ${theme.colorText};
border-color: ${theme.colorBorderSecondary};
padding: ${theme.paddingXXS}px ${theme.paddingXS}px;
border-radius: ${theme.borderRadius}px;
}
.dt-pagination .pagination > li.active > a,

View File

@@ -629,7 +629,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const startPosition = value[0];
const colSpan = value.length;
// Retrieve the originalLabel from the first column in this group
const originalLabel = columnsMeta[value[0]]?.originalLabel || key;
const firstColumnInGroup = filteredColumnsMeta[startPosition];
const originalLabel = firstColumnInGroup
? columnsMeta.find(col => col.key === firstColumnInGroup.key)
?.originalLabel || key
: key;
// Add placeholder <th> for columns before this header
for (let i = currentColumnIndex; i < startPosition; i += 1) {

View File

@@ -17,10 +17,7 @@
* under the License.
*/
import { formatSelectOptions } from '@superset-ui/chart-controls';
import { addLocaleData, t } from '@superset-ui/core';
import i18n from './i18n';
addLocaleData(i18n);
import { t } from '@superset-ui/core';
export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
[0, t('page_size.all')],

View File

@@ -1,66 +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 { Locale } from '@superset-ui/core';
const en = {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Show'],
'page_size.all': ['All'],
'page_size.entries': ['entries'],
'table.previous_page': ['Previous'],
'table.next_page': ['Next'],
'search.num_records': ['%s record', '%s records...'],
};
const translations: Partial<Record<Locale, typeof en>> = {
en,
fr: {
'Query Mode': [''],
Aggregate: [''],
'Raw Records': [''],
'Emit Filter Events': [''],
'Show Cell Bars': [''],
'page_size.show': ['Afficher'],
'page_size.all': ['tous'],
'page_size.entries': ['entrées'],
'table.previous_page': ['Précédent'],
'table.next_page': ['Suivante'],
'search.num_records': ['%s enregistrement', '%s enregistrements...'],
},
zh: {
'Query Mode': ['查询模式'],
Aggregate: ['分组聚合'],
'Raw Records': ['原始数据'],
'Emit Filter Events': ['关联看板过滤器'],
'Show Cell Bars': ['为指标添加条状图背景'],
'page_size.show': ['每页显示'],
'page_size.all': ['全部'],
'page_size.entries': ['条'],
'table.previous_page': ['上一页'],
'table.next_page': ['下一页'],
'search.num_records': ['%s条记录...'],
},
};
export default translations;

View File

@@ -18,7 +18,7 @@
*/
import { useCallback, useState, FormEvent } from 'react';
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
import { Radio, RadioChangeEvent } from '@superset-ui/core/components/Radio';
import {
AsyncSelect,
@@ -27,6 +27,8 @@ import {
Modal,
Input,
type SelectValue,
Icons,
Flex,
} from '@superset-ui/core/components';
import {
styled,
@@ -372,17 +374,17 @@ export const SaveDatasetModal = ({
return (
<Modal
show={visible}
title={t('Save or Overwrite Dataset')}
name={t('Save or Overwrite Dataset')}
title={
<ModalTitleWithIcon
title={t('Save or Overwrite Dataset')}
icon={<Icons.SaveOutlined />}
data-test="save-or-overwrite-dataset-title"
/>
}
onHide={onHide}
footer={
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '8px',
}}
>
<Flex align="center" justify="flex-end" gap="8px">
{isFeatureEnabled(FeatureFlag.EnableTemplateProcessing) && (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Checkbox
@@ -422,7 +424,7 @@ export const SaveDatasetModal = ({
</Button>
</>
)}
</div>
</Flex>
}
>
<Styles>

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import { useState, useEffect, useMemo, ChangeEvent } from 'react';
import type { DatabaseObject } from 'src/features/databases/types';
import { t, styled } from '@superset-ui/core';
import {
@@ -28,6 +27,7 @@ import {
Modal,
Row,
Col,
Icons,
} from '@superset-ui/core/components';
import { Menu } from '@superset-ui/core/components/Menu';
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
@@ -43,6 +43,7 @@ import {
LOG_ACTIONS_SQLLAB_CREATE_CHART,
LOG_ACTIONS_SQLLAB_SAVE_QUERY,
} from 'src/logger/LogUtils';
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
interface SaveQueryProps {
queryEditorId: string;
@@ -224,7 +225,14 @@ const SaveQuery = ({
primaryButtonName={isSaved ? t('Save') : t('Save as')}
width="620px"
show={showSave}
title={<h4>{t('Save query')}</h4>}
name={t('Save query')}
title={
<ModalTitleWithIcon
title={t('Save query')}
icon={<Icons.SaveOutlined />}
data-test="save-query-modal-title"
/>
}
footer={
<>
<Button

View File

@@ -1089,6 +1089,7 @@ const SqlEditor: FC<Props> = ({
)}
<Modal
show={showCreateAsModal}
name={t(createViewModalTitle)}
title={t(createViewModalTitle)}
onHide={() => setShowCreateAsModal(false)}
footer={

View File

@@ -368,7 +368,9 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
<div data-test="table-element" css={{ paddingTop: 6 }}>
{renderWell()}
<div>
{cols?.map(col => <ColumnElement column={col} key={col.name} />)}
{cols?.map(col => (
<ColumnElement column={col} key={col.name} />
))}
</div>
</div>
);

View File

@@ -16,7 +16,9 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 15.0942V13.8545L8.88866 6.92188H9.86557V8.74676H9.2457L6.10669 13.7156V13.795H12.1219V15.0942H4.5ZM9.31512 17.0778V14.7173L9.32504 14.152V6.92188H10.778V17.0778H9.31512Z" fill="#666666"/>
<path d="M15.2336 14.4942L15.2237 12.6842H15.4816L18.5164 9.46085H20.2917L16.8304 13.1305H16.5973L15.2336 14.4942ZM13.8699 17.0778V6.92188H15.3526V17.0778H13.8699ZM18.6801 17.0778L15.9527 13.4577L16.9742 12.4213L20.5 17.0778H18.6801Z" fill="#666666"/>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 14" fill="none">
<g transform="translate(-4.5, -6.92188)">
<path d="M4.5 15.0942V13.8545L8.88866 6.92188H9.86557V8.74676H9.2457L6.10669 13.7156V13.795H12.1219V15.0942H4.5ZM9.31512 17.0778V14.7173L9.32504 14.152V6.92188H10.778V17.0778H9.31512Z" fill="#666666"/>
<path d="M15.2336 14.4942L15.2237 12.6842H15.4816L18.5164 9.46085H20.2917L16.8304 13.1305H16.5973L15.2336 14.4942ZM13.8699 17.0778V6.92188H15.3526V17.0778H13.8699ZM18.6801 17.0778L15.9527 13.4577L16.9742 12.4213L20.5 17.0778H18.6801Z" fill="#666666"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -467,6 +467,7 @@ export default function DrillByModal({
`}
show
onHide={onHideModal ?? (() => null)}
name={t('Drill by: %s', chartName)}
title={t('Drill by: %s', chartName)}
footer={<ModalFooter formData={drilledFormData} />}
responsive

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