mirror of
https://github.com/apache/superset.git
synced 2026-06-13 11:39:16 +00:00
Compare commits
1 Commits
default_ch
...
example_th
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc01eba075 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
# https://github.com/apache/superset/issues/13351
|
||||
|
||||
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
|
||||
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho
|
||||
|
||||
# Notify some committers of changes in the components
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -131,6 +131,3 @@ superset/static/stats/statistics.html
|
||||
# LLM-related
|
||||
CLAUDE.local.md
|
||||
.aider*
|
||||
.claude_rc*
|
||||
.env.local
|
||||
PROJECT.md
|
||||
|
||||
@@ -32,10 +32,11 @@ else
|
||||
SUPERSET_VERSION="${1}"
|
||||
SUPERSET_RC="${2}"
|
||||
SUPERSET_PGP_FULLNAME="${3}"
|
||||
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
|
||||
SUPERSET_RELEASE_RC_TARBALL="apache_superset-${SUPERSET_VERSION_RC}-source.tar.gz"
|
||||
fi
|
||||
|
||||
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
|
||||
|
||||
if [ -z "${SUPERSET_SVN_DEV_PATH}" ]; then
|
||||
SUPERSET_SVN_DEV_PATH="$HOME/svn/superset_dev"
|
||||
fi
|
||||
|
||||
@@ -28,7 +28,6 @@ These features are considered **unfinished** and should only be used on developm
|
||||
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
|
||||
|
||||
- ALERT_REPORT_TABS
|
||||
- DATE_RANGE_TIMESHIFTS_ENABLED
|
||||
- ENABLE_ADVANCED_DATA_TYPES
|
||||
- PRESTO_EXPAND_DATA
|
||||
- SHARE_QUERIES_VIA_KV_STORE
|
||||
|
||||
@@ -94,9 +94,9 @@ under the License.
|
||||
| can available domains on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can request access on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||
| can post on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
|
||||
| can expanded on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
|
||||
| can delete on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
|
||||
| can post on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| can expanded on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| can delete on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| can get on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
| can post on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
| can delete query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
|
||||
@@ -23,13 +23,6 @@ This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
|
||||
- Change `"error.base"` to just `"error"` after this PR
|
||||
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
|
||||
- Custom colors are no longer supported to maintain consistency with Ant Design components
|
||||
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
|
||||
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
|
||||
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
|
||||
@@ -39,7 +32,6 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
|
||||
@@ -17,47 +17,16 @@
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Lightweight docker-compose for running multiple Superset instances
|
||||
# This includes only essential services: database and Superset app (no Redis)
|
||||
# This includes only essential services: database, Redis, and Superset app
|
||||
#
|
||||
# RUNNING SUPERSET:
|
||||
# 1. Start services: docker-compose -f docker-compose-light.yml up
|
||||
# 2. Access at: http://localhost:9001 (or NODE_PORT if specified)
|
||||
#
|
||||
# RUNNING MULTIPLE INSTANCES:
|
||||
# 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
|
||||
#
|
||||
# RUNNING TESTS WITH PYTEST:
|
||||
# Tests run in an isolated environment with a separate test database.
|
||||
# The pytest-runner service automatically creates and initializes the test database on first use.
|
||||
#
|
||||
# Basic usage:
|
||||
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/
|
||||
#
|
||||
# Run specific test file:
|
||||
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/test_foo.py
|
||||
#
|
||||
# Run with pytest options:
|
||||
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest -v -s -x tests/
|
||||
#
|
||||
# Force reload test database and run tests (when tests are failing due to bad state):
|
||||
# docker-compose -f docker-compose-light.yml run --rm -e FORCE_RELOAD=true pytest-runner pytest tests/
|
||||
#
|
||||
# Run any command in test environment:
|
||||
# docker-compose -f docker-compose-light.yml run --rm pytest-runner bash
|
||||
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest --collect-only
|
||||
#
|
||||
# For parallel test execution with different projects:
|
||||
# docker-compose -p project1 -f docker-compose-light.yml run --rm pytest-runner pytest tests/
|
||||
#
|
||||
# DEVELOPMENT TIPS:
|
||||
# - First test run takes ~20-30 seconds (database creation + initialization)
|
||||
# - Subsequent runs are fast (~2-3 seconds startup)
|
||||
# - Use FORCE_RELOAD=true when you need a clean test database
|
||||
# - Tests use SimpleCache instead of Redis (no Redis required)
|
||||
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed logs
|
||||
# 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
|
||||
@@ -87,14 +56,13 @@ services:
|
||||
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
|
||||
# Increase max connections for test runs
|
||||
command: postgres -c max_connections=200
|
||||
|
||||
superset-light:
|
||||
env_file:
|
||||
@@ -182,34 +150,6 @@ services:
|
||||
required: false
|
||||
volumes: *superset-volumes
|
||||
|
||||
pytest-runner:
|
||||
build:
|
||||
<<: *common-build
|
||||
entrypoint: ["/app/docker/docker-pytest-entrypoint.sh"]
|
||||
env_file:
|
||||
- path: docker/.env # default
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
profiles:
|
||||
- test # Only starts when --profile test is used
|
||||
depends_on:
|
||||
db-light:
|
||||
condition: service_started
|
||||
user: *superset-user
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
# Test-specific database configuration
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: test
|
||||
POSTGRES_DB: test
|
||||
# Point to test database
|
||||
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@db-light:5432/test
|
||||
# Use the light test config that doesn't require Redis
|
||||
SUPERSET_CONFIG: superset_test_config_light
|
||||
# Python path includes test directory
|
||||
PYTHONPATH: /app/pythonpath:/app/docker/pythonpath_dev:/app
|
||||
|
||||
volumes:
|
||||
superset_home_light:
|
||||
external: false
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
echo "Waiting for database to be ready..."
|
||||
for i in {1..30}; do
|
||||
if python3 -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
|
||||
conn.close()
|
||||
print('Database is ready!')
|
||||
except:
|
||||
exit(1)
|
||||
" 2>/dev/null; then
|
||||
echo "Database connection established!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for database... ($i/30)"
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "Database connection timeout after 30 seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Handle database setup based on FORCE_RELOAD
|
||||
if [ "${FORCE_RELOAD}" = "true" ]; then
|
||||
echo "Force reload requested - resetting test database"
|
||||
# Drop and recreate the test database using Python
|
||||
python3 -c "
|
||||
import psycopg2
|
||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||
|
||||
# Connect to default database
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
|
||||
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Drop and recreate test database
|
||||
try:
|
||||
cur.execute('DROP DATABASE IF EXISTS test')
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.execute('CREATE DATABASE test')
|
||||
conn.close()
|
||||
|
||||
# Connect to test database to create schemas
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
|
||||
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute('CREATE SCHEMA sqllab_test_db')
|
||||
cur.execute('CREATE SCHEMA admin_database')
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print('Test database reset successfully')
|
||||
"
|
||||
# Use --no-reset-db since we already reset it
|
||||
FLAGS="--no-reset-db"
|
||||
else
|
||||
echo "Using existing test database (set FORCE_RELOAD=true to reset)"
|
||||
FLAGS="--no-reset-db"
|
||||
|
||||
# Ensure test database exists using Python
|
||||
python3 -c "
|
||||
import psycopg2
|
||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||
|
||||
# Check if test database exists
|
||||
try:
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
|
||||
conn.close()
|
||||
print('Test database already exists')
|
||||
except:
|
||||
print('Creating test database...')
|
||||
# Connect to default database to create test database
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
|
||||
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Create test database
|
||||
cur.execute('CREATE DATABASE test')
|
||||
conn.close()
|
||||
|
||||
# Connect to test database to create schemas
|
||||
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
|
||||
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute('CREATE SCHEMA IF NOT EXISTS sqllab_test_db')
|
||||
cur.execute('CREATE SCHEMA IF NOT EXISTS admin_database')
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print('Test database created successfully')
|
||||
"
|
||||
fi
|
||||
|
||||
# Always run database migrations to ensure schema is up to date
|
||||
echo "Running database migrations..."
|
||||
cd /app
|
||||
superset db upgrade
|
||||
|
||||
# Initialize test environment if needed
|
||||
if [ "${FORCE_RELOAD}" = "true" ] || [ ! -f "/app/superset_home/.test_initialized" ]; then
|
||||
echo "Initializing test environment..."
|
||||
# Run initialization commands
|
||||
superset init
|
||||
echo "Loading test users..."
|
||||
superset load-test-users
|
||||
|
||||
# Mark as initialized
|
||||
touch /app/superset_home/.test_initialized
|
||||
else
|
||||
echo "Test environment already initialized (skipping init and load-test-users)"
|
||||
echo "Tip: Use FORCE_RELOAD=true to reinitialize the test database"
|
||||
fi
|
||||
|
||||
# Create missing scripts needed for tests
|
||||
if [ ! -f "/app/scripts/tag_latest_release.sh" ]; then
|
||||
echo "Creating missing tag_latest_release.sh script for tests..."
|
||||
cp /app/docker/tag_latest_release.sh /app/scripts/tag_latest_release.sh 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install pip module for Shillelagh compatibility (aligns with CI environment)
|
||||
echo "Installing pip module for Shillelagh compatibility..."
|
||||
uv pip install pip
|
||||
|
||||
# If arguments provided, execute them
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
@@ -1,55 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Test configuration for docker-compose-light.yml - uses SimpleCache instead of Redis
|
||||
|
||||
# Import all settings from the main test config first
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the tests directory to the path to import the test config
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
from tests.integration_tests.superset_test_config import * # noqa: F403
|
||||
|
||||
# Override Redis-based caching to use simple in-memory cache
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "SimpleCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
"CACHE_KEY_PREFIX": "superset_test_",
|
||||
}
|
||||
|
||||
DATA_CACHE_CONFIG = {
|
||||
**CACHE_CONFIG,
|
||||
"CACHE_DEFAULT_TIMEOUT": 30,
|
||||
"CACHE_KEY_PREFIX": "superset_test_data_",
|
||||
}
|
||||
|
||||
# Keep SimpleCache for these as they're already using it
|
||||
# FILTER_STATE_CACHE_CONFIG - already SimpleCache in parent
|
||||
# EXPLORE_FORM_DATA_CACHE_CONFIG - already SimpleCache in parent
|
||||
|
||||
# Disable Celery for lightweight testing
|
||||
CELERY_CONFIG = None
|
||||
|
||||
# Use FileSystemCache for SQL Lab results instead of Redis
|
||||
from flask_caching.backends.filesystemcache import FileSystemCache # noqa: E402
|
||||
|
||||
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab_test")
|
||||
|
||||
# Override WEBDRIVER_BASEURL for tests to match expected values
|
||||
WEBDRIVER_BASEURL = "http://0.0.0.0:8080/"
|
||||
WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL
|
||||
@@ -1,190 +0,0 @@
|
||||
#! /bin/bash
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
run_git_tag () {
|
||||
if [[ "$DRY_RUN" == "false" ]] && [[ "$SKIP_TAG" == "false" ]]
|
||||
then
|
||||
git tag -a -f latest "${GITHUB_TAG_NAME}" -m "latest tag"
|
||||
echo "${GITHUB_TAG_NAME} has been tagged 'latest'"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
###
|
||||
# separating out git commands into functions so they can be mocked in unit tests
|
||||
###
|
||||
git_show_ref () {
|
||||
if [[ "$TEST_ENV" == "true" ]]
|
||||
then
|
||||
if [[ "$GITHUB_TAG_NAME" == "does_not_exist" ]]
|
||||
# mock return for testing only
|
||||
then
|
||||
echo ""
|
||||
else
|
||||
echo "2817aebd69dc7d199ec45d973a2079f35e5658b6 refs/tags/${GITHUB_TAG_NAME}"
|
||||
fi
|
||||
fi
|
||||
result=$(git show-ref "${GITHUB_TAG_NAME}")
|
||||
echo "${result}"
|
||||
}
|
||||
|
||||
get_latest_tag_list () {
|
||||
if [[ "$TEST_ENV" == "true" ]]
|
||||
then
|
||||
echo "(tag: 2.1.0, apache/2.1test)"
|
||||
else
|
||||
result=$(git show-ref --tags --dereference latest | awk '{print $2}' | xargs git show --pretty=tformat:%d -s | grep tag:)
|
||||
echo "${result}"
|
||||
fi
|
||||
}
|
||||
###
|
||||
|
||||
split_string () {
|
||||
local version="$1"
|
||||
local delimiter="$2"
|
||||
local components=()
|
||||
local tmp=""
|
||||
for (( i=0; i<${#version}; i++ )); do
|
||||
local char="${version:$i:1}"
|
||||
if [[ "$char" != "$delimiter" ]]; then
|
||||
tmp="$tmp$char"
|
||||
elif [[ -n "$tmp" ]]; then
|
||||
components+=("$tmp")
|
||||
tmp=""
|
||||
fi
|
||||
done
|
||||
if [[ -n "$tmp" ]]; then
|
||||
components+=("$tmp")
|
||||
fi
|
||||
echo "${components[@]}"
|
||||
}
|
||||
|
||||
DRY_RUN=false
|
||||
|
||||
# get params passed in with script when it was run
|
||||
# --dry-run is optional and returns the value of SKIP_TAG, but does not run the git tag statement
|
||||
# A tag name is required as a param. A SHA won't work. You must first tag a sha with a release number
|
||||
# and then run this script
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift # past value
|
||||
;;
|
||||
*) # this should be the tag name
|
||||
GITHUB_TAG_NAME=$key
|
||||
shift # past value
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "${GITHUB_TAG_NAME}" ]; then
|
||||
echo "Missing tag parameter, usage: ./scripts/tag_latest_release.sh <GITHUB_TAG_NAME>"
|
||||
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$(git_show_ref)" ]; then
|
||||
echo "The tag ${GITHUB_TAG_NAME} does not exist. Please use a different tag."
|
||||
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# check that this tag only contains a proper semantic version
|
||||
if ! [[ ${GITHUB_TAG_NAME} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
then
|
||||
echo "This tag ${GITHUB_TAG_NAME} is not a valid release version. Not tagging."
|
||||
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## split the current GITHUB_TAG_NAME into an array at the dot
|
||||
THIS_TAG_NAME=$(split_string "${GITHUB_TAG_NAME}" ".")
|
||||
|
||||
# look up the 'latest' tag on git
|
||||
LATEST_TAG_LIST=$(get_latest_tag_list) || echo 'not found'
|
||||
|
||||
# if 'latest' tag doesn't exist, then set this commit to latest
|
||||
if [[ -z "$LATEST_TAG_LIST" ]]
|
||||
then
|
||||
echo "there are no latest tags yet, so I'm going to start by tagging this sha as the latest"
|
||||
run_git_tag
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# remove parenthesis and tag: from the list of tags
|
||||
LATEST_TAGS_STRINGS=$(echo "$LATEST_TAG_LIST" | sed 's/tag: \([^,]*\)/\1/g' | tr -d '()')
|
||||
|
||||
LATEST_TAGS=$(split_string "$LATEST_TAGS_STRINGS" ",")
|
||||
TAGS=($(split_string "$LATEST_TAGS" " "))
|
||||
|
||||
# Initialize a flag for comparison result
|
||||
compare_result=""
|
||||
|
||||
# Iterate through the tags of the latest release
|
||||
for tag in $TAGS
|
||||
do
|
||||
if [[ $tag == "latest" ]]; then
|
||||
continue
|
||||
else
|
||||
## extract just the version from this tag
|
||||
LATEST_RELEASE_TAG="$tag"
|
||||
echo "LATEST_RELEASE_TAG: ${LATEST_RELEASE_TAG}"
|
||||
|
||||
# check that this only contains a proper semantic version
|
||||
if ! [[ ${LATEST_RELEASE_TAG} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
then
|
||||
echo "'Latest' has been associated with tag ${LATEST_RELEASE_TAG} which is not a valid release version. Looking for another."
|
||||
continue
|
||||
fi
|
||||
echo "The current release with the latest tag is version ${LATEST_RELEASE_TAG}"
|
||||
# Split the version strings into arrays
|
||||
THIS_TAG_NAME_ARRAY=($(split_string "$THIS_TAG_NAME" "."))
|
||||
LATEST_RELEASE_TAG_ARRAY=($(split_string "$LATEST_RELEASE_TAG" "."))
|
||||
|
||||
# Iterate through the components of the version strings
|
||||
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
|
||||
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="greater"
|
||||
break
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="lesser"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
# Determine the result based on the comparison
|
||||
if [[ -z "$compare_result" ]]; then
|
||||
echo "Versions are equal"
|
||||
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
|
||||
elif [[ "$compare_result" == "greater" ]]; then
|
||||
echo "This release tag ${GITHUB_TAG_NAME} is newer than the latest."
|
||||
echo "SKIP_TAG=false" >> $GITHUB_OUTPUT
|
||||
# Add other actions you want to perform for a newer version
|
||||
elif [[ "$compare_result" == "lesser" ]]; then
|
||||
echo "This release tag ${GITHUB_TAG_NAME} is older than the latest."
|
||||
echo "This release tag ${GITHUB_TAG_NAME} is not the latest. Not tagging."
|
||||
# if you've gotten this far, then we don't want to run any tags in the next step
|
||||
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -13,9 +13,9 @@ apache-superset>=6.0
|
||||
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.
|
||||
|
||||
## Managing Themes via UI
|
||||
## Managing Themes via CRUD Interface
|
||||
|
||||
Superset includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
|
||||
Superset now includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
|
||||
|
||||
### Creating a New Theme
|
||||
|
||||
@@ -29,38 +29,22 @@ Superset includes a built-in **Theme Management** interface accessible from the
|
||||
|
||||
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
|
||||
|
||||
### System Theme Administration
|
||||
|
||||
When `ENABLE_UI_THEME_ADMINISTRATION = True` is configured, administrators can manage system-wide themes directly from the UI:
|
||||
|
||||
#### Setting System Themes
|
||||
- **System Default Theme**: Click the sun icon on any theme to set it as the system-wide default
|
||||
- **System Dark Theme**: Click the moon icon on any theme to set it as the system dark mode theme
|
||||
- **Automatic OS Detection**: When both default and dark themes are set, Superset automatically detects and applies the appropriate theme based on OS preferences
|
||||
|
||||
#### Managing System Themes
|
||||
- System themes are indicated with special badges in the theme list
|
||||
- Only administrators with write permissions can modify system theme settings
|
||||
- Removing a system theme designation reverts to configuration file defaults
|
||||
|
||||
### Applying Themes to Dashboards
|
||||
|
||||
Once created, themes can be applied to individual dashboards:
|
||||
- Edit any dashboard and select your custom theme from the theme dropdown
|
||||
- Each dashboard can have its own theme, allowing for branded or context-specific styling
|
||||
|
||||
## Configuration Options
|
||||
## Alternative: Instance-wide Configuration
|
||||
|
||||
### Python Configuration
|
||||
For system-wide theming, you can configure default themes via Python configuration:
|
||||
|
||||
Configure theme behavior via `superset_config.py`:
|
||||
### Setting Default Themes
|
||||
|
||||
```python
|
||||
# Enable UI-based theme administration for admins
|
||||
ENABLE_UI_THEME_ADMINISTRATION = True
|
||||
# superset_config.py
|
||||
|
||||
# Optional: Set initial default themes via configuration
|
||||
# These can be overridden via the UI when ENABLE_UI_THEME_ADMINISTRATION = True
|
||||
# Default theme (light mode)
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"colorPrimary": "#2893B3",
|
||||
@@ -69,7 +53,7 @@ THEME_DEFAULT = {
|
||||
}
|
||||
}
|
||||
|
||||
# Optional: Dark theme configuration
|
||||
# Dark theme configuration
|
||||
THEME_DARK = {
|
||||
"algorithm": "dark",
|
||||
"token": {
|
||||
@@ -78,28 +62,23 @@ THEME_DARK = {
|
||||
}
|
||||
}
|
||||
|
||||
# To force a single theme on all users, set THEME_DARK = None
|
||||
# When both themes are defined (via UI or config):
|
||||
# - Users can manually switch between themes
|
||||
# - OS preference detection is automatically enabled
|
||||
# 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
|
||||
}
|
||||
```
|
||||
|
||||
### Migration from Configuration to UI
|
||||
### Copying Themes from CRUD Interface
|
||||
|
||||
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
|
||||
To use a theme created via the CRUD interface as your system default:
|
||||
|
||||
1. System themes set via the UI take precedence over configuration file settings
|
||||
2. The UI shows which themes are currently set as system defaults
|
||||
3. Administrators can change system themes without restarting Superset
|
||||
4. Configuration file themes serve as fallbacks when no UI themes are set
|
||||
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
|
||||
|
||||
### Copying Themes Between Systems
|
||||
|
||||
To export a theme for use in configuration files or another instance:
|
||||
|
||||
1. Navigate to **Settings > Themes** and click the export icon on your desired theme
|
||||
2. Extract the JSON configuration from the exported YAML file
|
||||
3. Use this JSON in your `superset_config.py` or import it into another Superset instance
|
||||
Restart Superset to apply changes.
|
||||
|
||||
## Theme Development Workflow
|
||||
|
||||
@@ -167,26 +146,7 @@ This feature works with the stock Docker image - no custom build required!
|
||||
|
||||
## Advanced Features
|
||||
|
||||
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
|
||||
- **System Themes**: Superset includes built-in light and dark themes
|
||||
- **Per-Dashboard Theming**: Each dashboard can have its own visual identity
|
||||
- **JSON Editor**: Edit theme configurations directly within Superset's interface
|
||||
- **Custom Fonts**: Load external fonts via configuration without rebuilding
|
||||
- **OS Dark Mode Detection**: Automatically switches themes based on system preferences
|
||||
- **Theme Import/Export**: Share themes between instances via YAML files
|
||||
|
||||
## API Access
|
||||
|
||||
For programmatic theme management, Superset provides REST endpoints:
|
||||
|
||||
- `GET /api/v1/theme/` - List all themes
|
||||
- `POST /api/v1/theme/` - Create a new theme
|
||||
- `PUT /api/v1/theme/{id}` - Update a theme
|
||||
- `DELETE /api/v1/theme/{id}` - Delete a theme
|
||||
- `PUT /api/v1/theme/{id}/set_system_default` - Set as system default theme (admin only)
|
||||
- `PUT /api/v1/theme/{id}/set_system_dark` - Set as system dark theme (admin only)
|
||||
- `DELETE /api/v1/theme/unset_system_default` - Remove system default designation
|
||||
- `DELETE /api/v1/theme/unset_system_dark` - Remove system dark designation
|
||||
- `GET /api/v1/theme/export/` - Export themes as YAML
|
||||
- `POST /api/v1/theme/import/` - Import themes from YAML
|
||||
|
||||
These endpoints require appropriate permissions and are subject to RBAC controls.
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
title: CVEs fixed by release
|
||||
sidebar_position: 2
|
||||
---
|
||||
#### Version 5.0.0
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2025-55673 | Exposure of Sensitive Information to an Unauthorized Actor | < 5.0.0 |
|
||||
| CVE-2025-55674 | Improper Neutralization of Special Elements used in an SQL Command | < 5.0.0 |
|
||||
| CVE-2025-55675 | Improper Access Control leading to Information Disclosure | < 5.0.0 |
|
||||
|
||||
#### Version 4.1.3
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2025-55672 | Improper Neutralization of Input During Web Page Generation | < 4.1.3 |
|
||||
|
||||
#### Version 4.1.2
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.0",
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
200
docs/yarn.lock
200
docs/yarn.lock
@@ -2150,7 +2150,14 @@
|
||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz"
|
||||
integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
@@ -2198,7 +2205,12 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.32.0", "@eslint/js@^9.32.0":
|
||||
"@eslint/js@9.31.0":
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
|
||||
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
|
||||
|
||||
"@eslint/js@^9.32.0":
|
||||
version "9.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
|
||||
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
|
||||
@@ -2208,10 +2220,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
|
||||
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
||||
|
||||
"@eslint/plugin-kit@^0.3.4":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz#c6b9f165e94bf4d9fdd493f1c028a94aaf5fc1cc"
|
||||
integrity sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==
|
||||
"@eslint/plugin-kit@^0.3.1":
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
|
||||
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
|
||||
dependencies:
|
||||
"@eslint/core" "^0.15.1"
|
||||
levn "^0.4.1"
|
||||
@@ -3717,79 +3729,79 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.39.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz#c9afec1866ee1a6ea3d768b5f8e92201efbbba06"
|
||||
integrity sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==
|
||||
"@typescript-eslint/eslint-plugin@8.37.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
|
||||
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.39.0"
|
||||
"@typescript-eslint/type-utils" "8.39.0"
|
||||
"@typescript-eslint/utils" "8.39.0"
|
||||
"@typescript-eslint/visitor-keys" "8.39.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/type-utils" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.39.0", "@typescript-eslint/parser@^8.37.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.0.tgz#c4b895d7a47f4cd5ee6ee77ea30e61d58b802008"
|
||||
integrity sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==
|
||||
"@typescript-eslint/parser@8.37.0", "@typescript-eslint/parser@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
|
||||
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.39.0"
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/typescript-estree" "8.39.0"
|
||||
"@typescript-eslint/visitor-keys" "8.39.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz#71cb29c3f8139f99a905b8705127bffc2ae84759"
|
||||
integrity sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==
|
||||
"@typescript-eslint/project-service@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
|
||||
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.39.0"
|
||||
"@typescript-eslint/types" "^8.39.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.37.0"
|
||||
"@typescript-eslint/types" "^8.37.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz#ba4bf6d8257bbc172c298febf16bc22df4856570"
|
||||
integrity sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==
|
||||
"@typescript-eslint/scope-manager@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
|
||||
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/visitor-keys" "8.39.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.39.0", "@typescript-eslint/tsconfig-utils@^8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz#b2e87fef41a3067c570533b722f6af47be213f13"
|
||||
integrity sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==
|
||||
"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
|
||||
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz#310ec781ae5e7bb0f5940bfd652573587f22786b"
|
||||
integrity sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==
|
||||
"@typescript-eslint/type-utils@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
|
||||
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/typescript-estree" "8.39.0"
|
||||
"@typescript-eslint/utils" "8.39.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.39.0", "@typescript-eslint/types@^8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.0.tgz#80f010b7169d434a91cd0529d70a528dbc9c99c6"
|
||||
integrity sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==
|
||||
"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
|
||||
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz#b9477a5c47a0feceffe91adf553ad9a3cd4cb3d6"
|
||||
integrity sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==
|
||||
"@typescript-eslint/typescript-estree@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
|
||||
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.39.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.39.0"
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/visitor-keys" "8.39.0"
|
||||
"@typescript-eslint/project-service" "8.37.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/visitor-keys" "8.37.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -3797,22 +3809,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.0.tgz#dfea42f3c7ec85f9f3e994ff0bba8f3b2f09e220"
|
||||
integrity sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==
|
||||
"@typescript-eslint/utils@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
|
||||
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.39.0"
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/typescript-estree" "8.39.0"
|
||||
"@typescript-eslint/scope-manager" "8.37.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz#5d619a6e810cdd3fd1913632719cbccab08bf875"
|
||||
integrity sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==
|
||||
"@typescript-eslint/visitor-keys@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
|
||||
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.39.0"
|
||||
"@typescript-eslint/types" "8.37.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -5605,13 +5617,20 @@ debug@2.6.9:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0:
|
||||
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
decode-named-character-reference@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf"
|
||||
@@ -6137,10 +6156,10 @@ eslint-config-prettier@^10.1.8:
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
|
||||
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
|
||||
|
||||
eslint-plugin-prettier@^5.5.3:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
|
||||
integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
|
||||
eslint-plugin-prettier@^5.5.1:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
|
||||
integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.11.7"
|
||||
@@ -6195,10 +6214,10 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.32.0:
|
||||
version "9.32.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.32.0.tgz#4ea28df4a8dbc454e1251e0f3aed4bcf4ce50a47"
|
||||
integrity sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==
|
||||
eslint@^9.31.0:
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba"
|
||||
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
@@ -6206,8 +6225,8 @@ eslint@^9.32.0:
|
||||
"@eslint/config-helpers" "^0.3.0"
|
||||
"@eslint/core" "^0.15.0"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.32.0"
|
||||
"@eslint/plugin-kit" "^0.3.4"
|
||||
"@eslint/js" "9.31.0"
|
||||
"@eslint/plugin-kit" "^0.3.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@humanwhocodes/retry" "^0.4.2"
|
||||
@@ -9040,6 +9059,11 @@ ms@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@2.1.3, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
@@ -12348,15 +12372,15 @@ types-ramda@^0.30.0:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.39.0:
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.39.0.tgz#b19c1a925cf8566831ae3875d2881ee2349808a5"
|
||||
integrity sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==
|
||||
typescript-eslint@^8.37.0:
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
|
||||
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.39.0"
|
||||
"@typescript-eslint/parser" "8.39.0"
|
||||
"@typescript-eslint/typescript-estree" "8.39.0"
|
||||
"@typescript-eslint/utils" "8.39.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.37.0"
|
||||
"@typescript-eslint/parser" "8.37.0"
|
||||
"@typescript-eslint/typescript-estree" "8.37.0"
|
||||
"@typescript-eslint/utils" "8.37.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
apiVersion: v2
|
||||
appVersion: "5.0.0"
|
||||
appVersion: "4.1.2"
|
||||
description: Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
name: superset
|
||||
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.15.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.14.3
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 13.4.4
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -336,6 +336,3 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
|
||||
| tolerations | list | `[]` | |
|
||||
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
|
||||
|
||||
## Versioning
|
||||
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.
|
||||
|
||||
@@ -48,6 +48,3 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
|
||||
## Versioning
|
||||
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.
|
||||
|
||||
@@ -79,7 +79,6 @@ dependencies = [
|
||||
"parsedatetime",
|
||||
"paramiko>=3.4.0",
|
||||
"pgsanity",
|
||||
"Pillow>=11.0.0, <12",
|
||||
"polyline>=2.0.0, <3.0",
|
||||
"pyparsing>=3.0.6, <4",
|
||||
"python-dateutil",
|
||||
@@ -182,7 +181,7 @@ tdengine = [
|
||||
"taos-ws-py>=0.3.8"
|
||||
]
|
||||
teradata = ["teradatasql>=16.20.0.23"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
thumbnails = ["Pillow>=10.0.1, <11"]
|
||||
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
|
||||
netezza = ["nzalchemy>=11.0.2"]
|
||||
starrocks = ["starrocks>=1.0.0"]
|
||||
@@ -196,7 +195,6 @@ development = [
|
||||
"grpcio>=1.55.3",
|
||||
"openapi-spec-validator",
|
||||
"parameterized",
|
||||
"pip",
|
||||
"pre-commit",
|
||||
"progress>=1.5,<2",
|
||||
"psutil",
|
||||
@@ -401,7 +399,6 @@ authorized_licenses = [
|
||||
"isc license (iscl)",
|
||||
"isc license",
|
||||
"mit",
|
||||
"mit-cmu",
|
||||
"mozilla public license 2.0 (mpl 2.0)",
|
||||
"osi approved",
|
||||
"osi approved",
|
||||
|
||||
@@ -266,8 +266,6 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
pillow==11.3.0
|
||||
# via apache_superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
|
||||
@@ -537,12 +537,10 @@ pgsanity==0.2.9
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
pillow==11.3.0
|
||||
pillow==10.3.0
|
||||
# via
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
pip==25.1.1
|
||||
# via apache-superset
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
|
||||
@@ -403,7 +403,6 @@ module.exports = {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'icons/no-fa-icons-usage': 'error',
|
||||
'i18n-strings/no-template-vars': ['error', true],
|
||||
'i18n-strings/sentence-case-buttons': 'error',
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { LOGIN } from 'cypress/utils/urls';
|
||||
|
||||
function interceptLogin() {
|
||||
cy.intercept('POST', '**/login/').as('login');
|
||||
cy.intercept('POST', '/login/').as('login');
|
||||
}
|
||||
|
||||
describe('Login view', () => {
|
||||
|
||||
@@ -31,52 +31,6 @@ import {
|
||||
interceptFormDataKey,
|
||||
} from '../explore/utils';
|
||||
|
||||
const interceptDrillInfo = () => {
|
||||
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
result: {
|
||||
id: 1,
|
||||
changed_on_humanized: '2 days ago',
|
||||
created_on_humanized: 'a week ago',
|
||||
table_name: 'birth_names',
|
||||
changed_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
created_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
column_name: 'gender',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'state',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'name',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'ds',
|
||||
verbose_name: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).as('drillInfo');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="close-drill-by-modal"]').length) {
|
||||
@@ -108,7 +62,6 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
@@ -282,14 +235,12 @@ describe('Drill by modal', () => {
|
||||
closeModal();
|
||||
});
|
||||
before(() => {
|
||||
interceptDrillInfo();
|
||||
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
|
||||
});
|
||||
|
||||
describe('Modal actions + Table', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -438,7 +389,6 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -602,7 +552,6 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('Horizontal FilterBar', () => {
|
||||
]);
|
||||
setFilterBarOrientation('horizontal');
|
||||
|
||||
cy.get('.filter-item-wrapper').should('have.length', 4);
|
||||
cy.get('.filter-item-wrapper').should('have.length', 3);
|
||||
openMoreFilters();
|
||||
cy.getBySel('form-item-value').should('have.length', 12);
|
||||
cy.getBySel('filter-control-name').contains('test_3').should('be.visible');
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import {
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
@@ -343,7 +344,7 @@ describe('Native filters', () => {
|
||||
it('User can delete a native filter', () => {
|
||||
enterNativeFilterEditModal(false);
|
||||
cy.get(nativeFilters.filtersList.removeIcon).first().click();
|
||||
cy.contains('Restore filter').should('not.exist', { timeout: 10000 });
|
||||
cy.contains('Restore Filter').should('not.exist', { timeout: 10000 });
|
||||
});
|
||||
|
||||
it('User can cancel creating a new filter', () => {
|
||||
|
||||
20
superset-frontend/cypress-base/package-lock.json
generated
20
superset-frontend/cypress-base/package-lock.json
generated
@@ -10227,11 +10227,14 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
|
||||
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"dependencies": {
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
"node": ">=8.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
@@ -18595,9 +18598,12 @@
|
||||
"peer": true
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
|
||||
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ=="
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"requires": {
|
||||
"rimraf": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
|
||||
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can’t handle strings that include variables",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -52,67 +52,5 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
},
|
||||
'sentence-case-buttons': {
|
||||
create(context) {
|
||||
function isTitleCase(str) {
|
||||
// Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words)
|
||||
return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str);
|
||||
}
|
||||
|
||||
function isButtonContext(node) {
|
||||
const { parent } = node;
|
||||
if (!parent) return false;
|
||||
|
||||
// Check for button-specific props
|
||||
if (parent.type === 'Property') {
|
||||
const key = parent.key.name;
|
||||
return [
|
||||
'primaryButtonName',
|
||||
'secondaryButtonName',
|
||||
'confirmButtonText',
|
||||
'cancelButtonText',
|
||||
].includes(key);
|
||||
}
|
||||
|
||||
// Check for Button components
|
||||
if (parent.type === 'JSXExpressionContainer') {
|
||||
const jsx = parent.parent;
|
||||
if (jsx?.type === 'JSXElement') {
|
||||
const elementName = jsx.openingElement.name.name;
|
||||
return elementName === 'Button';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handler(node) {
|
||||
if (node.arguments.length) {
|
||||
const firstArg = node.arguments[0];
|
||||
if (
|
||||
firstArg.type === 'Literal' &&
|
||||
typeof firstArg.value === 'string'
|
||||
) {
|
||||
const text = firstArg.value;
|
||||
|
||||
if (isButtonContext(node) && isTitleCase(text)) {
|
||||
const sentenceCase = text
|
||||
.toLowerCase()
|
||||
.replace(/^\w/, c => c.toUpperCase());
|
||||
context.report({
|
||||
node: firstArg,
|
||||
message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression[callee.name='t']": handler,
|
||||
"CallExpression[callee.name='tn']": handler,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
1749
superset-frontend/package-lock.json
generated
1749
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.21.1",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.12",
|
||||
"@rjsf/validator-ajv8": "^5.24.9",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -121,6 +121,8 @@
|
||||
"@visx/scale": "^3.5.0",
|
||||
"@visx/tooltip": "^3.0.0",
|
||||
"@visx/xychart": "^3.5.1",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "34.0.2",
|
||||
"antd": "^5.24.6",
|
||||
"chrono-node": "^2.7.8",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -142,7 +144,7 @@
|
||||
"geostyler-qgis-parser": "2.0.1",
|
||||
"geostyler-style": "7.5.0",
|
||||
"geostyler-wfs-parser": "^2.0.3",
|
||||
"googleapis": "^154.1.0",
|
||||
"googleapis": "^130.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"interweave": "^13.1.0",
|
||||
"jquery": "^3.7.1",
|
||||
@@ -174,7 +176,7 @@
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-json-tree": "^0.20.0",
|
||||
"react-lines-ellipsis": "^0.16.1",
|
||||
"react-lines-ellipsis": "^0.15.4",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
@@ -214,11 +216,11 @@
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/register": "^7.23.7",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@babel/runtime-corejs3": "^7.28.2",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@babel/runtime-corejs3": "^7.26.0",
|
||||
"@babel/types": "^7.26.9",
|
||||
"@cypress/react": "^8.0.2",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -241,6 +243,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -282,7 +285,7 @@
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-file-progress": "^1.5.0",
|
||||
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.5",
|
||||
"jest": "^30.0.4",
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -36,19 +36,18 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
const columns = ensureIsArray(
|
||||
queryObject.series_columns || queryObject.columns,
|
||||
);
|
||||
const timeOffsets = ensureIsArray(formData.time_compare);
|
||||
const { truncate_metric } = formData;
|
||||
const xAxisLabel = getXAxisLabel(formData);
|
||||
const isTimeComparisonValue = isTimeComparison(formData, queryObject);
|
||||
|
||||
// remove or rename top level of column name(metric name) in the MultiIndex when
|
||||
// 1) at least 1 metric
|
||||
// 2) dimension exist or multiple time shift metrics exist
|
||||
// 2) dimension exist
|
||||
// 3) xAxis exist
|
||||
// 4) truncate_metric in form_data and truncate_metric is true
|
||||
if (
|
||||
metrics.length > 0 &&
|
||||
(columns.length > 0 || timeOffsets.length > 1) &&
|
||||
columns.length > 0 &&
|
||||
xAxisLabel &&
|
||||
truncate_metric !== undefined &&
|
||||
!!truncate_metric
|
||||
@@ -85,8 +84,7 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
ComparisonType.Percentage,
|
||||
ComparisonType.Ratio,
|
||||
].includes(formData.comparison_type) &&
|
||||
metrics.length === 1 &&
|
||||
renamePairs.length === 0
|
||||
metrics.length === 1
|
||||
) {
|
||||
renamePairs.push([getMetricLabel(metrics[0]), null]);
|
||||
}
|
||||
|
||||
@@ -29,4 +29,3 @@ export * from './getStandardizedControls';
|
||||
export * from './getTemporalColumns';
|
||||
export * from './displayTimeRelatedControls';
|
||||
export * from './colorControls';
|
||||
export * from './metricColumnFilter';
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { QueryFormMetric, SqlaFormData } from '@superset-ui/core';
|
||||
import {
|
||||
shouldSkipMetricColumn,
|
||||
isRegularMetric,
|
||||
isPercentMetric,
|
||||
} from './metricColumnFilter';
|
||||
|
||||
const createMetric = (label: string): QueryFormMetric =>
|
||||
({
|
||||
label,
|
||||
expressionType: 'SIMPLE',
|
||||
column: { column_name: label },
|
||||
aggregate: 'SUM',
|
||||
}) as QueryFormMetric;
|
||||
|
||||
describe('metricColumnFilter', () => {
|
||||
const createFormData = (
|
||||
metrics: string[],
|
||||
percentMetrics: string[],
|
||||
): SqlaFormData =>
|
||||
({
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'table',
|
||||
metrics: metrics.map(createMetric),
|
||||
percent_metrics: percentMetrics.map(createMetric),
|
||||
}) as SqlaFormData;
|
||||
|
||||
describe('shouldSkipMetricColumn', () => {
|
||||
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
|
||||
const colnames = ['metric1', '%metric1'];
|
||||
const formData = createFormData([], ['metric1']);
|
||||
|
||||
const result = shouldSkipMetricColumn({
|
||||
colname: 'metric1',
|
||||
colnames,
|
||||
formData,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not skip if column is also a regular metric', () => {
|
||||
const colnames = ['metric1', '%metric1'];
|
||||
const formData = createFormData(['metric1'], ['metric1']);
|
||||
|
||||
const result = shouldSkipMetricColumn({
|
||||
colname: 'metric1',
|
||||
colnames,
|
||||
formData,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not skip if column starts with %', () => {
|
||||
const colnames = ['%metric1'];
|
||||
const formData = createFormData(['metric1'], []);
|
||||
|
||||
const result = shouldSkipMetricColumn({
|
||||
colname: '%metric1',
|
||||
colnames,
|
||||
formData,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not skip if no prefixed version exists', () => {
|
||||
const colnames = ['metric1'];
|
||||
const formData = createFormData([], ['metric1']);
|
||||
|
||||
const result = shouldSkipMetricColumn({
|
||||
colname: 'metric1',
|
||||
colnames,
|
||||
formData,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRegularMetric', () => {
|
||||
it('should return true for regular metrics', () => {
|
||||
const formData = createFormData(['metric1', 'metric2'], []);
|
||||
expect(isRegularMetric('metric1', formData)).toBe(true);
|
||||
expect(isRegularMetric('metric2', formData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-metrics', () => {
|
||||
const formData = createFormData(['metric1'], []);
|
||||
expect(isRegularMetric('non_metric', formData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for percentage metrics', () => {
|
||||
const formData = createFormData([], ['percent_metric1']);
|
||||
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPercentMetric', () => {
|
||||
it('should return true for percentage metrics', () => {
|
||||
const formData = createFormData([], ['percent_metric1']);
|
||||
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-percentage metrics', () => {
|
||||
const formData = createFormData(['regular_metric'], []);
|
||||
expect(isPercentMetric('regular_metric', formData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for regular metrics', () => {
|
||||
const formData = createFormData(['metric1'], []);
|
||||
expect(isPercentMetric('metric1', formData)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
QueryFormMetric,
|
||||
getMetricLabel,
|
||||
SqlaFormData,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export interface MetricColumnFilterParams {
|
||||
colname: string;
|
||||
colnames: string[];
|
||||
formData: SqlaFormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a column should be skipped based on metric filtering logic.
|
||||
*
|
||||
* This function implements the logic to skip unprefixed percent metric columns
|
||||
* if a prefixed version exists, but doesn't skip if it's also a regular metric.
|
||||
*
|
||||
* @param params - The parameters for metric column filtering
|
||||
* @returns true if the column should be skipped, false otherwise
|
||||
*/
|
||||
export function shouldSkipMetricColumn({
|
||||
colname,
|
||||
colnames,
|
||||
formData,
|
||||
}: MetricColumnFilterParams): boolean {
|
||||
if (!colname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this column name exists as a percent metric in form data
|
||||
const isPercentMetric = formData.percent_metrics?.some(
|
||||
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
|
||||
);
|
||||
|
||||
// Check if this column name exists as a regular metric in form data
|
||||
const isRegularMetric = formData.metrics?.some(
|
||||
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
|
||||
);
|
||||
|
||||
// Check if there's a prefixed version of this column in the column list
|
||||
const hasPrefixedVersion = colnames.includes(`%${colname}`);
|
||||
|
||||
// Skip if: has prefixed version AND is percent metric AND is NOT regular metric
|
||||
return hasPrefixedVersion && isPercentMetric && !isRegularMetric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a column is a regular metric.
|
||||
*
|
||||
* @param colname - The column name to check
|
||||
* @param formData - The form data containing metrics
|
||||
* @returns true if the column is a regular metric, false otherwise
|
||||
*/
|
||||
export function isRegularMetric(
|
||||
colname: string,
|
||||
formData: SqlaFormData,
|
||||
): boolean {
|
||||
return !!formData.metrics?.some(metric => getMetricLabel(metric) === colname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a column is a percentage metric.
|
||||
*
|
||||
* @param colname: string,
|
||||
* @param formData - The form data containing percent_metrics
|
||||
* @returns true if the column is a percentage metric, false otherwise
|
||||
*/
|
||||
export function isPercentMetric(
|
||||
colname: string,
|
||||
formData: SqlaFormData,
|
||||
): boolean {
|
||||
return !!formData.percent_metrics?.some(
|
||||
(metric: QueryFormMetric) => `%${getMetricLabel(metric)}` === colname,
|
||||
);
|
||||
}
|
||||
@@ -65,20 +65,6 @@ test('should skip renameOperator if series does not exist', () => {
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('should skip renameOperator if series does not exist and a single time shift exists', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
{ ...formData, ...{ time_compare: ['1 year ago'] } },
|
||||
{
|
||||
...queryObject,
|
||||
...{
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('should skip renameOperator if does not exist x_axis and is_timeseries', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
@@ -107,26 +93,6 @@ test('should add renameOperator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if a metric exists and multiple time shift', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
{
|
||||
...formData,
|
||||
...{ time_compare: ['1 year ago', '2 years ago'] },
|
||||
},
|
||||
{
|
||||
...queryObject,
|
||||
...{
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'rename',
|
||||
options: { columns: { 'count(*)': null }, inplace: true, level: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if exists derived metrics', () => {
|
||||
[
|
||||
ComparisonType.Difference,
|
||||
@@ -210,6 +176,7 @@ test('should add renameOperator if exist "actual value" time comparison', () =>
|
||||
operation: 'rename',
|
||||
options: {
|
||||
columns: {
|
||||
'count(*)': null,
|
||||
'count(*)__1 year ago': '1 year ago',
|
||||
'count(*)__1 year later': '1 year later',
|
||||
},
|
||||
|
||||
@@ -25,13 +25,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"ace-builds": "^1.43.1",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "34.0.2",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.2.5",
|
||||
"csstype": "^3.1.3",
|
||||
@@ -48,10 +46,10 @@
|
||||
"lodash": "^4.17.21",
|
||||
"math-expression-evaluator": "^2.0.6",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"re-resizable": "^6.10.1",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
@@ -80,7 +78,7 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.10.3",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/prop-types": "^15.7.2",
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"fetch-mock": "^11.1.4",
|
||||
|
||||
@@ -24,7 +24,6 @@ export const Badge = styled((props: BadgeProps) => <AntdBadge {...props} />)`
|
||||
${({ theme, color, count }) => `
|
||||
& > sup,
|
||||
& > sup.ant-badge-count {
|
||||
box-shadow: none;
|
||||
${
|
||||
count !== undefined ? `background: ${color || theme.colorPrimary};` : ''
|
||||
}
|
||||
|
||||
@@ -132,12 +132,11 @@ export function Button(props: ButtonProps) {
|
||||
'& > span > :first-of-type': {
|
||||
marginRight: firstChildMargin,
|
||||
},
|
||||
':not(:hover)': effectiveButtonStyle === 'secondary' &&
|
||||
!disabled && {
|
||||
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
|
||||
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
|
||||
color: `${theme.colorPrimaryTextHover} !important`,
|
||||
},
|
||||
':not(:hover)': effectiveButtonStyle === 'secondary' && {
|
||||
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
|
||||
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
|
||||
color: `${theme.colorPrimaryTextHover} !important`,
|
||||
},
|
||||
}}
|
||||
icon={icon}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { DrawerProps } from './types';
|
||||
|
||||
export { Drawer } from 'antd';
|
||||
export type { DrawerProps };
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { DrawerProps } from 'antd/es/drawer';
|
||||
|
||||
export { DrawerProps };
|
||||
@@ -1,395 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { Global } from '@emotion/react';
|
||||
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { Badge, Icons, Button, Tooltip, Popover } from '..';
|
||||
import { DropdownContainerProps, DropdownItem, DropdownRef } from './types';
|
||||
|
||||
const MAX_HEIGHT = 500;
|
||||
|
||||
export const DropdownContainer = forwardRef(
|
||||
(
|
||||
{
|
||||
items,
|
||||
onOverflowingStateChange,
|
||||
dropdownContent,
|
||||
dropdownRef,
|
||||
dropdownStyle = {},
|
||||
dropdownTriggerCount,
|
||||
dropdownTriggerIcon,
|
||||
dropdownTriggerText = t('More'),
|
||||
dropdownTriggerTooltip = null,
|
||||
forceRender,
|
||||
style,
|
||||
}: DropdownContainerProps,
|
||||
outerRef: RefObject<DropdownRef>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
|
||||
const previousWidth = usePrevious(width) || 0;
|
||||
const { current } = ref;
|
||||
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
// We use React.useState to be able to mock the state in Jest
|
||||
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
|
||||
|
||||
let targetRef = useRef<HTMLDivElement>(null);
|
||||
if (dropdownRef) {
|
||||
targetRef = dropdownRef;
|
||||
}
|
||||
|
||||
const [showOverflow, setShowOverflow] = useState(false);
|
||||
|
||||
// callback to update item widths so that the useLayoutEffect runs whenever
|
||||
// width of any of the child changes
|
||||
const recalculateItemWidths = useCallback(() => {
|
||||
const mainItemsContainerNode = current?.children.item(0);
|
||||
if (mainItemsContainerNode) {
|
||||
const visibleChildrenElements = Array.from(
|
||||
mainItemsContainerNode.children,
|
||||
);
|
||||
setItemsWidth(prevGlobalWidths => {
|
||||
if (prevGlobalWidths.length !== items.length) {
|
||||
return prevGlobalWidths;
|
||||
}
|
||||
|
||||
const newGlobalWidths = [...prevGlobalWidths];
|
||||
let changed = false;
|
||||
visibleChildrenElements.forEach((child, indexInVisible) => {
|
||||
const originalItemIndex = indexInVisible;
|
||||
if (originalItemIndex < newGlobalWidths.length) {
|
||||
const newWidth = child.getBoundingClientRect().width;
|
||||
if (newGlobalWidths[originalItemIndex] !== newWidth) {
|
||||
newGlobalWidths[originalItemIndex] = newWidth;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changed ? newGlobalWidths : prevGlobalWidths;
|
||||
});
|
||||
}
|
||||
}, [current?.children, items.length]);
|
||||
|
||||
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
|
||||
items.reduce(
|
||||
([items, ids], item) => {
|
||||
items.push({
|
||||
id: item.id,
|
||||
element: cloneElement(item.element, { key: item.id }),
|
||||
});
|
||||
ids.push(item.id);
|
||||
return [items, ids];
|
||||
},
|
||||
[[], []] as [DropdownItem[], string[]],
|
||||
);
|
||||
|
||||
const [notOverflowedItems, notOverflowedIds] = useMemo(
|
||||
() =>
|
||||
reduceItems(
|
||||
items.slice(
|
||||
0,
|
||||
overflowingIndex !== -1 ? overflowingIndex : items.length,
|
||||
),
|
||||
),
|
||||
[items, overflowingIndex],
|
||||
);
|
||||
|
||||
const [overflowedItems, overflowedIds] = useMemo(
|
||||
() =>
|
||||
overflowingIndex !== -1
|
||||
? reduceItems(items.slice(overflowingIndex))
|
||||
: [[], []],
|
||||
[items, overflowingIndex],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const container = current?.children.item(0);
|
||||
if (!container) return;
|
||||
|
||||
const childrenArray = Array.from(container.children);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
recalculateItemWidths();
|
||||
});
|
||||
|
||||
childrenArray.map(child => resizeObserver.observe(child));
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => {
|
||||
childrenArray.map(child => resizeObserver.unobserve(child));
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [items.length, current, recalculateItemWidths]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
return;
|
||||
}
|
||||
const container = current?.children.item(0);
|
||||
if (container) {
|
||||
const { children } = container;
|
||||
const childrenArray = Array.from(children);
|
||||
// If items length change, add all items to the container
|
||||
// and recalculate the widths
|
||||
if (itemsWidth.length !== items.length) {
|
||||
if (childrenArray.length === items.length) {
|
||||
setItemsWidth(
|
||||
childrenArray.map(child => child.getBoundingClientRect().width),
|
||||
);
|
||||
} else {
|
||||
setOverflowingIndex(-1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the index of the first overflowed element
|
||||
// +1 is to give at least one pixel of difference and avoid flakiness
|
||||
const index = childrenArray.findIndex(
|
||||
child =>
|
||||
child.getBoundingClientRect().right >
|
||||
container.getBoundingClientRect().right + 1,
|
||||
);
|
||||
|
||||
// If elements fit (-1) and there's overflowed items
|
||||
// then preserve the overflow index. We can't use overflowIndex
|
||||
// directly because the items may have been modified
|
||||
let newOverflowingIndex =
|
||||
index === -1 && overflowedItems.length > 0
|
||||
? items.length - overflowedItems.length
|
||||
: index;
|
||||
|
||||
if (width > previousWidth) {
|
||||
// Calculates remaining space in the container
|
||||
const button = current?.children.item(1);
|
||||
const buttonRight = button?.getBoundingClientRect().right || 0;
|
||||
const containerRight = current?.getBoundingClientRect().right || 0;
|
||||
const remainingSpace = containerRight - buttonRight;
|
||||
|
||||
// Checks if some elements in the dropdown fits in the remaining space
|
||||
let sum = 0;
|
||||
for (let i = childrenArray.length; i < items.length; i += 1) {
|
||||
sum += itemsWidth[i];
|
||||
if (sum <= remainingSpace) {
|
||||
newOverflowingIndex = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOverflowingIndex(newOverflowingIndex);
|
||||
}
|
||||
}, [
|
||||
current,
|
||||
items.length,
|
||||
itemsWidth,
|
||||
overflowedItems.length,
|
||||
previousWidth,
|
||||
width,
|
||||
popoverVisible,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onOverflowingStateChange) {
|
||||
onOverflowingStateChange({
|
||||
notOverflowed: notOverflowedIds,
|
||||
overflowed: overflowedIds,
|
||||
});
|
||||
}
|
||||
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
|
||||
|
||||
const overflowingCount =
|
||||
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() =>
|
||||
dropdownContent || overflowingCount ? (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.sizeUnit * 4}px;
|
||||
`}
|
||||
data-test="dropdown-content"
|
||||
style={dropdownStyle}
|
||||
ref={targetRef}
|
||||
>
|
||||
{dropdownContent
|
||||
? dropdownContent(overflowedItems)
|
||||
: overflowedItems.map(item => item.element)}
|
||||
</div>
|
||||
) : null,
|
||||
[
|
||||
dropdownContent,
|
||||
overflowingCount,
|
||||
theme.sizeUnit,
|
||||
dropdownStyle,
|
||||
overflowedItems,
|
||||
],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
// Measures scroll height after rendering the elements
|
||||
setTimeout(() => {
|
||||
if (targetRef.current) {
|
||||
// We only set overflow when there's enough space to display
|
||||
// Select's popovers because they are restrained by the overflow property.
|
||||
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [popoverVisible]);
|
||||
|
||||
useImperativeHandle(
|
||||
outerRef,
|
||||
() => ({
|
||||
...(ref.current as HTMLDivElement),
|
||||
open: () => setPopoverVisible(true),
|
||||
}),
|
||||
[ref],
|
||||
);
|
||||
|
||||
// Closes the popover when scrolling on the document
|
||||
useEffect(() => {
|
||||
document.onscroll = popoverVisible
|
||||
? () => setPopoverVisible(false)
|
||||
: null;
|
||||
return () => {
|
||||
document.onscroll = null;
|
||||
};
|
||||
}, [popoverVisible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${theme.sizeUnit * 4}px;
|
||||
margin-right: ${theme.sizeUnit * 4}px;
|
||||
min-width: 0px;
|
||||
`}
|
||||
data-test="container"
|
||||
style={style}
|
||||
>
|
||||
{notOverflowedItems.map(item => item.element)}
|
||||
</div>
|
||||
{popoverContent && (
|
||||
<>
|
||||
<Global
|
||||
styles={css`
|
||||
.ant-popover-inner {
|
||||
// Some OS versions only show the scroll when hovering.
|
||||
// These settings will make the scroll always visible.
|
||||
::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 9px;
|
||||
background-color: ${theme.colors.grayscale.light1};
|
||||
border: 3px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: ${theme.colors.grayscale.light4};
|
||||
border-left: 1px solid ${theme.colors.grayscale.light2};
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
overflow: showOverflow ? 'auto' : 'visible',
|
||||
},
|
||||
}}
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={popoverVisible}
|
||||
onOpenChange={visible => setPopoverVisible(visible)}
|
||||
placement="bottom"
|
||||
forceRender={forceRender}
|
||||
>
|
||||
<Tooltip title={dropdownTriggerTooltip}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
data-test="dropdown-container-btn"
|
||||
icon={dropdownTriggerIcon}
|
||||
css={css`
|
||||
padding-left: ${theme.paddingXS}px;
|
||||
padding-right: ${theme.paddingXXS}px;
|
||||
gap: ${theme.sizeXXS}px;
|
||||
`}
|
||||
>
|
||||
{dropdownTriggerText}
|
||||
<Badge
|
||||
count={dropdownTriggerCount ?? overflowingCount}
|
||||
color={
|
||||
(dropdownTriggerCount ?? overflowingCount) > 0
|
||||
? theme.colorPrimary
|
||||
: theme.colors.grayscale.light1
|
||||
}
|
||||
showZero
|
||||
css={css`
|
||||
margin-left: ${theme.sizeUnit * 2}px;
|
||||
`}
|
||||
/>
|
||||
<Icons.DownOutlined
|
||||
iconSize="m"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
css={css`
|
||||
.anticon {
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -16,6 +16,448 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
CSSProperties,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
export { DropdownContainer } from './DropdownContainer';
|
||||
export type * from './types';
|
||||
import { Global } from '@emotion/react';
|
||||
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { Badge, Icons, Button, Tooltip, Popover } from '..';
|
||||
/**
|
||||
* Container item.
|
||||
*/
|
||||
export interface DropdownItem {
|
||||
/**
|
||||
* String that uniquely identifies the item.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The element to be rendered.
|
||||
*/
|
||||
element: ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal container that displays overflowed items in a dropdown.
|
||||
* It shows an indicator of how many items are currently overflowing.
|
||||
*/
|
||||
export interface DropdownContainerProps {
|
||||
/**
|
||||
* Array of items. The id property is used to uniquely identify
|
||||
* the elements when rendering or dealing with event handlers.
|
||||
*/
|
||||
items: DropdownItem[];
|
||||
/**
|
||||
* Event handler called every time an element moves between
|
||||
* main container and dropdown.
|
||||
*/
|
||||
onOverflowingStateChange?: (overflowingState: {
|
||||
notOverflowed: string[];
|
||||
overflowed: string[];
|
||||
}) => void;
|
||||
/**
|
||||
* Option to customize the content of the dropdown.
|
||||
*/
|
||||
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
|
||||
/**
|
||||
* Dropdown ref.
|
||||
*/
|
||||
dropdownRef?: RefObject<HTMLDivElement>;
|
||||
/**
|
||||
* Dropdown additional style properties.
|
||||
*/
|
||||
dropdownStyle?: CSSProperties;
|
||||
/**
|
||||
* Displayed count in the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerCount?: number;
|
||||
/**
|
||||
* Icon of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerIcon?: ReactElement;
|
||||
/**
|
||||
* Text of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerText?: string;
|
||||
/**
|
||||
* Text of the dropdown trigger tooltip
|
||||
*/
|
||||
dropdownTriggerTooltip?: ReactNode | null;
|
||||
/**
|
||||
* Main container additional style properties.
|
||||
*/
|
||||
style?: CSSProperties;
|
||||
/**
|
||||
* Force render popover content before it's first opened
|
||||
*/
|
||||
forceRender?: boolean;
|
||||
}
|
||||
|
||||
export type DropdownRef = HTMLDivElement & { open: () => void };
|
||||
|
||||
const MAX_HEIGHT = 500;
|
||||
|
||||
export const DropdownContainer = forwardRef(
|
||||
(
|
||||
{
|
||||
items,
|
||||
onOverflowingStateChange,
|
||||
dropdownContent,
|
||||
dropdownRef,
|
||||
dropdownStyle = {},
|
||||
dropdownTriggerCount,
|
||||
dropdownTriggerIcon,
|
||||
dropdownTriggerText = t('More'),
|
||||
dropdownTriggerTooltip = null,
|
||||
forceRender,
|
||||
style,
|
||||
}: DropdownContainerProps,
|
||||
outerRef: RefObject<DropdownRef>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
|
||||
const previousWidth = usePrevious(width) || 0;
|
||||
const { current } = ref;
|
||||
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
// We use React.useState to be able to mock the state in Jest
|
||||
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
|
||||
|
||||
let targetRef = useRef<HTMLDivElement>(null);
|
||||
if (dropdownRef) {
|
||||
targetRef = dropdownRef;
|
||||
}
|
||||
|
||||
const [showOverflow, setShowOverflow] = useState(false);
|
||||
|
||||
// callback to update item widths so that the useLayoutEffect runs whenever
|
||||
// width of any of the child changes
|
||||
const recalculateItemWidths = useCallback(() => {
|
||||
const mainItemsContainerNode = current?.children.item(0);
|
||||
if (mainItemsContainerNode) {
|
||||
const visibleChildrenElements = Array.from(
|
||||
mainItemsContainerNode.children,
|
||||
);
|
||||
setItemsWidth(prevGlobalWidths => {
|
||||
if (prevGlobalWidths.length !== items.length) {
|
||||
return prevGlobalWidths;
|
||||
}
|
||||
|
||||
const newGlobalWidths = [...prevGlobalWidths];
|
||||
let changed = false;
|
||||
visibleChildrenElements.forEach((child, indexInVisible) => {
|
||||
const originalItemIndex = indexInVisible;
|
||||
if (originalItemIndex < newGlobalWidths.length) {
|
||||
const newWidth = child.getBoundingClientRect().width;
|
||||
if (newGlobalWidths[originalItemIndex] !== newWidth) {
|
||||
newGlobalWidths[originalItemIndex] = newWidth;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changed ? newGlobalWidths : prevGlobalWidths;
|
||||
});
|
||||
}
|
||||
}, [current?.children, items.length]);
|
||||
|
||||
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
|
||||
items.reduce(
|
||||
([items, ids], item) => {
|
||||
items.push({
|
||||
id: item.id,
|
||||
element: cloneElement(item.element, { key: item.id }),
|
||||
});
|
||||
ids.push(item.id);
|
||||
return [items, ids];
|
||||
},
|
||||
[[], []] as [DropdownItem[], string[]],
|
||||
);
|
||||
|
||||
const [notOverflowedItems, notOverflowedIds] = useMemo(
|
||||
() =>
|
||||
reduceItems(
|
||||
items.slice(
|
||||
0,
|
||||
overflowingIndex !== -1 ? overflowingIndex : items.length,
|
||||
),
|
||||
),
|
||||
[items, overflowingIndex],
|
||||
);
|
||||
|
||||
const [overflowedItems, overflowedIds] = useMemo(
|
||||
() =>
|
||||
overflowingIndex !== -1
|
||||
? reduceItems(items.slice(overflowingIndex))
|
||||
: [[], []],
|
||||
[items, overflowingIndex],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const container = current?.children.item(0);
|
||||
if (!container) return;
|
||||
|
||||
const childrenArray = Array.from(container.children);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
recalculateItemWidths();
|
||||
});
|
||||
|
||||
childrenArray.map(child => resizeObserver.observe(child));
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => {
|
||||
childrenArray.map(child => resizeObserver.unobserve(child));
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [items.length, current, recalculateItemWidths]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
return;
|
||||
}
|
||||
const container = current?.children.item(0);
|
||||
if (container) {
|
||||
const { children } = container;
|
||||
const childrenArray = Array.from(children);
|
||||
// If items length change, add all items to the container
|
||||
// and recalculate the widths
|
||||
if (itemsWidth.length !== items.length) {
|
||||
if (childrenArray.length === items.length) {
|
||||
setItemsWidth(
|
||||
childrenArray.map(child => child.getBoundingClientRect().width),
|
||||
);
|
||||
} else {
|
||||
setOverflowingIndex(-1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the index of the first overflowed element
|
||||
// +1 is to give at least one pixel of difference and avoid flakiness
|
||||
const index = childrenArray.findIndex(
|
||||
child =>
|
||||
child.getBoundingClientRect().right >
|
||||
container.getBoundingClientRect().right + 1,
|
||||
);
|
||||
|
||||
// If elements fit (-1) and there's overflowed items
|
||||
// then preserve the overflow index. We can't use overflowIndex
|
||||
// directly because the items may have been modified
|
||||
let newOverflowingIndex =
|
||||
index === -1 && overflowedItems.length > 0
|
||||
? items.length - overflowedItems.length
|
||||
: index;
|
||||
|
||||
if (width > previousWidth) {
|
||||
// Calculates remaining space in the container
|
||||
const button = current?.children.item(1);
|
||||
const buttonRight = button?.getBoundingClientRect().right || 0;
|
||||
const containerRight = current?.getBoundingClientRect().right || 0;
|
||||
const remainingSpace = containerRight - buttonRight;
|
||||
|
||||
// Checks if some elements in the dropdown fits in the remaining space
|
||||
let sum = 0;
|
||||
for (let i = childrenArray.length; i < items.length; i += 1) {
|
||||
sum += itemsWidth[i];
|
||||
if (sum <= remainingSpace) {
|
||||
newOverflowingIndex = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOverflowingIndex(newOverflowingIndex);
|
||||
}
|
||||
}, [
|
||||
current,
|
||||
items.length,
|
||||
itemsWidth,
|
||||
overflowedItems.length,
|
||||
previousWidth,
|
||||
width,
|
||||
popoverVisible,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onOverflowingStateChange) {
|
||||
onOverflowingStateChange({
|
||||
notOverflowed: notOverflowedIds,
|
||||
overflowed: overflowedIds,
|
||||
});
|
||||
}
|
||||
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
|
||||
|
||||
const overflowingCount =
|
||||
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() =>
|
||||
dropdownContent || overflowingCount ? (
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.sizeUnit * 4}px;
|
||||
`}
|
||||
data-test="dropdown-content"
|
||||
style={dropdownStyle}
|
||||
ref={targetRef}
|
||||
>
|
||||
{dropdownContent
|
||||
? dropdownContent(overflowedItems)
|
||||
: overflowedItems.map(item => item.element)}
|
||||
</div>
|
||||
) : null,
|
||||
[
|
||||
dropdownContent,
|
||||
overflowingCount,
|
||||
theme.sizeUnit,
|
||||
dropdownStyle,
|
||||
overflowedItems,
|
||||
],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
// Measures scroll height after rendering the elements
|
||||
setTimeout(() => {
|
||||
if (targetRef.current) {
|
||||
// We only set overflow when there's enough space to display
|
||||
// Select's popovers because they are restrained by the overflow property.
|
||||
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [popoverVisible]);
|
||||
|
||||
useImperativeHandle(
|
||||
outerRef,
|
||||
() => ({
|
||||
...(ref.current as HTMLDivElement),
|
||||
open: () => setPopoverVisible(true),
|
||||
}),
|
||||
[ref],
|
||||
);
|
||||
|
||||
// Closes the popover when scrolling on the document
|
||||
useEffect(() => {
|
||||
document.onscroll = popoverVisible
|
||||
? () => setPopoverVisible(false)
|
||||
: null;
|
||||
return () => {
|
||||
document.onscroll = null;
|
||||
};
|
||||
}, [popoverVisible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${theme.sizeUnit * 4}px;
|
||||
margin-right: ${theme.sizeUnit * 4}px;
|
||||
min-width: 0px;
|
||||
`}
|
||||
data-test="container"
|
||||
style={style}
|
||||
>
|
||||
{notOverflowedItems.map(item => item.element)}
|
||||
</div>
|
||||
{popoverContent && (
|
||||
<>
|
||||
<Global
|
||||
styles={css`
|
||||
.ant-popover-inner {
|
||||
// Some OS versions only show the scroll when hovering.
|
||||
// These settings will make the scroll always visible.
|
||||
::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 9px;
|
||||
background-color: ${theme.colors.grayscale.light1};
|
||||
border: 3px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: ${theme.colors.grayscale.light4};
|
||||
border-left: 1px solid ${theme.colors.grayscale.light2};
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: `${MAX_HEIGHT}px`,
|
||||
overflow: showOverflow ? 'auto' : 'visible',
|
||||
},
|
||||
}}
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={popoverVisible}
|
||||
onOpenChange={visible => setPopoverVisible(visible)}
|
||||
placement="bottom"
|
||||
forceRender={forceRender}
|
||||
>
|
||||
<Tooltip title={dropdownTriggerTooltip}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
data-test="dropdown-container-btn"
|
||||
>
|
||||
{dropdownTriggerIcon}
|
||||
{dropdownTriggerText}
|
||||
<Badge
|
||||
count={dropdownTriggerCount ?? overflowingCount}
|
||||
color={
|
||||
(dropdownTriggerCount ?? overflowingCount) > 0
|
||||
? theme.colorPrimary
|
||||
: theme.colors.grayscale.light1
|
||||
}
|
||||
showZero
|
||||
css={css`
|
||||
margin-left: ${theme.sizeUnit * 2}px;
|
||||
`}
|
||||
/>
|
||||
<Icons.DownOutlined
|
||||
iconSize="m"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
css={css`
|
||||
.anticon {
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
|
||||
import { IconType } from '../Icons';
|
||||
|
||||
/**
|
||||
* Container item.
|
||||
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
|
||||
/**
|
||||
* Icon of the dropdown trigger.
|
||||
*/
|
||||
dropdownTriggerIcon?: IconType;
|
||||
dropdownTriggerIcon?: ReactElement;
|
||||
/**
|
||||
* Text of the dropdown trigger.
|
||||
*/
|
||||
|
||||
@@ -45,16 +45,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
|
||||
const labelType = datasetType === 'physical' ? 'primary' : 'default';
|
||||
|
||||
return (
|
||||
<Label
|
||||
icon={icon}
|
||||
type={labelType}
|
||||
style={{
|
||||
color:
|
||||
datasetType === 'physical'
|
||||
? theme.colorPrimaryText
|
||||
: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Label icon={icon} type={labelType}>
|
||||
{label}
|
||||
</Label>
|
||||
);
|
||||
|
||||
@@ -1047,119 +1047,6 @@ test('typing and deleting the last character for a new option displays correctly
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('grouped options search', () => {
|
||||
const GROUPED_OPTIONS = [
|
||||
{
|
||||
label: 'Male',
|
||||
options: OPTIONS.filter(option => option.gender === 'Male'),
|
||||
},
|
||||
{
|
||||
label: 'Female',
|
||||
options: OPTIONS.filter(option => option.gender === 'Female'),
|
||||
},
|
||||
];
|
||||
|
||||
it('searches within grouped options and shows matching groups', async () => {
|
||||
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
|
||||
await open();
|
||||
|
||||
await type('John');
|
||||
|
||||
expect(await findSelectOption('John')).toBeInTheDocument();
|
||||
expect(await findSelectOption('Johnny')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Female')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Olivia')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Male')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Female')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows multiple groups when search matches both', async () => {
|
||||
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
|
||||
await open();
|
||||
|
||||
await type('er');
|
||||
|
||||
expect(screen.getByText('Male')).toBeInTheDocument();
|
||||
expect(screen.getByText('Female')).toBeInTheDocument();
|
||||
expect(await findSelectOption('Oliver')).toBeInTheDocument();
|
||||
expect(await findSelectOption('Cher')).toBeInTheDocument();
|
||||
expect(await findSelectOption('Her')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles case-insensitive search in grouped options', async () => {
|
||||
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
|
||||
await open();
|
||||
|
||||
await type('EMMA');
|
||||
|
||||
expect(await findSelectOption('Emma')).toBeInTheDocument();
|
||||
expect(screen.getByText('Female')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Male')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no options when search matches nothing in any group', async () => {
|
||||
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
|
||||
await open();
|
||||
|
||||
await type('xyz123');
|
||||
|
||||
expect(screen.queryByText('Male')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Female')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(NO_DATA, { selector: '.ant-empty-description' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works in multiple selection mode with grouped options', async () => {
|
||||
render(
|
||||
<Select {...defaultProps} options={GROUPED_OPTIONS} mode="multiple" />,
|
||||
);
|
||||
await open();
|
||||
|
||||
await type('John');
|
||||
|
||||
await userEvent.click(await findSelectOption('John'));
|
||||
|
||||
// Clear search and search for female name
|
||||
await clearTypedText();
|
||||
await type('Emma');
|
||||
await userEvent.click(await findSelectOption('Emma'));
|
||||
|
||||
// Both should be selected
|
||||
const values = await findAllSelectValues();
|
||||
expect(values).toHaveLength(2);
|
||||
expect(values[0]).toHaveTextContent('John');
|
||||
expect(values[1]).toHaveTextContent('Emma');
|
||||
});
|
||||
|
||||
it('preserves group structure when not searching', async () => {
|
||||
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
|
||||
await open();
|
||||
|
||||
expect(screen.getByText('Male')).toBeInTheDocument();
|
||||
expect(screen.getByText('Female')).toBeInTheDocument();
|
||||
expect(await findSelectOption('John')).toBeInTheDocument();
|
||||
expect(await findSelectOption('Emma')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty groups gracefully', async () => {
|
||||
const optionsWithEmptyGroup = [
|
||||
...GROUPED_OPTIONS,
|
||||
{
|
||||
label: 'Empty Group',
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
|
||||
render(<Select {...defaultProps} options={optionsWithEmptyGroup} />);
|
||||
await open();
|
||||
|
||||
await type('John');
|
||||
expect(await findSelectOption('John')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Empty Group')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
TODO: Add tests that require scroll interaction. Needs further investigation.
|
||||
- Fetches more data when scrolling and more data is available
|
||||
|
||||
@@ -373,27 +373,9 @@ const Select = forwardRef(
|
||||
setSelectOptions(updatedOptions);
|
||||
}
|
||||
|
||||
const filteredOptions = updatedOptions
|
||||
.map((option: any) => {
|
||||
/*
|
||||
If it's a group, filter its nested options and only return it
|
||||
if it has matching options
|
||||
*/
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
const filteredGroupOptions = option.options.filter(
|
||||
(subOption: AntdLabeledValue) =>
|
||||
handleFilterOption(search, subOption),
|
||||
);
|
||||
return filteredGroupOptions.length > 0
|
||||
? { ...option, options: filteredGroupOptions }
|
||||
: null;
|
||||
}
|
||||
|
||||
return handleFilterOption(search, option as AntdLabeledValue)
|
||||
? option
|
||||
: null;
|
||||
})
|
||||
.filter((option): option is AntdLabeledValue => option !== null);
|
||||
const filteredOptions = updatedOptions.filter(
|
||||
(option: AntdLabeledValue) => handleFilterOption(search, option),
|
||||
);
|
||||
|
||||
setVisibleOptions(filteredOptions);
|
||||
setInputValue(searchValue);
|
||||
|
||||
@@ -32,9 +32,8 @@ export const StyledHeader = styled.span<{ headerPosition: string }>`
|
||||
`;
|
||||
|
||||
export const StyledContainer = styled.div<{ headerPosition: string }>`
|
||||
${({ headerPosition, theme }) => `
|
||||
${({ headerPosition }) => `
|
||||
display: flex;
|
||||
gap: ${theme.sizeUnit}px;
|
||||
flex-direction: ${headerPosition === 'top' ? 'column' : 'row'};
|
||||
align-items: ${headerPosition === 'left' ? 'center' : undefined};
|
||||
width: 100%;
|
||||
|
||||
@@ -132,7 +132,7 @@ const VirtualTable = <RecordType extends object>(
|
||||
if (gridRef.current) {
|
||||
return gridRef.current?.state?.scrollLeft;
|
||||
}
|
||||
return 0;
|
||||
return null;
|
||||
},
|
||||
set: (scrollLeft: number) => {
|
||||
if (gridRef.current) {
|
||||
|
||||
@@ -52,9 +52,7 @@ interface TableCollectionProps<T extends object> {
|
||||
const StyledTable = styled(Table)`
|
||||
${({ theme }) => `
|
||||
th.ant-column-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: fit-content;
|
||||
}
|
||||
.actions {
|
||||
opacity: 0;
|
||||
@@ -85,6 +83,7 @@ const StyledTable = styled(Table)`
|
||||
font-feature-settings: 'tnum' 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 320px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
padding-left: ${theme.sizeUnit * 4}px;
|
||||
@@ -150,7 +149,7 @@ function TableCollection<T extends object>({
|
||||
size={size}
|
||||
data-test="listview-table"
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
tableLayout="auto"
|
||||
rowKey="rowId"
|
||||
rowSelection={rowSelection}
|
||||
locale={{ emptyText: null }}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function mapColumns<T extends object>(
|
||||
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
|
||||
hidden: column.hidden,
|
||||
key: column.id,
|
||||
width: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md,
|
||||
minWidth: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md,
|
||||
ellipsis: !columnsForWrapText?.includes(column.id),
|
||||
defaultSortOrder: (isSorted
|
||||
? isSortedDesc
|
||||
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
userEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from 'spec/helpers/testing-library';
|
||||
} from '@superset-ui/core/spec';
|
||||
import { ThemeMode } from '@superset-ui/core';
|
||||
import { Menu } from '@superset-ui/core/components';
|
||||
import { ThemeSubMenuProps, useThemeMenuItems } from './useThemeMenuItems';
|
||||
import { ThemeSubMenu } from '.';
|
||||
|
||||
// Mock the translation function
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
@@ -33,12 +33,7 @@ jest.mock('@superset-ui/core', () => ({
|
||||
t: (key: string) => key,
|
||||
}));
|
||||
|
||||
const TestComponent = (props: ThemeSubMenuProps) => {
|
||||
const menuItem = useThemeMenuItems(props);
|
||||
return <Menu items={[menuItem]} />;
|
||||
};
|
||||
|
||||
describe('useThemeMenuItems', () => {
|
||||
describe('ThemeSubMenu', () => {
|
||||
const defaultProps = {
|
||||
allowOSPreference: true,
|
||||
setThemeMode: jest.fn(),
|
||||
@@ -47,8 +42,12 @@ describe('useThemeMenuItems', () => {
|
||||
onClearLocalSettings: jest.fn(),
|
||||
};
|
||||
|
||||
const renderThemeMenu = (props = defaultProps) =>
|
||||
render(<TestComponent {...props} />);
|
||||
const renderThemeSubMenu = (props = defaultProps) =>
|
||||
render(
|
||||
<Menu>
|
||||
<ThemeSubMenu {...props} />
|
||||
</Menu>,
|
||||
);
|
||||
|
||||
const findMenuWithText = async (text: string) => {
|
||||
await waitFor(() => {
|
||||
@@ -67,7 +66,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders Light and Dark theme options by default', async () => {
|
||||
renderThemeMenu();
|
||||
renderThemeSubMenu();
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Light');
|
||||
@@ -77,7 +76,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('does not render Match system option when allowOSPreference is false', async () => {
|
||||
renderThemeMenu({ ...defaultProps, allowOSPreference: false });
|
||||
renderThemeSubMenu({ ...defaultProps, allowOSPreference: false });
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -86,7 +85,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders with allowOSPreference as true by default', async () => {
|
||||
renderThemeMenu();
|
||||
renderThemeSubMenu();
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Match system');
|
||||
@@ -96,7 +95,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('renders clear option when both hasLocalOverride and onClearLocalSettings are provided', async () => {
|
||||
const mockClear = jest.fn();
|
||||
renderThemeMenu({
|
||||
renderThemeSubMenu({
|
||||
...defaultProps,
|
||||
hasLocalOverride: true,
|
||||
onClearLocalSettings: mockClear,
|
||||
@@ -110,7 +109,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('does not render clear option when hasLocalOverride is false', async () => {
|
||||
const mockClear = jest.fn();
|
||||
renderThemeMenu({
|
||||
renderThemeSubMenu({
|
||||
...defaultProps,
|
||||
hasLocalOverride: false,
|
||||
onClearLocalSettings: mockClear,
|
||||
@@ -125,7 +124,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('calls setThemeMode with DEFAULT when Light is clicked', async () => {
|
||||
const mockSet = jest.fn();
|
||||
renderThemeMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Light');
|
||||
@@ -136,7 +135,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('calls setThemeMode with DARK when Dark is clicked', async () => {
|
||||
const mockSet = jest.fn();
|
||||
renderThemeMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Dark');
|
||||
@@ -147,7 +146,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('calls setThemeMode with SYSTEM when Match system is clicked', async () => {
|
||||
const mockSet = jest.fn();
|
||||
renderThemeMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Match system');
|
||||
@@ -158,7 +157,7 @@ describe('useThemeMenuItems', () => {
|
||||
|
||||
it('calls onClearLocalSettings when Clear local theme is clicked', async () => {
|
||||
const mockClear = jest.fn();
|
||||
renderThemeMenu({
|
||||
renderThemeSubMenu({
|
||||
...defaultProps,
|
||||
hasLocalOverride: true,
|
||||
onClearLocalSettings: mockClear,
|
||||
@@ -172,27 +171,27 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('displays sun icon for DEFAULT theme', () => {
|
||||
renderThemeMenu({ ...defaultProps, themeMode: ThemeMode.DEFAULT });
|
||||
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DEFAULT });
|
||||
expect(screen.getByTestId('sun')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays moon icon for DARK theme', () => {
|
||||
renderThemeMenu({ ...defaultProps, themeMode: ThemeMode.DARK });
|
||||
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DARK });
|
||||
expect(screen.getByTestId('moon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays format-painter icon for SYSTEM theme', () => {
|
||||
renderThemeMenu({ ...defaultProps, themeMode: ThemeMode.SYSTEM });
|
||||
renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.SYSTEM });
|
||||
expect(screen.getByTestId('format-painter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays override icon when hasLocalOverride is true', () => {
|
||||
renderThemeMenu({ ...defaultProps, hasLocalOverride: true });
|
||||
renderThemeSubMenu({ ...defaultProps, hasLocalOverride: true });
|
||||
expect(screen.getByTestId('format-painter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Theme group header', async () => {
|
||||
renderThemeMenu();
|
||||
renderThemeSubMenu();
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Theme');
|
||||
@@ -201,7 +200,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders sun icon for Light theme option', async () => {
|
||||
renderThemeMenu();
|
||||
renderThemeSubMenu();
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Light');
|
||||
@@ -211,7 +210,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders moon icon for Dark theme option', async () => {
|
||||
renderThemeMenu();
|
||||
renderThemeSubMenu();
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Dark');
|
||||
@@ -221,7 +220,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders format-painter icon for Match system option', async () => {
|
||||
renderThemeMenu({ ...defaultProps, allowOSPreference: true });
|
||||
renderThemeSubMenu({ ...defaultProps, allowOSPreference: true });
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const menu = await findMenuWithText('Match system');
|
||||
@@ -233,7 +232,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders clear icon for Clear local theme option', async () => {
|
||||
renderThemeMenu({
|
||||
renderThemeSubMenu({
|
||||
...defaultProps,
|
||||
hasLocalOverride: true,
|
||||
onClearLocalSettings: jest.fn(),
|
||||
@@ -249,7 +248,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('renders divider before clear option when clear option is present', async () => {
|
||||
renderThemeMenu({
|
||||
renderThemeSubMenu({
|
||||
...defaultProps,
|
||||
hasLocalOverride: true,
|
||||
onClearLocalSettings: jest.fn(),
|
||||
@@ -264,7 +263,7 @@ describe('useThemeMenuItems', () => {
|
||||
});
|
||||
|
||||
it('does not render divider when clear option is not present', async () => {
|
||||
renderThemeMenu({ ...defaultProps });
|
||||
renderThemeSubMenu({ ...defaultProps });
|
||||
|
||||
userEvent.hover(await screen.findByRole('menuitem'));
|
||||
const divider = document.querySelector('.ant-menu-item-divider');
|
||||
@@ -17,9 +17,44 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { Icons } from '@superset-ui/core/components';
|
||||
import type { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { t, ThemeMode, useTheme, ThemeAlgorithm } from '@superset-ui/core';
|
||||
import { Icons, Menu } from '@superset-ui/core/components';
|
||||
import {
|
||||
css,
|
||||
styled,
|
||||
t,
|
||||
ThemeMode,
|
||||
useTheme,
|
||||
ThemeAlgorithm,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
const StyledThemeSubMenu = styled(Menu.SubMenu)`
|
||||
${({ theme }) => css`
|
||||
[data-icon='caret-down'] {
|
||||
color: ${theme.colorIcon};
|
||||
font-size: ${theme.fontSizeXS}px;
|
||||
margin-left: ${theme.sizeUnit}px;
|
||||
}
|
||||
&.ant-menu-submenu-active {
|
||||
.ant-menu-title-content {
|
||||
color: ${theme.colorPrimary};
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledThemeSubMenuItem = styled(Menu.Item)<{ selected: boolean }>`
|
||||
${({ theme, selected }) => css`
|
||||
&:hover {
|
||||
color: ${theme.colorPrimary} !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
${selected &&
|
||||
css`
|
||||
background-color: ${theme.colors.primary.light4} !important;
|
||||
color: ${theme.colors.primary.dark1} !important;
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
||||
export interface ThemeSubMenuOption {
|
||||
key: ThemeMode;
|
||||
@@ -36,13 +71,13 @@ export interface ThemeSubMenuProps {
|
||||
allowOSPreference?: boolean;
|
||||
}
|
||||
|
||||
export const useThemeMenuItems = ({
|
||||
export const ThemeSubMenu: React.FC<ThemeSubMenuProps> = ({
|
||||
setThemeMode,
|
||||
themeMode,
|
||||
hasLocalOverride = false,
|
||||
onClearLocalSettings,
|
||||
allowOSPreference = true,
|
||||
}: ThemeSubMenuProps): MenuItem => {
|
||||
}: ThemeSubMenuProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleSelect = (mode: ThemeMode) => {
|
||||
@@ -72,70 +107,64 @@ export const useThemeMenuItems = ({
|
||||
[hasLocalOverride, theme.colors.error.base, themeIconMap, themeMode],
|
||||
);
|
||||
|
||||
const themeOptions: MenuItem[] = [
|
||||
const themeOptions: ThemeSubMenuOption[] = [
|
||||
{
|
||||
key: ThemeMode.DEFAULT,
|
||||
label: (
|
||||
<>
|
||||
<Icons.SunOutlined /> {t('Light')}
|
||||
</>
|
||||
),
|
||||
label: t('Light'),
|
||||
icon: <Icons.SunOutlined />,
|
||||
onClick: () => handleSelect(ThemeMode.DEFAULT),
|
||||
},
|
||||
{
|
||||
key: ThemeMode.DARK,
|
||||
label: (
|
||||
<>
|
||||
<Icons.MoonOutlined /> {t('Dark')}
|
||||
</>
|
||||
),
|
||||
label: t('Dark'),
|
||||
icon: <Icons.MoonOutlined />,
|
||||
onClick: () => handleSelect(ThemeMode.DARK),
|
||||
},
|
||||
...(allowOSPreference
|
||||
? [
|
||||
{
|
||||
key: ThemeMode.SYSTEM,
|
||||
label: (
|
||||
<>
|
||||
<Icons.FormatPainterOutlined /> {t('Match system')}
|
||||
</>
|
||||
),
|
||||
label: t('Match system'),
|
||||
icon: <Icons.FormatPainterOutlined />,
|
||||
onClick: () => handleSelect(ThemeMode.SYSTEM),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const children: MenuItem[] = [
|
||||
{
|
||||
type: 'group' as const,
|
||||
label: t('Theme'),
|
||||
key: 'theme-group',
|
||||
children: themeOptions,
|
||||
},
|
||||
];
|
||||
|
||||
// Add clear settings option only when there's a local theme active
|
||||
if (onClearLocalSettings && hasLocalOverride) {
|
||||
children.push({
|
||||
type: 'divider' as const,
|
||||
key: 'theme-divider',
|
||||
});
|
||||
children.push({
|
||||
key: 'clear-local',
|
||||
label: (
|
||||
<>
|
||||
<Icons.ClearOutlined /> {t('Clear local theme')}
|
||||
</>
|
||||
),
|
||||
onClick: onClearLocalSettings,
|
||||
});
|
||||
}
|
||||
const clearOption =
|
||||
onClearLocalSettings && hasLocalOverride
|
||||
? {
|
||||
key: 'clear-local',
|
||||
label: t('Clear local theme'),
|
||||
icon: <Icons.ClearOutlined />,
|
||||
onClick: onClearLocalSettings,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
key: 'theme-sub-menu',
|
||||
label: selectedThemeModeIcon,
|
||||
icon: <Icons.CaretDownOutlined iconSize="xs" />,
|
||||
children,
|
||||
};
|
||||
return (
|
||||
<StyledThemeSubMenu
|
||||
key="theme-sub-menu"
|
||||
title={selectedThemeModeIcon}
|
||||
icon={<Icons.CaretDownOutlined iconSize="xs" />}
|
||||
>
|
||||
<Menu.ItemGroup title={t('Theme')} />
|
||||
{themeOptions.map(option => (
|
||||
<StyledThemeSubMenuItem
|
||||
key={option.key}
|
||||
onClick={option.onClick}
|
||||
selected={option.key === themeMode}
|
||||
>
|
||||
{option.icon} {option.label}
|
||||
</StyledThemeSubMenuItem>
|
||||
))}
|
||||
{clearOption && [
|
||||
<Menu.Divider key="theme-divider" />,
|
||||
<Menu.Item key={clearOption.key} onClick={clearOption.onClick}>
|
||||
{clearOption.icon} {clearOption.label}
|
||||
</Menu.Item>,
|
||||
]}
|
||||
</StyledThemeSubMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,220 +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 { render, screen } from '@superset-ui/core/spec';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { createRef } from 'react';
|
||||
import { ThemeProvider, supersetTheme } from '../../theme';
|
||||
import { ThemedAgGridReact } from './index';
|
||||
import * as themeUtils from '../../theme/utils/themeUtils';
|
||||
|
||||
// Mock useThemeMode hook
|
||||
jest.mock('../../theme/utils/themeUtils', () => ({
|
||||
...jest.requireActual('../../theme/utils/themeUtils'),
|
||||
useThemeMode: jest.fn(() => false), // Default to light mode
|
||||
}));
|
||||
|
||||
// Mock ag-grid-react to avoid complex setup
|
||||
jest.mock('ag-grid-react', () => ({
|
||||
AgGridReact: jest.fn(({ theme, ...props }) => (
|
||||
<div
|
||||
data-test="ag-grid-react"
|
||||
data-theme={JSON.stringify(theme)}
|
||||
{...props}
|
||||
>
|
||||
AgGrid Mock
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock ag-grid-community
|
||||
jest.mock('ag-grid-community', () => ({
|
||||
themeQuartz: {
|
||||
withPart: jest.fn().mockReturnThis(),
|
||||
withParams: jest.fn(params => ({ ...params, _type: 'theme' })),
|
||||
},
|
||||
colorSchemeDark: { _type: 'dark' },
|
||||
colorSchemeLight: { _type: 'light' },
|
||||
AllCommunityModule: {},
|
||||
ClientSideRowModelModule: {},
|
||||
ModuleRegistry: { registerModules: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockRowData = [
|
||||
{ id: 1, name: 'Test 1' },
|
||||
{ id: 2, name: 'Test 2' },
|
||||
];
|
||||
|
||||
const mockColumnDefs = [
|
||||
{ field: 'id', headerName: 'ID' },
|
||||
{ field: 'name', headerName: 'Name' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset to light mode by default
|
||||
(themeUtils.useThemeMode as jest.Mock).mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('renders the AgGridReact component', () => {
|
||||
render(
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('applies light theme when background is light', () => {
|
||||
const lightTheme = {
|
||||
...supersetTheme,
|
||||
colorBgBase: '#ffffff',
|
||||
colorText: '#000000',
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const agGrid = screen.getByTestId('ag-grid-react');
|
||||
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
|
||||
|
||||
expect(theme.browserColorScheme).toBe('light');
|
||||
expect(theme.foregroundColor).toBe('#000000');
|
||||
});
|
||||
|
||||
test('applies dark theme when background is dark', () => {
|
||||
// Mock dark mode
|
||||
(themeUtils.useThemeMode as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const darkTheme = {
|
||||
...supersetTheme,
|
||||
colorBgBase: '#1a1a1a',
|
||||
colorText: '#ffffff',
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const agGrid = screen.getByTestId('ag-grid-react');
|
||||
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
|
||||
|
||||
expect(theme.browserColorScheme).toBe('dark');
|
||||
expect(theme.foregroundColor).toBe('#ffffff');
|
||||
});
|
||||
|
||||
test('forwards ref to AgGridReact', () => {
|
||||
const ref = createRef<AgGridReact>();
|
||||
|
||||
render(
|
||||
<ThemedAgGridReact
|
||||
ref={ref}
|
||||
rowData={mockRowData}
|
||||
columnDefs={mockColumnDefs}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Check that AgGridReact was called with the ref
|
||||
expect(AgGridReact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rowData: mockRowData,
|
||||
columnDefs: mockColumnDefs,
|
||||
}),
|
||||
expect.any(Object), // ref is passed as second argument
|
||||
);
|
||||
});
|
||||
|
||||
test('passes all props through to AgGridReact', () => {
|
||||
const onGridReady = jest.fn();
|
||||
const onCellClicked = jest.fn();
|
||||
|
||||
render(
|
||||
<ThemedAgGridReact
|
||||
rowData={mockRowData}
|
||||
columnDefs={mockColumnDefs}
|
||||
onGridReady={onGridReady}
|
||||
onCellClicked={onCellClicked}
|
||||
pagination
|
||||
paginationPageSize={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(AgGridReact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rowData: mockRowData,
|
||||
columnDefs: mockColumnDefs,
|
||||
onGridReady,
|
||||
onCellClicked,
|
||||
pagination: true,
|
||||
paginationPageSize: 10,
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
test('applies custom theme colors from Superset theme', () => {
|
||||
const customTheme = {
|
||||
...supersetTheme,
|
||||
colorFillTertiary: '#e5e5e5',
|
||||
colorSplit: '#d9d9d9',
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const agGrid = screen.getByTestId('ag-grid-react');
|
||||
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
|
||||
|
||||
// Just verify a couple key theme properties are applied
|
||||
expect(theme.headerBackgroundColor).toBe('#e5e5e5');
|
||||
expect(theme.borderColor).toBe('#d9d9d9');
|
||||
});
|
||||
|
||||
test('wraps component with proper container div', () => {
|
||||
const { container } = render(
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />,
|
||||
);
|
||||
|
||||
const wrapper = container.querySelector('[data-themed-ag-grid="true"]');
|
||||
expect(wrapper).toBeInTheDocument();
|
||||
// Styles are now applied via css prop, not inline styles
|
||||
expect(wrapper).toHaveAttribute('data-themed-ag-grid', 'true');
|
||||
});
|
||||
|
||||
test('handles missing theme gracefully', () => {
|
||||
const incompleteTheme = {
|
||||
...supersetTheme,
|
||||
colorBgBase: undefined,
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={incompleteTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
// Should still render without crashing
|
||||
expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,190 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo, forwardRef } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { AgGridReact, type AgGridReactProps } from 'ag-grid-react';
|
||||
import {
|
||||
themeQuartz,
|
||||
colorSchemeDark,
|
||||
colorSchemeLight,
|
||||
} from 'ag-grid-community';
|
||||
import { useTheme } from '../../theme';
|
||||
import { useThemeMode } from '../../theme/utils/themeUtils';
|
||||
|
||||
// Note: With ag-grid v34's new theming API, CSS files are injected automatically
|
||||
// Do NOT import 'ag-grid-community/styles/ag-grid.css' or theme CSS files
|
||||
|
||||
export interface ThemedAgGridReactProps extends AgGridReactProps {
|
||||
/**
|
||||
* Optional theme parameter overrides to customize specific ag-grid theme values.
|
||||
* These will be merged with the default Superset theme values.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ThemedAgGridReact
|
||||
* rowData={data}
|
||||
* columnDefs={columns}
|
||||
* themeOverrides={{
|
||||
* headerBackgroundColor: '#custom-color',
|
||||
* fontSize: 14,
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
themeOverrides?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemedAgGridReact - A wrapper around AgGridReact that applies Superset theming
|
||||
*
|
||||
* This component:
|
||||
* - Preserves the full AgGridReactProps interface for drop-in replacement
|
||||
* - Applies Superset theme variables via ag-grid's JavaScript theming API
|
||||
* - Supports automatic dark/light mode switching
|
||||
* - Allows custom theme parameter overrides
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ThemedAgGridReact
|
||||
* rowData={data}
|
||||
* columnDefs={columns}
|
||||
* themeOverrides={{ fontSize: 14 }}
|
||||
* // ... any other AgGridReactProps
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const ThemedAgGridReact = forwardRef<
|
||||
AgGridReact,
|
||||
ThemedAgGridReactProps
|
||||
>(function ThemedAgGridReact({ themeOverrides, ...props }, ref) {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = useThemeMode();
|
||||
|
||||
// Get the appropriate ag-grid theme based on dark/light mode
|
||||
const agGridTheme = useMemo(() => {
|
||||
// Use quaternary fill for odd rows
|
||||
const oddRowBg = theme?.colorFillQuaternary;
|
||||
|
||||
const baseTheme = isDarkMode
|
||||
? themeQuartz.withPart(colorSchemeDark)
|
||||
: themeQuartz.withPart(colorSchemeLight);
|
||||
|
||||
// Use withParams to set colors directly via ag-grid's API
|
||||
const params = {
|
||||
// Core colors
|
||||
backgroundColor: 'transparent',
|
||||
foregroundColor: theme.colorText,
|
||||
browserColorScheme: isDarkMode ? 'dark' : 'light',
|
||||
|
||||
// Header styling
|
||||
headerBackgroundColor: theme.colorFillTertiary,
|
||||
headerTextColor: theme.colorTextHeading,
|
||||
|
||||
// Cell and row styling
|
||||
oddRowBackgroundColor: oddRowBg,
|
||||
rowHoverColor: theme.colorFillSecondary,
|
||||
selectedRowBackgroundColor: theme.colorPrimaryBg,
|
||||
cellTextColor: theme.colorText,
|
||||
|
||||
// Borders
|
||||
borderColor: theme.colorSplit,
|
||||
columnBorderColor: theme.colorSplit,
|
||||
|
||||
// Interactive elements
|
||||
accentColor: theme.colorPrimary,
|
||||
rangeSelectionBorderColor: theme.colorPrimary,
|
||||
rangeSelectionBackgroundColor: theme.colorPrimaryBg,
|
||||
|
||||
// Input fields (for filters)
|
||||
inputBackgroundColor: theme.colorBgContainer,
|
||||
inputBorderColor: theme.colorSplit,
|
||||
inputTextColor: theme.colorText,
|
||||
inputPlaceholderTextColor: theme.colorTextPlaceholder,
|
||||
|
||||
// Typography
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSizeSM,
|
||||
|
||||
// Spacing
|
||||
spacing: theme.sizeUnit,
|
||||
};
|
||||
|
||||
// Only apply params if we have a valid theme
|
||||
if (!theme || !theme.colorBgBase) {
|
||||
return baseTheme;
|
||||
}
|
||||
|
||||
// Merge theme overrides if provided
|
||||
const finalParams = themeOverrides
|
||||
? { ...params, ...themeOverrides }
|
||||
: params;
|
||||
|
||||
return baseTheme.withParams(finalParams);
|
||||
}, [theme, isDarkMode, themeOverrides]);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.ag-cell {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
`}
|
||||
data-themed-ag-grid="true"
|
||||
>
|
||||
<AgGridReact ref={ref} theme={agGridTheme} {...props} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Re-export commonly used types for convenience
|
||||
export type { CustomCellRendererProps } from 'ag-grid-react';
|
||||
|
||||
// Re-export commonly used ag-grid-community types
|
||||
export type {
|
||||
ColDef,
|
||||
Column,
|
||||
GridOptions,
|
||||
GridState,
|
||||
GridReadyEvent,
|
||||
CellClickedEvent,
|
||||
CellClassParams,
|
||||
IMenuActionParams,
|
||||
IHeaderParams,
|
||||
SortModelItem,
|
||||
ValueFormatterParams,
|
||||
ValueGetterParams,
|
||||
} from 'ag-grid-community';
|
||||
|
||||
// Re-export modules and themes commonly used with ThemedAgGridReact
|
||||
export {
|
||||
AllCommunityModule,
|
||||
ClientSideRowModelModule,
|
||||
ModuleRegistry,
|
||||
themeQuartz,
|
||||
colorSchemeDark,
|
||||
colorSchemeLight,
|
||||
} from 'ag-grid-community';
|
||||
|
||||
// Re-export AgGridReact for ref types
|
||||
export { AgGridReact } from 'ag-grid-react';
|
||||
|
||||
// Export the setup function for AG-Grid modules
|
||||
export { setupAGGridModules } from './setupAGGridModules';
|
||||
@@ -76,7 +76,6 @@ export { CronPicker, type CronError } from './CronPicker';
|
||||
export * from './DatePicker';
|
||||
export { DeleteModal, type DeleteModalProps } from './DeleteModal';
|
||||
export { Divider, type DividerProps } from './Divider';
|
||||
export { Drawer, type DrawerProps } from './Drawer';
|
||||
export {
|
||||
Dropdown,
|
||||
MenuDotsDropdown,
|
||||
@@ -166,11 +165,7 @@ export * from './Table';
|
||||
export * from './TableView';
|
||||
export * from './Tag';
|
||||
export * from './TelemetryPixel';
|
||||
export * from './ThemeSubMenu';
|
||||
export * from './UnsavedChangesModal';
|
||||
export * from './constants';
|
||||
export * from './Result';
|
||||
export {
|
||||
ThemedAgGridReact,
|
||||
type ThemedAgGridReactProps,
|
||||
setupAGGridModules,
|
||||
} from './ThemedAgGridReact';
|
||||
|
||||
@@ -115,15 +115,11 @@ export default class SupersetClientClass {
|
||||
return this.getCSRFToken();
|
||||
}
|
||||
|
||||
async postForm(
|
||||
endpoint: string,
|
||||
payload: Record<string, any>,
|
||||
target = '_blank',
|
||||
) {
|
||||
if (endpoint) {
|
||||
async postForm(url: string, payload: Record<string, any>, target = '_blank') {
|
||||
if (url) {
|
||||
await this.ensureAuth();
|
||||
const hiddenForm = document.createElement('form');
|
||||
hiddenForm.action = this.getUrl({ endpoint });
|
||||
hiddenForm.action = url;
|
||||
hiddenForm.method = 'POST';
|
||||
hiddenForm.target = target;
|
||||
const payloadWithToken: Record<string, any> = {
|
||||
|
||||
@@ -204,8 +204,6 @@ export interface SqlaFormData extends BaseFormData {
|
||||
|
||||
export type QueryFormData = SqlaFormData;
|
||||
|
||||
export type LatestQueryFormData = Partial<QueryFormData>;
|
||||
|
||||
//---------------------------------------------------
|
||||
// Type guards
|
||||
//---------------------------------------------------
|
||||
|
||||
@@ -57,5 +57,206 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
},
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
},
|
||||
claudette: {
|
||||
algorithm: 'dark',
|
||||
token: {
|
||||
colorPrimary: '#C15F3C',
|
||||
colorPrimaryHover: '#d16b48',
|
||||
colorPrimaryActive: '#a84f30',
|
||||
colorBgBase: '#1a1a1a',
|
||||
colorBgContainer: '#2a2a2a',
|
||||
colorBgElevated: '#323232',
|
||||
colorBgLayout: '#0f0f0f',
|
||||
colorBgSpotlight: '#323232',
|
||||
colorText: '#F4F3EE',
|
||||
colorTextSecondary: '#B1ADA1',
|
||||
colorTextTertiary: '#8a8680',
|
||||
colorTextQuaternary: '#6b6862',
|
||||
colorTextPlaceholder: '#B1ADA1',
|
||||
colorTextLightSolid: '#F4F3EE',
|
||||
colorBorder: '#404040',
|
||||
colorBorderSecondary: '#303030',
|
||||
colorFill: '#404040',
|
||||
colorFillSecondary: '#353535',
|
||||
colorFillTertiary: '#2a2a2a',
|
||||
colorFillQuaternary: '#1f1f1f',
|
||||
colorFillAlter: '#323232',
|
||||
colorIcon: '#B1ADA1',
|
||||
colorIconHover: '#F4F3EE',
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu'",
|
||||
fontFamilyCode:
|
||||
"'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||
fontSize: 14,
|
||||
borderRadius: 6,
|
||||
borderRadiusLG: 8,
|
||||
wireframe: false,
|
||||
},
|
||||
},
|
||||
figmate: {
|
||||
algorithm: 'light',
|
||||
token: {
|
||||
colorPrimary: '#0d99ff',
|
||||
colorPrimaryHover: '#3dadff',
|
||||
colorPrimaryActive: '#0085e6',
|
||||
colorInfo: '#4c74f4',
|
||||
colorInfoHover: '#6b8af5',
|
||||
colorInfoActive: '#2d5af2',
|
||||
colorSuccess: '#00d2aa',
|
||||
colorSuccessHover: '#1adbba',
|
||||
colorSuccessActive: '#00c299',
|
||||
colorWarning: '#ffad33',
|
||||
colorWarningHover: '#ffbf5c',
|
||||
colorWarningActive: '#ff9900',
|
||||
colorError: '#ff5757',
|
||||
colorErrorHover: '#ff7a7a',
|
||||
colorErrorActive: '#ff3333',
|
||||
colorBgBase: '#ffffff',
|
||||
colorBgContainer: '#fafafa',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f5f5f5',
|
||||
colorText: '#1a1a1a',
|
||||
colorTextSecondary: '#666666',
|
||||
colorTextTertiary: '#999999',
|
||||
colorTextQuaternary: '#cccccc',
|
||||
colorTextPlaceholder: '#999999',
|
||||
colorBorder: '#e6e6e6',
|
||||
colorBorderSecondary: '#f0f0f0',
|
||||
colorFill: '#f0f0f0',
|
||||
colorFillSecondary: '#f5f5f5',
|
||||
colorFillTertiary: '#fafafa',
|
||||
colorFillQuaternary: '#ffffff',
|
||||
colorIcon: '#666666',
|
||||
colorIconHover: '#333333',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 6,
|
||||
lineHeight: 1.5,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
hubert: {
|
||||
algorithm: 'dark',
|
||||
token: {
|
||||
colorPrimary: '#276EF1',
|
||||
colorPrimaryHover: '#4285f4',
|
||||
colorPrimaryActive: '#1557d6',
|
||||
colorInfo: '#276EF1',
|
||||
colorInfoHover: '#4285f4',
|
||||
colorInfoActive: '#1557d6',
|
||||
colorSuccess: '#3AA76D',
|
||||
colorSuccessHover: '#4db87d',
|
||||
colorSuccessActive: '#2d9657',
|
||||
colorWarning: '#FFC043',
|
||||
colorWarningHover: '#ffcd66',
|
||||
colorWarningActive: '#e6ac26',
|
||||
colorError: '#D44333',
|
||||
colorErrorHover: '#dd5a4c',
|
||||
colorErrorActive: '#bf3526',
|
||||
colorBgBase: '#000000',
|
||||
colorBgContainer: '#1a1a1a',
|
||||
colorBgElevated: '#2a2a2a',
|
||||
colorBgLayout: '#000000',
|
||||
colorBgSpotlight: '#333333',
|
||||
colorText: '#ffffff',
|
||||
colorTextSecondary: '#cccccc',
|
||||
colorTextTertiary: '#999999',
|
||||
colorTextQuaternary: '#666666',
|
||||
colorTextPlaceholder: '#999999',
|
||||
colorTextLightSolid: '#ffffff',
|
||||
colorBorder: '#333333',
|
||||
colorBorderSecondary: '#1a1a1a',
|
||||
colorFill: '#1a1a1a',
|
||||
colorFillSecondary: '#2a2a2a',
|
||||
colorFillTertiary: '#333333',
|
||||
colorFillQuaternary: '#404040',
|
||||
colorFillAlter: '#2a2a2a',
|
||||
colorIcon: '#cccccc',
|
||||
colorIconHover: '#ffffff',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
fontWeightStrong: 600,
|
||||
borderRadius: 4,
|
||||
borderRadiusLG: 6,
|
||||
borderRadiusSM: 2,
|
||||
lineHeight: 1.4,
|
||||
lineWidthBold: 2,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
bnb: {
|
||||
algorithm: 'light',
|
||||
token: {
|
||||
colorPrimary: '#FF5A5F',
|
||||
colorPrimaryHover: '#FF7479',
|
||||
colorPrimaryActive: '#E5464B',
|
||||
colorInfo: '#29696B',
|
||||
colorInfoHover: '#3D7D7F',
|
||||
colorInfoActive: '#1F5557',
|
||||
colorSuccess: '#5BCACE',
|
||||
colorSuccessHover: '#7DD4D8',
|
||||
colorSuccessActive: '#49B6BA',
|
||||
colorWarning: '#F4B02A',
|
||||
colorWarningHover: '#F6C054',
|
||||
colorWarningActive: '#E2A01E',
|
||||
colorError: '#C32F0E',
|
||||
colorErrorHover: '#D54428',
|
||||
colorErrorActive: '#A8280C',
|
||||
colorBgBase: '#ffffff',
|
||||
colorBgContainer: '#fafafa',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f7f7f7',
|
||||
colorText: '#222222',
|
||||
colorTextSecondary: '#484848',
|
||||
colorTextTertiary: '#767676',
|
||||
colorTextQuaternary: '#b0b0b0',
|
||||
colorTextPlaceholder: '#767676',
|
||||
colorBorder: '#e8e8e8',
|
||||
colorBorderSecondary: '#f0f0f0',
|
||||
colorFill: '#f7f7f7',
|
||||
colorFillSecondary: '#fafafa',
|
||||
colorFillTertiary: '#fcfcfc',
|
||||
colorFillQuaternary: '#ffffff',
|
||||
colorIcon: '#767676',
|
||||
colorIconHover: '#484848',
|
||||
fontFamily:
|
||||
'Circular, Circular Air Pro, Airbnb Cereal, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
fontWeightStrong: 600,
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 4,
|
||||
lineHeight: 1.5,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
};
|
||||
export default exampleThemes;
|
||||
|
||||
@@ -431,9 +431,14 @@ export interface ThemeContextType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for complete theme setup including default and dark themes
|
||||
* Configuration object for complete theme setup including default, dark themes and settings
|
||||
*/
|
||||
export interface SupersetThemeConfig {
|
||||
theme_default: AnyThemeConfig;
|
||||
theme_dark?: AnyThemeConfig;
|
||||
theme_settings?: {
|
||||
enforced?: boolean;
|
||||
allowSwitching?: boolean;
|
||||
allowOSPreference?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useTheme as useEmotionTheme } from '@emotion/react';
|
||||
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
|
||||
|
||||
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
|
||||
@@ -112,12 +111,3 @@ export function getColorVariants(
|
||||
export function isThemeDark(theme: SupersetTheme): boolean {
|
||||
return tinycolor(theme.colorBgContainer).isDark();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to determine if the current theme is dark mode
|
||||
* @returns true if theme is dark, false if light
|
||||
*/
|
||||
export function useThemeMode(): boolean {
|
||||
const theme = useEmotionTheme() as SupersetTheme;
|
||||
return isThemeDark(theme);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
*/
|
||||
import rison from 'rison';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { SupersetClient } from '../connection';
|
||||
import { getClientErrorObject } from '../query';
|
||||
import { ensureIsArray } from '../utils';
|
||||
import {
|
||||
SupersetClient,
|
||||
getClientErrorObject,
|
||||
ensureIsArray,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export const SEPARATOR = ' : ';
|
||||
|
||||
|
||||
@@ -23,13 +23,8 @@ import {
|
||||
ComponentType,
|
||||
} from 'react';
|
||||
import type { Editor } from 'brace';
|
||||
import type { QueryData } from '../chart/types/QueryResponse';
|
||||
import type {
|
||||
BaseFormData,
|
||||
LatestQueryFormData,
|
||||
QueryFormData,
|
||||
} from '../query';
|
||||
import type { JsonResponse } from '../connection';
|
||||
import { BaseFormData } from '../query';
|
||||
import { JsonResponse } from '../connection';
|
||||
|
||||
/**
|
||||
* A function which returns text (or marked-up text)
|
||||
@@ -224,19 +219,6 @@ export interface DateFilterControlProps {
|
||||
isOverflowingFilterBar?: boolean;
|
||||
}
|
||||
|
||||
export interface ExploreChartHeaderProps {
|
||||
chartId: number;
|
||||
queriesResponse: QueryData[] | null;
|
||||
sliceFormData: QueryFormData | null;
|
||||
queryFormData: QueryFormData;
|
||||
lastRendered: number;
|
||||
latestQueryFormData: LatestQueryFormData;
|
||||
chartUpdateEndTime: number | null;
|
||||
chartUpdateStartTime: number;
|
||||
queryController: AbortController | null;
|
||||
triggerQuery: boolean;
|
||||
}
|
||||
|
||||
export type Extensions = Partial<{
|
||||
'alertsreports.header.icon': ComponentType;
|
||||
'load.drillby.options': LoadDrillByOptions;
|
||||
@@ -269,5 +251,4 @@ export type Extensions = Partial<{
|
||||
ComponentType<SQLTablePreviewExtensionProps>,
|
||||
][];
|
||||
'filter.dateFilterControl': ComponentType<DateFilterControlProps>;
|
||||
'explore.chart.header': ComponentType<ExploreChartHeaderProps>;
|
||||
}>;
|
||||
|
||||
@@ -658,36 +658,26 @@ describe('SupersetClientClass', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each(['', '/prefix'])(
|
||||
"makes postForm request when appRoot is '%s'",
|
||||
async appRoot => {
|
||||
if (appRoot !== '') {
|
||||
client = new SupersetClientClass({ protocol, host, appRoot });
|
||||
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
|
||||
await client.init();
|
||||
}
|
||||
await client.postForm(mockPostFormEndpoint, {});
|
||||
it('makes postForm request', async () => {
|
||||
await client.postForm(mockPostFormUrl, {});
|
||||
|
||||
const hiddenForm = createElement.mock.results[0].value;
|
||||
const csrfTokenInput = createElement.mock.results[1].value;
|
||||
const hiddenForm = createElement.mock.results[0].value;
|
||||
const csrfTokenInput = createElement.mock.results[1].value;
|
||||
|
||||
expect(createElement.mock.calls).toHaveLength(2);
|
||||
expect(createElement.mock.calls).toHaveLength(2);
|
||||
|
||||
expect(hiddenForm.action).toBe(
|
||||
`${protocol}//${host}${appRoot}${mockPostFormEndpoint}`,
|
||||
);
|
||||
expect(hiddenForm.method).toBe('POST');
|
||||
expect(hiddenForm.target).toBe('_blank');
|
||||
expect(hiddenForm.action).toBe(mockPostFormUrl);
|
||||
expect(hiddenForm.method).toBe('POST');
|
||||
expect(hiddenForm.target).toBe('_blank');
|
||||
|
||||
expect(csrfTokenInput.type).toBe('hidden');
|
||||
expect(csrfTokenInput.name).toBe('csrf_token');
|
||||
expect(csrfTokenInput.value).toBe(1234);
|
||||
expect(csrfTokenInput.type).toBe('hidden');
|
||||
expect(csrfTokenInput.name).toBe('csrf_token');
|
||||
expect(csrfTokenInput.value).toBe(1234);
|
||||
|
||||
expect(appendChild.mock.calls).toHaveLength(1);
|
||||
expect(removeChild.mock.calls).toHaveLength(1);
|
||||
expect(authSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
expect(appendChild.mock.calls).toHaveLength(1);
|
||||
expect(removeChild.mock.calls).toHaveLength(1);
|
||||
expect(authSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('makes postForm request with guest token', async () => {
|
||||
client = new SupersetClientClass({ protocol, host, guestToken });
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.1"
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.14",
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/core": "^9.1.14",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.14",
|
||||
"@deck.gl/react": "^9.1.13",
|
||||
"@luma.gl/constants": "^9.1.9",
|
||||
"@luma.gl/core": "^9.1.9",
|
||||
"@luma.gl/engine": "^9.1.9",
|
||||
|
||||
@@ -89,7 +89,6 @@ export type CategoricalDeckGLContainerProps = {
|
||||
width: number;
|
||||
viewport: Viewport;
|
||||
getLayer: GetLayerType<unknown>;
|
||||
getHighlightLayer?: GetLayerType<unknown>;
|
||||
payload: JsonObject;
|
||||
onAddFilter?: HandlerFunction;
|
||||
setControlValue: (control: string, value: JsonValue) => void;
|
||||
@@ -214,7 +213,6 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
const getLayers = useCallback(() => {
|
||||
const {
|
||||
getLayer,
|
||||
getHighlightLayer,
|
||||
payload,
|
||||
formData: fd,
|
||||
onAddFilter,
|
||||
@@ -246,27 +244,19 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
data: { ...payload.data, features },
|
||||
};
|
||||
|
||||
const layerProps = {
|
||||
formData: fd,
|
||||
payload: filteredPayload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
datasource: props.datasource,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
};
|
||||
|
||||
const layer = getLayer(layerProps) as Layer;
|
||||
|
||||
if (emitCrossFilters && filterState?.value && getHighlightLayer) {
|
||||
const highlightLayer = getHighlightLayer(layerProps) as Layer;
|
||||
|
||||
return [layer, highlightLayer];
|
||||
}
|
||||
|
||||
return [layer];
|
||||
return [
|
||||
getLayer({
|
||||
formData: fd,
|
||||
payload: filteredPayload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
datasource: props.datasource,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}) as Layer,
|
||||
];
|
||||
}, [addColor, categories, props, setTooltip]);
|
||||
|
||||
const toggleCategory = useCallback(
|
||||
|
||||
@@ -23,11 +23,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
AdhocFilter,
|
||||
ContextMenuFilters,
|
||||
DataMask,
|
||||
Datasource,
|
||||
ensureIsArray,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
isDefined,
|
||||
JsonObject,
|
||||
@@ -68,15 +65,7 @@ export type DeckMultiProps = {
|
||||
height: number;
|
||||
width: number;
|
||||
datasource: Datasource;
|
||||
setDataMask?: (dataMask: DataMask) => void;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
onSelect: () => void;
|
||||
filterState?: FilterState;
|
||||
emitCrossFilters?: boolean;
|
||||
};
|
||||
|
||||
const DeckMulti = (props: DeckMultiProps) => {
|
||||
@@ -186,14 +175,16 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
const createLayerFromData = useCallback(
|
||||
(subslice: JsonObject, json: JsonObject): Layer =>
|
||||
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
|
||||
layerGenerators[subslice.form_data.viz_type]({
|
||||
formData: subslice.form_data,
|
||||
payload: json,
|
||||
layerGenerators[subslice.form_data.viz_type](
|
||||
subslice.form_data,
|
||||
json,
|
||||
props.onAddFilter,
|
||||
setTooltip,
|
||||
datasource: props.datasource,
|
||||
onSelect: props.onSelect,
|
||||
}),
|
||||
[props.onSelect, props.datasource, setTooltip],
|
||||
props.datasource,
|
||||
[],
|
||||
props.onSelect,
|
||||
),
|
||||
[props.onAddFilter, props.onSelect, props.datasource, setTooltip],
|
||||
);
|
||||
|
||||
const loadSingleLayer = useCallback(
|
||||
|
||||
@@ -87,7 +87,6 @@ interface GetPointsType {
|
||||
export function createDeckGLComponent(
|
||||
getLayer: GetLayerType<unknown>,
|
||||
getPoints: GetPointsType,
|
||||
getHighlightLayer?: GetLayerType<unknown>,
|
||||
) {
|
||||
// Higher order component
|
||||
return memo((props: DeckGLComponentProps) => {
|
||||
@@ -119,7 +118,7 @@ export function createDeckGLComponent(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const computeLayers = useCallback(
|
||||
const computeLayer = useCallback(
|
||||
(props: DeckGLComponentProps) => {
|
||||
const {
|
||||
formData,
|
||||
@@ -131,7 +130,7 @@ export function createDeckGLComponent(
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
const layerProps = {
|
||||
return getLayer({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
@@ -140,17 +139,7 @@ export function createDeckGLComponent(
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
};
|
||||
|
||||
const layer = getLayer(layerProps) as Layer;
|
||||
|
||||
if (emitCrossFilters && filterState?.value && getHighlightLayer) {
|
||||
const highlightLayer = getHighlightLayer(layerProps) as Layer;
|
||||
|
||||
return [layer, highlightLayer];
|
||||
}
|
||||
|
||||
return [layer];
|
||||
}) as Layer;
|
||||
},
|
||||
[setTooltip],
|
||||
);
|
||||
@@ -163,7 +152,7 @@ export function createDeckGLComponent(
|
||||
setCategories(categories);
|
||||
}, [props]);
|
||||
|
||||
const [layers, setLayers] = useState(computeLayers(props));
|
||||
const [layer, setLayer] = useState(computeLayer(props));
|
||||
|
||||
useEffect(() => {
|
||||
// Only recompute the layer if anything BUT the viewport has changed
|
||||
@@ -178,9 +167,9 @@ export function createDeckGLComponent(
|
||||
viewport: null,
|
||||
};
|
||||
if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
|
||||
setLayers(computeLayers(props));
|
||||
setLayer(computeLayer(props));
|
||||
}
|
||||
}, [computeLayers, prevFormData, prevFilterState, prevPayload, props]);
|
||||
}, [computeLayer, prevFormData, prevFilterState, prevPayload, props]);
|
||||
|
||||
const { formData, payload, setControlValue, height, width } = props;
|
||||
|
||||
@@ -190,7 +179,7 @@ export function createDeckGLComponent(
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
layers={[layer]}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
width={width}
|
||||
@@ -211,7 +200,6 @@ export function createDeckGLComponent(
|
||||
export function createCategoricalDeckGLComponent(
|
||||
getLayer: GetLayerType<Layer>,
|
||||
getPoints: GetPointsType,
|
||||
getHighlightLayer?: GetLayerType<Layer>,
|
||||
) {
|
||||
return function Component(props: DeckGLComponentProps) {
|
||||
const {
|
||||
@@ -236,7 +224,6 @@ export function createCategoricalDeckGLComponent(
|
||||
setControlValue={setControlValue}
|
||||
viewport={viewport}
|
||||
getLayer={getLayer}
|
||||
getHighlightLayer={getHighlightLayer}
|
||||
payload={payload}
|
||||
getPoints={getPoints}
|
||||
width={width}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { commonLayerProps } from '../common';
|
||||
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { Point } from '../../types';
|
||||
import { HIGHLIGHT_COLOR_ARRAY, TRANSPARENT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
const points: Point[] = [];
|
||||
@@ -74,7 +73,7 @@ export const getLayer: GetLayerType<ArcLayer> = function ({
|
||||
|
||||
return new ArcLayer({
|
||||
data,
|
||||
getSourceColor: (d: JsonObject) => {
|
||||
getSourceColor: (d: any) => {
|
||||
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
|
||||
return [sc.r, sc.g, sc.b, 255 * sc.a];
|
||||
}
|
||||
@@ -99,50 +98,7 @@ export const getLayer: GetLayerType<ArcLayer> = function ({
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
opacity: filterState?.value ? 0.1 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
export const getHighlightLayer: GetLayerType<ArcLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const data = payload.data.features;
|
||||
|
||||
const getColor = (d: {
|
||||
sourcePosition: [number, number];
|
||||
targetPosition: [number, number];
|
||||
}) => {
|
||||
const sourcePosition = filterState?.value[0];
|
||||
const targetPosition = filterState?.value[1];
|
||||
|
||||
if (
|
||||
sourcePosition &&
|
||||
targetPosition &&
|
||||
d.sourcePosition[0] === sourcePosition[0] &&
|
||||
d.sourcePosition[1] === sourcePosition[1] &&
|
||||
d.targetPosition[0] === targetPosition[0] &&
|
||||
d.targetPosition[1] === targetPosition[1]
|
||||
) {
|
||||
return HIGHLIGHT_COLOR_ARRAY;
|
||||
}
|
||||
|
||||
return TRANSPARENT_COLOR_ARRAY;
|
||||
};
|
||||
|
||||
return new ArcLayer({
|
||||
data,
|
||||
getSourceColor: getColor,
|
||||
getTargetColor: getColor,
|
||||
id: `path-hihglight-layer-${fd.slice_id}` as const,
|
||||
getWidth: fd.stroke_width ? fd.stroke_width : 3,
|
||||
});
|
||||
};
|
||||
|
||||
export default createCategoricalDeckGLComponent(
|
||||
getLayer,
|
||||
getPoints,
|
||||
getHighlightLayer,
|
||||
);
|
||||
export default createCategoricalDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ContourLayer } from '@deck.gl/aggregation-layers';
|
||||
import { PolygonLayer } from '@deck.gl/layers';
|
||||
import { Position } from '@deck.gl/core';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
@@ -25,7 +24,6 @@ import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
function setTooltipContent(o: any) {
|
||||
return (
|
||||
@@ -114,56 +112,4 @@ export function getPoints(data: any[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
export const getHighlightLayer: GetLayerType<PolygonLayer> = function ({
|
||||
formData,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
|
||||
const fromLonLat = filterState?.value[0];
|
||||
const toLonLat = filterState?.value[1];
|
||||
|
||||
const minLon = fromLonLat[0];
|
||||
const maxLon = toLonLat[0];
|
||||
const minLat = fromLonLat[1];
|
||||
const maxLat = toLonLat[1];
|
||||
|
||||
const boxPolygon = [
|
||||
[minLon, minLat],
|
||||
[maxLon, minLat],
|
||||
[maxLon, maxLat],
|
||||
[minLon, maxLat],
|
||||
[minLon, minLat],
|
||||
];
|
||||
|
||||
return new PolygonLayer({
|
||||
id: `contour-highlight-layer-${fd.slice_id}`,
|
||||
data: [{ polygon: boxPolygon }],
|
||||
getPolygon: (d: any) => d.polygon,
|
||||
getFillColor: [
|
||||
HIGHLIGHT_COLOR_ARRAY[0],
|
||||
HIGHLIGHT_COLOR_ARRAY[1],
|
||||
HIGHLIGHT_COLOR_ARRAY[2],
|
||||
100,
|
||||
],
|
||||
getLineColor: HIGHLIGHT_COLOR_ARRAY,
|
||||
getLineWidth: 4,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -43,7 +43,6 @@ import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
import { GetLayerType } from '../../factory';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -120,21 +119,7 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
||||
if (filterStateValue) {
|
||||
if (
|
||||
JSON.stringify(feature.geometry.coordinates) ===
|
||||
JSON.stringify(filterStateValue?.[0])
|
||||
) {
|
||||
return HIGHLIGHT_COLOR_ARRAY;
|
||||
}
|
||||
|
||||
const fillColor = feature?.properties?.fillColor;
|
||||
fillColor[3] = 125;
|
||||
return fillColor;
|
||||
}
|
||||
return feature?.properties?.fillColor;
|
||||
};
|
||||
const getFillColor = (feature: JsonObject) => feature?.properties?.fillColor;
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
@@ -175,8 +160,7 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
extruded: fd.extruded,
|
||||
filled: fd.filled,
|
||||
stroked: fd.stroked,
|
||||
getFillColor: (feature: JsonObject) =>
|
||||
getFillColor(feature, filterState?.value),
|
||||
getFillColor,
|
||||
getLineColor,
|
||||
getLineWidth: fd.line_width || 1,
|
||||
pointRadiusScale: fd.point_radius_scale,
|
||||
@@ -204,7 +188,6 @@ export type DeckGLGeoJsonProps = {
|
||||
filterState: FilterState;
|
||||
onContextMenu: HandlerFunction;
|
||||
setDataMask: SetDataMaskHook;
|
||||
emitCrossFilters?: boolean;
|
||||
};
|
||||
|
||||
export function getPoints(data: Point[]) {
|
||||
@@ -259,7 +242,6 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
onAddFilter,
|
||||
payload,
|
||||
formData,
|
||||
emitCrossFilters: props.emitCrossFilters,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,6 @@ import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { HIGHLIGHT_COLOR_ARRAY, TRANSPARENT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -87,7 +86,7 @@ export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
: aggFunc;
|
||||
|
||||
return new GridLayer({
|
||||
id: `grid-layer-${fd.slice_id}-${JSON.stringify(colorBreakpoints)}`,
|
||||
id: `grid-layer-${fd.slice_id}-${JSON.stringify(colorBreakpoints)}` as const,
|
||||
data,
|
||||
cellSize: fd.grid_size,
|
||||
extruded: fd.extruded,
|
||||
@@ -110,7 +109,6 @@ export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
opacity: filterState?.value ? 0.1 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -118,43 +116,4 @@ export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
export const getHighlightLayer: GetLayerType<GridLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
const selectedPointsSet = new Set(
|
||||
filterState?.value?.map((sp: [number, number]) => `${sp[0]},${sp[1]}`),
|
||||
);
|
||||
|
||||
const colorAggFunc = (p: JsonObject) =>
|
||||
selectedPointsSet.has(`${p.position[0]},${p.position[1]}`) ? 1 : 0;
|
||||
|
||||
return new GridLayer({
|
||||
id: `grid-highlight-layer-${fd.slice_id}-${JSON.stringify(filterState?.value)}`,
|
||||
data,
|
||||
cellSize: fd.grid_size,
|
||||
extruded: fd.extruded,
|
||||
colorDomain: [0, 1],
|
||||
colorRange: [TRANSPARENT_COLOR_ARRAY, HIGHLIGHT_COLOR_ARRAY],
|
||||
colorAggregation: 'MAX',
|
||||
outline: false,
|
||||
// @ts-ignore
|
||||
getElevationValue: aggFunc,
|
||||
getColorWeight: colorAggFunc,
|
||||
opacity: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
import { HeatmapLayer } from '@deck.gl/aggregation-layers';
|
||||
import { Position } from '@deck.gl/core';
|
||||
import { t, getSequentialSchemeRegistry, JsonObject } from '@superset-ui/core';
|
||||
import { isPointInBonds } from '../../utilities/utils';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -100,49 +98,4 @@ export function getPoints(data: any[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
export const getHighlightLayer: GetLayerType<HeatmapLayer> = ({
|
||||
formData,
|
||||
filterState,
|
||||
payload,
|
||||
}) => {
|
||||
const fd = formData;
|
||||
const {
|
||||
intensity = 1,
|
||||
radius_pixels: radiusPixels = 30,
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (jsFnMutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
|
||||
const dataInside = data.filter((d: JsonObject) =>
|
||||
isPointInBonds(d.position, filterState?.value),
|
||||
);
|
||||
|
||||
return new HeatmapLayer({
|
||||
id: `heatmap-layer-${fd.slice_id}` as const,
|
||||
data: dataInside,
|
||||
intensity,
|
||||
radiusPixels,
|
||||
colorRange: [
|
||||
[
|
||||
HIGHLIGHT_COLOR_ARRAY[0],
|
||||
HIGHLIGHT_COLOR_ARRAY[1],
|
||||
HIGHLIGHT_COLOR_ARRAY[2],
|
||||
55,
|
||||
],
|
||||
HIGHLIGHT_COLOR_ARRAY,
|
||||
],
|
||||
aggregation: aggregation.toUpperCase(),
|
||||
getPosition: (d: { position: Position; weight: number }) => d.position,
|
||||
getWeight: (d: { position: number[]; weight: number }) =>
|
||||
d.weight ? d.weight : 1,
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { HIGHLIGHT_COLOR_ARRAY, TRANSPARENT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -109,7 +108,6 @@ export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
opacity: filterState?.value ? 0.3 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -117,43 +115,4 @@ export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
export const getHighlightLayer: GetLayerType<HexagonLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
const selectedPointsSet = new Set(
|
||||
filterState?.value?.map((sp: [number, number]) => `${sp[0]},${sp[1]}`),
|
||||
);
|
||||
|
||||
const colorAggFunc = (p: JsonObject) =>
|
||||
selectedPointsSet.has(`${p.position[0]},${p.position[1]}`) ? 1 : 0;
|
||||
|
||||
return new HexagonLayer({
|
||||
id: `hex-highlight-layer-${fd.slice_id}-${JSON.stringify(filterState?.value)}`,
|
||||
data,
|
||||
radius: fd.grid_size,
|
||||
extruded: fd.extruded,
|
||||
colorDomain: [0, 1],
|
||||
colorRange: [TRANSPARENT_COLOR_ARRAY, HIGHLIGHT_COLOR_ARRAY],
|
||||
colorAggregation: 'MAX',
|
||||
outline: false,
|
||||
// @ts-ignore
|
||||
getElevationValue: aggFunc,
|
||||
getColorWeight: colorAggFunc,
|
||||
opacity: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -24,7 +24,6 @@ import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { Point } from '../../types';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -84,7 +83,6 @@ export const getLayer: GetLayerType<PathLayer> = function ({
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
opacity: filterState?.value ? 0.3 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,40 +95,4 @@ export function getPoints(data: JsonObject[]) {
|
||||
return points;
|
||||
}
|
||||
|
||||
export const getHighlightLayer: GetLayerType<PathLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const fixedColor = HIGHLIGHT_COLOR_ARRAY;
|
||||
let data = payload.data.features.map((feature: JsonObject) => ({
|
||||
...feature,
|
||||
path: feature.path,
|
||||
width: fd.line_width,
|
||||
color: fixedColor,
|
||||
}));
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const filteredData = data.filter(
|
||||
(d: JsonObject) =>
|
||||
JSON.stringify(d.path).replaceAll(' ', '') === filterState?.value[0],
|
||||
);
|
||||
|
||||
return new PathLayer({
|
||||
id: `path-highlight-layer-${fd.slice_id}` as const,
|
||||
getColor: () => HIGHLIGHT_COLOR_ARRAY,
|
||||
getPath: (d: any) => d.path,
|
||||
getWidth: (d: any) => d.width,
|
||||
data: filteredData,
|
||||
rounded: true,
|
||||
widthScale: 1,
|
||||
widthUnits: fd.line_width_unit,
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ContextMenuFilters,
|
||||
ensureIsArray,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
getBuckets,
|
||||
getBreakPointColorScaler,
|
||||
getColorBreakpointsBuckets,
|
||||
TRANSPARENT_COLOR_ARRAY,
|
||||
} from '../../utils';
|
||||
|
||||
import { commonLayerProps, getColorForBreakpoints } from '../common';
|
||||
@@ -57,7 +57,8 @@ import { TooltipProps } from '../../components/Tooltip';
|
||||
import { GetLayerType } from '../../factory';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { DEFAULT_DECKGL_COLOR } from '../../utilities/Shared_DeckGL';
|
||||
import { Point } from '../../types';
|
||||
|
||||
const DOUBLE_CLICK_THRESHOLD = 250; // milliseconds
|
||||
|
||||
function getElevation(
|
||||
d: JsonObject,
|
||||
@@ -109,6 +110,7 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
onSelect,
|
||||
selected,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData as PolygonFormData;
|
||||
@@ -179,20 +181,15 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
}
|
||||
|
||||
// when polygons are selected, reduce the opacity of non-selected polygons
|
||||
const colorScaler = (d: {
|
||||
polygon: Point[];
|
||||
}): [number, number, number, number] => {
|
||||
const baseColor =
|
||||
(baseColorScaler(d) as [number, number, number, number]) ||
|
||||
TRANSPARENT_COLOR_ARRAY;
|
||||
const polygonPoints = getPointsFromPolygon(d);
|
||||
|
||||
const isPolygonFilterSelected =
|
||||
JSON.stringify(polygonPoints).replaceAll(' ', '') ===
|
||||
filterState?.value?.[0];
|
||||
|
||||
if (filterState?.value && !isPolygonFilterSelected) {
|
||||
baseColor[3] /= 3;
|
||||
const colorScaler = (d: JsonObject): [number, number, number, number] => {
|
||||
const baseColor = (baseColorScaler(d) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
]) || [0, 0, 0, 0];
|
||||
if (!ensureIsArray(selected).includes(d[fd.line_column])) {
|
||||
baseColor[3] /= 2;
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
@@ -219,7 +216,6 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
getElevation: (d: JsonObject) => getElevation(d, colorScaler),
|
||||
elevationScale: fd.multiplier,
|
||||
fp64: true,
|
||||
opacity: fd.opacity ? fd.opacity / 100 : 1,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
@@ -280,14 +276,18 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
return viewport;
|
||||
}, [props]);
|
||||
|
||||
const [lastClick, setLastClick] = useState(0);
|
||||
const [viewport, setViewport] = useState(getAdjustedViewport());
|
||||
const [stateFormData, setStateFormData] = useState(props.payload.form_data);
|
||||
const [selected, setSelected] = useState<JsonObject[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const { payload } = props;
|
||||
|
||||
if (payload.form_data !== stateFormData) {
|
||||
setViewport(getAdjustedViewport());
|
||||
setSelected([]);
|
||||
setLastClick(0);
|
||||
setStateFormData(payload.form_data);
|
||||
}
|
||||
}, [getAdjustedViewport, props, stateFormData, viewport]);
|
||||
@@ -299,6 +299,37 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(polygon: JsonObject) => {
|
||||
const { formData, onAddFilter } = props;
|
||||
|
||||
const now = new Date().getDate();
|
||||
const doubleClick = now - lastClick <= DOUBLE_CLICK_THRESHOLD;
|
||||
|
||||
// toggle selected polygons
|
||||
const selectedCopy = [...selected];
|
||||
if (doubleClick) {
|
||||
selectedCopy.splice(0, selectedCopy.length, polygon);
|
||||
} else if (formData.toggle_polygons) {
|
||||
const i = selectedCopy.indexOf(polygon);
|
||||
if (i === -1) {
|
||||
selectedCopy.push(polygon);
|
||||
} else {
|
||||
selectedCopy.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
selectedCopy.splice(0, 1, polygon);
|
||||
}
|
||||
|
||||
setSelected(selectedCopy);
|
||||
setLastClick(now);
|
||||
if (formData.table_filter) {
|
||||
onAddFilter(formData.line_column, selected, false, true);
|
||||
}
|
||||
},
|
||||
[lastClick, props, selected],
|
||||
);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const {
|
||||
formData,
|
||||
@@ -319,6 +350,8 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
selected,
|
||||
onSelect,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
@@ -326,7 +359,7 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
});
|
||||
|
||||
return [layer];
|
||||
}, [setTooltip, props]);
|
||||
}, [onSelect, selected, setTooltip, props]);
|
||||
|
||||
const { payload, formData, setControlValue } = props;
|
||||
|
||||
|
||||
@@ -184,6 +184,32 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'table_filter',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Emit Filter Events'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t('Whether to apply filter when items are clicked'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'toggle_polygons',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Multiple filtering'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Allow sending multiple polygons as a filter event',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[legendPosition],
|
||||
[legendFormat],
|
||||
],
|
||||
|
||||
@@ -23,12 +23,10 @@ import {
|
||||
QueryFormData,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { isPointInBonds } from '../../utilities/utils';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { createCategoricalDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { unitToRadius } from '../../utils/geo';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
@@ -107,43 +105,7 @@ export const getLayer: GetLayerType<ScatterplotLayer> = function ({
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
opacity: filterState?.value ? 0.3 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
export const getHighlightLayer: GetLayerType<ScatterplotLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const dataWithRadius = payload.data.features.map((d: JsonObject) => {
|
||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||
if (fd.multiplier) {
|
||||
radius *= fd.multiplier;
|
||||
}
|
||||
|
||||
return { ...d, radius };
|
||||
});
|
||||
|
||||
const dataInside = dataWithRadius.filter((d: JsonObject) =>
|
||||
isPointInBonds(d.position, filterState?.value),
|
||||
);
|
||||
|
||||
return new ScatterplotLayer({
|
||||
id: `scatter-highlight-layer-${fd.slice_id}` as const,
|
||||
data: dataInside,
|
||||
fp64: true,
|
||||
getFillColor: () => HIGHLIGHT_COLOR_ARRAY,
|
||||
getRadius: (d: any) => d.radius,
|
||||
radiusMinPixels: Number(fd.min_radius) || undefined,
|
||||
radiusMaxPixels: Number(fd.max_radius) || undefined,
|
||||
stroked: false,
|
||||
});
|
||||
};
|
||||
|
||||
export default createCategoricalDeckGLComponent(
|
||||
getLayer,
|
||||
getPoints,
|
||||
getHighlightLayer,
|
||||
);
|
||||
export default createCategoricalDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -22,16 +22,11 @@
|
||||
import { ScreenGridLayer } from '@deck.gl/aggregation-layers';
|
||||
import { CategoricalColorNamespace, JsonObject, t } from '@superset-ui/core';
|
||||
import { Color } from '@deck.gl/core';
|
||||
import {
|
||||
COLOR_SCHEME_TYPES,
|
||||
ColorSchemeType,
|
||||
isPointInBonds,
|
||||
} from '../../utilities/utils';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../../utilities/utils';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { HIGHLIGHT_COLOR_ARRAY, TRANSPARENT_COLOR_ARRAY } from '../../utils';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
@@ -118,39 +113,7 @@ export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
}),
|
||||
getWeight: aggFunc,
|
||||
colorScaleType: colorSchemeType === 'default' ? 'linear' : 'quantize',
|
||||
opacity: filterState?.value ? 0.3 : 1,
|
||||
});
|
||||
};
|
||||
|
||||
const getHighlightLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
formData,
|
||||
filterState,
|
||||
payload,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const dataInside = data.filter((d: JsonObject) =>
|
||||
isPointInBonds(d.position, filterState?.value),
|
||||
);
|
||||
|
||||
const aggFunc = (d: JsonObject) => d.weight || 0;
|
||||
|
||||
return new ScreenGridLayer({
|
||||
id: `screengrid-highlight-layer-${formData.slice_id}` as const,
|
||||
data: dataInside,
|
||||
cellSizePixels: formData.grid_size,
|
||||
colorDomain: [0, 1],
|
||||
colorRange: [TRANSPARENT_COLOR_ARRAY, HIGHLIGHT_COLOR_ARRAY],
|
||||
outline: false,
|
||||
getWeight: aggFunc,
|
||||
colorScaleType: 'quantize',
|
||||
});
|
||||
};
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints, getHighlightLayer);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -116,8 +116,6 @@ export function commonLayerProps({
|
||||
drillBy: {},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,19 +38,3 @@ export const isColorSchemeTypeVisible = (
|
||||
controls: ControlStateMapping,
|
||||
colorSchemeType: ColorSchemeType,
|
||||
) => controls.color_scheme_type?.value === colorSchemeType;
|
||||
|
||||
export const isPointInBonds = (
|
||||
position: [number, number],
|
||||
area: [[number, number], [number, number]],
|
||||
) => {
|
||||
const [lon, lat] = position;
|
||||
const fromLonLat = area[0];
|
||||
const toLatLon = area[1];
|
||||
|
||||
return (
|
||||
lon >= fromLonLat[0] &&
|
||||
lon <= toLatLon[0] &&
|
||||
lat >= fromLonLat[1] &&
|
||||
lat <= toLatLon[1]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,9 +31,6 @@ import { BitmapLayer, PathLayer } from '@deck.gl/layers';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { ColorBreakpointType } from './types';
|
||||
|
||||
export const TRANSPARENT_COLOR_ARRAY = [0, 0, 0, 0] as Color;
|
||||
export const HIGHLIGHT_COLOR_ARRAY = [255, 0, 0, 255] as Color;
|
||||
|
||||
const DEFAULT_NUM_BUCKETS = 10;
|
||||
|
||||
export const MAPBOX_LAYER_PREFIX = 'mapbox://';
|
||||
@@ -124,7 +121,7 @@ export function getBreakPointColorScaler(
|
||||
: getSequentialSchemeRegistry().get(linearColorScheme);
|
||||
|
||||
if (!colorScheme) {
|
||||
return () => TRANSPARENT_COLOR_ARRAY;
|
||||
return () => [0, 0, 0, 0];
|
||||
}
|
||||
let scaler: ScaleLinear<string, string> | ScaleThreshold<number, string>;
|
||||
let maskPoint: (v: number | undefined) => boolean;
|
||||
@@ -163,7 +160,7 @@ export function getBreakPointColorScaler(
|
||||
return (d: JsonObject): Color => {
|
||||
const v = accessor(d);
|
||||
if (!v) {
|
||||
return TRANSPARENT_COLOR_ARRAY;
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
const c = hexToRGB(scaler(v));
|
||||
if (maskPoint(v)) {
|
||||
|
||||
@@ -101,21 +101,18 @@ describe('getCrossFilterDataMask', () => {
|
||||
filters: [
|
||||
{
|
||||
col: 'LON',
|
||||
op: 'IN',
|
||||
val: [-122.4205965, -122.4215375],
|
||||
op: '==',
|
||||
val: -122.4205965,
|
||||
},
|
||||
{
|
||||
col: 'LAT',
|
||||
op: 'IN',
|
||||
val: [37.8054735, 37.8058583],
|
||||
op: '==',
|
||||
val: 37.8054735,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [
|
||||
[-122.4205965, 37.8054735],
|
||||
[-122.4215375, 37.8058583],
|
||||
],
|
||||
value: [-122.4205965, 37.8054735],
|
||||
customColumnLabel: 'LON, LAT',
|
||||
},
|
||||
},
|
||||
@@ -160,12 +157,7 @@ describe('getCrossFilterDataMask', () => {
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: latlongFormData,
|
||||
data: latlongPickingData,
|
||||
filterState: {
|
||||
value: [
|
||||
[-122.4205965, 37.8054735],
|
||||
[-122.4215375, 37.8058583],
|
||||
],
|
||||
},
|
||||
filterState: { value: [-122.4205965, 37.8054735] },
|
||||
});
|
||||
|
||||
const expected = {
|
||||
@@ -221,13 +213,13 @@ describe('getCrossFilterDataMask', () => {
|
||||
filters: [
|
||||
{
|
||||
col: 'LONLAT',
|
||||
op: 'IN',
|
||||
val: [`-122.4205965,37.8054735`, `-122.4215375,37.8058583`],
|
||||
op: '==',
|
||||
val: `-122.4205965,37.8054735`,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [`-122.4205965,37.8054735`, `-122.4215375,37.8058583`],
|
||||
value: [`-122.4205965,37.8054735`],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
@@ -275,13 +267,13 @@ describe('getCrossFilterDataMask', () => {
|
||||
filters: [
|
||||
{
|
||||
col: 'LONLAT',
|
||||
op: 'IN',
|
||||
val: [`37.8054735,-122.4205965`, `37.8058583,-122.4215375`],
|
||||
op: '==',
|
||||
val: `37.8054735,-122.4205965`,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [`37.8054735,-122.4205965`, `37.8058583,-122.4215375`],
|
||||
value: [`37.8054735,-122.4205965`],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
@@ -324,8 +316,8 @@ describe('getCrossFilterDataMask', () => {
|
||||
filters: [
|
||||
{
|
||||
col: 'geohash',
|
||||
op: 'IN',
|
||||
val: [`9q8zn620c751`],
|
||||
op: '==',
|
||||
val: `9q8zn620c751`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -78,12 +78,10 @@ export interface ValidatedPickingData {
|
||||
|
||||
const getFiltersBySpatialType = ({
|
||||
position,
|
||||
positions,
|
||||
positionBounds,
|
||||
spatialData,
|
||||
}: {
|
||||
position?: [number, number];
|
||||
positions?: [number, number][];
|
||||
position: [number, number];
|
||||
spatialData: SpatialData;
|
||||
positionBounds?: PositionBounds;
|
||||
}) => {
|
||||
@@ -100,7 +98,7 @@ const getFiltersBySpatialType = ({
|
||||
let filters: QueryObjectFilterClause[] = [];
|
||||
let customColumnLabel;
|
||||
|
||||
if (!position && !positions && !positionBounds)
|
||||
if (!position && !positionBounds)
|
||||
throw new Error('Position of picked data is required');
|
||||
|
||||
switch (type) {
|
||||
@@ -108,23 +106,7 @@ const getFiltersBySpatialType = ({
|
||||
if (lonCol != null && latCol != null) {
|
||||
const cols = [lonCol, latCol];
|
||||
|
||||
if (positions && positions.length > 0) {
|
||||
values = positions;
|
||||
customColumnLabel = cols.join(', ');
|
||||
|
||||
filters = [
|
||||
{
|
||||
col: lonCol,
|
||||
op: 'IN',
|
||||
val: positions.map(pos => pos[0]),
|
||||
},
|
||||
{
|
||||
col: latCol,
|
||||
op: 'IN',
|
||||
val: positions.map(pos => pos[1]),
|
||||
},
|
||||
];
|
||||
} else if (position) {
|
||||
if (position) {
|
||||
values = position;
|
||||
customColumnLabel = cols.join(', ');
|
||||
|
||||
@@ -170,35 +152,19 @@ const getFiltersBySpatialType = ({
|
||||
|
||||
if (!col) throw new Error('Column is required');
|
||||
|
||||
if (positions && positions.length > 0) {
|
||||
const vals = positions.map(pos =>
|
||||
(reverseCheckbox ? [...pos].reverse() : pos).join(delimiter),
|
||||
);
|
||||
const val = (reverseCheckbox ? position.reverse() : position).join(
|
||||
delimiter,
|
||||
);
|
||||
|
||||
values = vals;
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: 'IN',
|
||||
val: vals,
|
||||
},
|
||||
];
|
||||
} else if (position) {
|
||||
const val = (reverseCheckbox ? position.reverse() : position).join(
|
||||
delimiter,
|
||||
);
|
||||
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
}
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -207,35 +173,18 @@ const getFiltersBySpatialType = ({
|
||||
|
||||
if (!col) throw new Error('Column is required');
|
||||
|
||||
if (positions && positions.length > 0) {
|
||||
const vals = positions.map(pos => {
|
||||
const [lon, lat] = pos;
|
||||
return ngeohash.encode(lat, lon, GEOHASH_PRECISION);
|
||||
});
|
||||
const [lon, lat] = position;
|
||||
const val = ngeohash.encode(lat, lon, GEOHASH_PRECISION);
|
||||
|
||||
values = vals;
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: 'IN',
|
||||
val: vals,
|
||||
},
|
||||
];
|
||||
} else if (position) {
|
||||
const [lon, lat] = position;
|
||||
const val = ngeohash.encode(lat, lon, GEOHASH_PRECISION);
|
||||
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
}
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -331,69 +280,31 @@ const getStartEndSpatialFilters = ({
|
||||
};
|
||||
};
|
||||
|
||||
const isPointInBounds = (
|
||||
point: [number, number],
|
||||
bounds: PositionBounds,
|
||||
): boolean =>
|
||||
point[0] >= bounds.from[0] &&
|
||||
point[0] <= bounds.to[0] &&
|
||||
point[1] >= bounds.from[1] &&
|
||||
point[1] <= bounds.to[1];
|
||||
|
||||
const getSpatialFilters = ({
|
||||
formData,
|
||||
data,
|
||||
filterState,
|
||||
}: {
|
||||
formData: LayerFormData;
|
||||
data: PickingInfo;
|
||||
filterState?: FilterState;
|
||||
}): FilterResult => {
|
||||
const positions = data.object?.points?.map(
|
||||
(point: { position: [number, number]; weight: number }) => point.position,
|
||||
) as [number, number][];
|
||||
const position = (data.object?.points?.[0]?.position ||
|
||||
data.object?.position) as [number, number];
|
||||
|
||||
let positionBounds: PositionBounds | undefined;
|
||||
|
||||
if (!positions && data.coordinate && data.viewport) {
|
||||
if (!position && data.coordinate && data.viewport) {
|
||||
const pickedPositionBounds = calculatePickedPositionBounds({
|
||||
pickedCoordinates: data.coordinate,
|
||||
viewport: data.viewport,
|
||||
});
|
||||
|
||||
positionBounds = pickedPositionBounds;
|
||||
|
||||
if (filterState?.value && data.coordinate) {
|
||||
const currentFilterValues = filterState.value;
|
||||
if (
|
||||
Array.isArray(currentFilterValues) &&
|
||||
currentFilterValues.length === 2
|
||||
) {
|
||||
const currentBounds: PositionBounds = {
|
||||
from: currentFilterValues[0] as [number, number],
|
||||
to: currentFilterValues[1] as [number, number],
|
||||
};
|
||||
|
||||
const pickedPoint: [number, number] = [
|
||||
data.coordinate[0],
|
||||
data.coordinate[1],
|
||||
];
|
||||
|
||||
if (isPointInBounds(pickedPoint, currentBounds)) {
|
||||
return {
|
||||
filters: [],
|
||||
values: currentFilterValues,
|
||||
customColumnLabel: filterState.customColumnLabel,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!formData.spatial) throw new Error('Spatial data is required');
|
||||
|
||||
return getFiltersBySpatialType({
|
||||
positions,
|
||||
position,
|
||||
positionBounds,
|
||||
spatialData: formData.spatial,
|
||||
});
|
||||
@@ -442,7 +353,7 @@ const getGeojsonFilters = ({
|
||||
const val = `%${JSON.stringify(geometry)}%`;
|
||||
|
||||
return {
|
||||
values: [geometry],
|
||||
values: [val],
|
||||
filters: [
|
||||
{
|
||||
col: {
|
||||
@@ -474,7 +385,7 @@ export const getCrossFilterDataMask = ({
|
||||
const result = getStartEndSpatialFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else if (formData.spatial?.type) {
|
||||
const result = getSpatialFilters({ formData, data, filterState });
|
||||
const result = getSpatialFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else if (formData.line_column) {
|
||||
const result = getLineColumnFilters({ formData, data });
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@types/d3-array": "^2.9.0",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"ag-grid-community": "^34.0.2",
|
||||
"ag-grid-react": "^34.0.2",
|
||||
"classnames": "^2.5.1",
|
||||
"d3-array": "^2.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -27,9 +27,7 @@ import {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { ThemedAgGridReact } from '@superset-ui/core/components';
|
||||
import {
|
||||
AgGridReact,
|
||||
AllCommunityModule,
|
||||
ClientSideRowModelModule,
|
||||
type ColDef,
|
||||
@@ -38,7 +36,9 @@ import {
|
||||
GridState,
|
||||
CellClickedEvent,
|
||||
IMenuActionParams,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
themeQuartz,
|
||||
} from 'ag-grid-community';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { type FunctionComponent } from 'react';
|
||||
import { JsonObject, DataRecordValue, DataRecord, t } from '@superset-ui/core';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
@@ -257,7 +257,11 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles} ref={containerRef}>
|
||||
<div
|
||||
className="ag-theme-quartz"
|
||||
style={containerStyles}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="dropdown-controls-container">
|
||||
{renderTimeComparisonDropdown && (
|
||||
<div className="time-comparison-dropdown">
|
||||
@@ -297,9 +301,10 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ThemedAgGridReact
|
||||
<AgGridReact
|
||||
ref={gridRef}
|
||||
onGridReady={onGridReady}
|
||||
theme={themeQuartz}
|
||||
className="ag-container"
|
||||
rowData={rowData}
|
||||
headerHeight={36}
|
||||
|
||||
@@ -26,10 +26,7 @@ import {
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
CellClickedEvent,
|
||||
IMenuActionParams,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { CellClickedEvent, IMenuActionParams } from 'ag-grid-community';
|
||||
import {
|
||||
AgGridTableChartTransformedProps,
|
||||
InputColumn,
|
||||
|
||||
@@ -36,19 +36,18 @@ import {
|
||||
QueryModeLabel,
|
||||
sections,
|
||||
sharedControls,
|
||||
shouldSkipMetricColumn,
|
||||
isRegularMetric,
|
||||
isPercentMetric,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
GenericDataType,
|
||||
getMetricLabel,
|
||||
isAdhocColumn,
|
||||
isFeatureEnabled,
|
||||
isPhysicalColumn,
|
||||
legacyValidateInteger,
|
||||
QueryFormColumn,
|
||||
QueryFormMetric,
|
||||
QueryMode,
|
||||
SMART_DATE_ID,
|
||||
t,
|
||||
@@ -534,25 +533,14 @@ const config: ControlPanelConfig = {
|
||||
)
|
||||
.forEach((colname, index) => {
|
||||
if (
|
||||
shouldSkipMetricColumn({
|
||||
colname,
|
||||
colnames,
|
||||
formData: explore.form_data,
|
||||
})
|
||||
explore.form_data.metrics?.some(
|
||||
metric => getMetricLabel(metric) === colname,
|
||||
) ||
|
||||
explore.form_data.percent_metrics?.some(
|
||||
(metric: QueryFormMetric) =>
|
||||
getMetricLabel(metric) === colname,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMetric = isRegularMetric(
|
||||
colname,
|
||||
explore.form_data,
|
||||
);
|
||||
const isPercentMetricValue = isPercentMetric(
|
||||
colname,
|
||||
explore.form_data,
|
||||
);
|
||||
|
||||
if (isMetric || isPercentMetricValue) {
|
||||
const comparisonColumns =
|
||||
generateComparisonColumns(colname);
|
||||
comparisonColumns.forEach((name, idx) => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { CustomCellRendererProps } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { CustomCellRendererProps } from 'ag-grid-react';
|
||||
import { BasicColorFormatterType, InputColumn } from '../types';
|
||||
import { useIsDark } from '../utils/useTableTheme';
|
||||
|
||||
|
||||
@@ -342,15 +342,8 @@ export const StyledChartContainer = styled.div<{
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.ag-header {
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
}
|
||||
|
||||
.ag-row {
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
}
|
||||
|
||||
.ag-header,
|
||||
.ag-row,
|
||||
.ag-spanned-row {
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
|
||||
@@ -36,12 +36,8 @@ import {
|
||||
JsonObject,
|
||||
Metric,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ColDef,
|
||||
Column,
|
||||
IHeaderParams,
|
||||
CustomCellRendererProps,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { ColDef, Column, IHeaderParams } from 'ag-grid-community';
|
||||
import { CustomCellRendererProps } from 'ag-grid-react';
|
||||
|
||||
export type CustomFormatter = (value: DataRecordValue) => string;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ValueGetterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { ValueGetterParams } from 'ag-grid-community';
|
||||
|
||||
const filterValueGetter = (params: ValueGetterParams) => {
|
||||
const raw = params.data[params.colDef.field as string];
|
||||
|
||||
@@ -25,10 +25,7 @@ import {
|
||||
isProbablyHTML,
|
||||
sanitizeHtml,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ValueFormatterParams,
|
||||
ValueGetterParams,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { ValueFormatterParams, ValueGetterParams } from 'ag-grid-community';
|
||||
import { DataColumnMeta, InputColumn } from '../types';
|
||||
import DateWithFormatter from './DateWithFormatter';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CellClassParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { CellClassParams } from 'ag-grid-community';
|
||||
import { InputColumn } from '../types';
|
||||
|
||||
type GetCellClassParams = CellClassParams & {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { ColorFormatters } from '@superset-ui/chart-controls';
|
||||
import { CellClassParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { CellClassParams } from 'ag-grid-community';
|
||||
import { BasicColorFormatterType, InputColumn } from '../types';
|
||||
|
||||
type CellStyleParams = CellClassParams & {
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
// All ag grid sort related stuff
|
||||
import {
|
||||
GridState,
|
||||
SortModelItem,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { GridState, SortModelItem } from 'ag-grid-community';
|
||||
import { SortByItem } from '../types';
|
||||
|
||||
const getInitialSortState = (sortBy?: SortByItem[]): SortModelItem[] => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ColDef } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { ColDef } from 'ag-grid-community';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { DataRecord, GenericDataType } from '@superset-ui/core';
|
||||
import { ColorFormatters } from '@superset-ui/chart-controls';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
colorSchemeDark,
|
||||
colorSchemeLight,
|
||||
themeQuartz,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
} from 'ag-grid-community';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
formatSelectOptionsForRange,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { xAxisLabelRotation } from '../controls';
|
||||
|
||||
const sortAxisChoices = [
|
||||
['alpha_asc', t('Axis ascending')],
|
||||
@@ -154,7 +153,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'xscale_interval',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('X-scale interval'),
|
||||
label: t('XScale Interval'),
|
||||
renderTrigger: true,
|
||||
choices: [[-1, t('Auto')]].concat(
|
||||
formatSelectOptionsForRange(1, 50),
|
||||
@@ -172,7 +171,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'yscale_interval',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Y-scale interval'),
|
||||
label: t('YScale Interval'),
|
||||
choices: [[-1, t('Auto')]].concat(
|
||||
formatSelectOptionsForRange(1, 50),
|
||||
),
|
||||
@@ -249,7 +248,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
['y_axis_format'],
|
||||
['x_axis_time_format'],
|
||||
[xAxisLabelRotation],
|
||||
['currency_format'],
|
||||
[
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user