mirror of
https://github.com/apache/superset.git
synced 2026-09-11 01:34:35 +00:00
Compare commits
@@ -107,6 +107,12 @@ jobs:
|
||||
needs.changes.outputs.docker == 'true'
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
# Required for the vulnerability scan below to upload its SARIF
|
||||
# results to GitHub code scanning (advanced-security is enabled by
|
||||
# default), matching the same grant in github-action-validator.yml.
|
||||
security-events: write
|
||||
strategy:
|
||||
matrix:
|
||||
build_preset: ${{fromJson(needs.setup_matrix.outputs.matrix_config)}}
|
||||
@@ -213,6 +219,39 @@ jobs:
|
||||
docker images $IMAGE_TAG
|
||||
docker history $IMAGE_TAG
|
||||
|
||||
# Scan the built image for known vulnerabilities and publish results
|
||||
# to the Security tab. Trivy did this until #38780 removed it: both
|
||||
# aquasecurity/trivy-action and the trivy binary itself were
|
||||
# compromised (twice) to steal GitHub Secrets from CI runs. Grype is a
|
||||
# different tool from a different maintainer with no shared supply
|
||||
# chain, and is already on the ASF Infra GitHub Actions allowlist.
|
||||
- name: Scan built image for vulnerabilities
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && matrix.build_preset == 'lean'
|
||||
id: grype-scan
|
||||
# This step's own failure (e.g. a transient issue pulling the Grype
|
||||
# vulnerability DB) must not fail docker-build, matching the
|
||||
# informational fail-build: false below -- one is findings, the
|
||||
# other is the scan itself not completing.
|
||||
continue-on-error: true
|
||||
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
output-format: sarif
|
||||
severity-cutoff: high
|
||||
only-fixed: true
|
||||
# Informational only, matching the prior Trivy setup: this
|
||||
# workflow does not gate merges on scan findings.
|
||||
fail-build: false
|
||||
|
||||
- name: Upload vulnerability scan results to GitHub Security tab
|
||||
if: >-
|
||||
github.event_name == 'push' && github.ref == 'refs/heads/master' &&
|
||||
matrix.build_preset == 'lean' && steps.grype-scan.outputs.sarif != ''
|
||||
continue-on-error: true
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
sarif_file: ${{ steps.grype-scan.outputs.sarif }}
|
||||
|
||||
- name: WebSocket server smoke test
|
||||
if: contains(fromJson('["lean", "dev"]'), matrix.build_preset)
|
||||
shell: bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: 🎪 Superset Showtime
|
||||
|
||||
# Ultra-simple: just sync on any PR state change
|
||||
# Sync on Showtime label changes and updates to PRs using Showtime.
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; PR code is
|
||||
# only checked out and built after the maintainer-authorization gate (write/admin actors)
|
||||
@@ -19,6 +19,19 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# Triggers on labeled/unlabeled/synchronize/closed -- far more events per PR than
|
||||
# typical CI's synchronize-only trigger. Without this, every event queues its own
|
||||
# run and nothing ever supersedes a stale one, so runs pile up indefinitely on
|
||||
# active PRs (observed: 6 simultaneously queued runs for a single PR) and end up
|
||||
# starved competing for the same runner pool as everything else. cancel-in-progress
|
||||
# is correct here, not just a queue-relief hack: this job's whole purpose is
|
||||
# reconciling to the *current* desired state, so a run still working toward an
|
||||
# already-superseded state is wasted work regardless of whether it's queued or
|
||||
# mid-sync.
|
||||
concurrency:
|
||||
group: showtime-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Common environment variables for all jobs (non-sensitive only)
|
||||
env:
|
||||
AWS_REGION: us-west-2
|
||||
@@ -32,6 +45,15 @@ permissions:
|
||||
jobs:
|
||||
sync:
|
||||
name: 🎪 Sync PR to desired state
|
||||
# Inspect the changed label so removing the last Showtime label still syncs.
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
startsWith(github.event.label.name, '🎪 ') ||
|
||||
(
|
||||
(github.event.action == 'synchronize' ||
|
||||
github.event.action == 'closed') &&
|
||||
contains(toJson(github.event.pull_request.labels.*.name), '"🎪 ')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
uses: $/.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -109,13 +109,25 @@ jobs:
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Setup postgres
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
@@ -125,25 +137,54 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Install Playwright
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: playwright-install
|
||||
- name: Run Playwright (Experimental Tests)
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" experimental/
|
||||
- name: Run Playwright (Mobile Tests)
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
# Scoped to this step: setting feature flags at the job level would
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
uses: ./.github/actions/change-detector/
|
||||
uses: $/.github/actions/change-detector/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -75,13 +75,25 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Setup MySQL
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: setup-mysql
|
||||
- name: Start Celery worker
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (MySQL)
|
||||
@@ -160,16 +172,28 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
uses: $/.github/actions/setup-backend/
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup Postgres
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: |
|
||||
setup-postgres
|
||||
- name: Start Celery worker
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (PostgreSQL)
|
||||
@@ -224,7 +248,13 @@ jobs:
|
||||
# sqlite needs this working directory
|
||||
mkdir ${{ github.workspace }}/.temp
|
||||
- name: Start Celery worker
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python integration tests (SQLite)
|
||||
|
||||
@@ -92,7 +92,13 @@ jobs:
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Start Celery worker
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: celery-worker
|
||||
- name: Python unit tests (PostgreSQL)
|
||||
@@ -149,7 +155,13 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Setup Postgres
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Start Celery worker
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# db_engine_specs tests against real databases (testcontainers)
|
||||
name: Testcontainers
|
||||
|
||||
# Spins up real Docker containers (see tests/testcontainers/ for the current
|
||||
# dialect list) via testcontainers-python, which catches real dialect/driver
|
||||
# regressions -- the kind mocked db_engine_specs unit tests structurally
|
||||
# cannot, e.g. apache/superset#42899 (Trino emitting OFFSET before LIMIT).
|
||||
# Runs on a nightly cron (catches drift from a driver's own releases, not
|
||||
# just from Superset's changes) and on pull_request, scoped via `paths` to
|
||||
# only PRs that actually touch this test suite or the workflow itself, so
|
||||
# unrelated PRs across the repo are never affected.
|
||||
#
|
||||
# A matrix entry can set `nightly_only: true` to run only on the cron (or a
|
||||
# manual workflow_dispatch), never on pull_request -- for a dialect whose
|
||||
# image is too heavy (a multi-service cluster, a many-GB image, a slow
|
||||
# licensed installer) to justify adding its wall-clock/resource cost to
|
||||
# every PR that merely touches this suite. Omit the field entirely for a
|
||||
# normal dialect; it isn't nightly-only by default.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 5 * * *"
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/testcontainers.yml"
|
||||
- "tests/testcontainers/**"
|
||||
- "superset/db_engine_specs/**"
|
||||
- "pyproject.toml"
|
||||
- "requirements/development.in"
|
||||
- "requirements/development.txt"
|
||||
|
||||
concurrency:
|
||||
# Scoped by ref, not just workflow name -- otherwise every PR run and the
|
||||
# nightly cron share one group, and starting the workflow on another PR
|
||||
# (or the nightly firing mid-PR-run) cancels an unrelated in-progress run.
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compute-matrix:
|
||||
# Filters out `nightly_only` dialects for a pull_request run *before* any
|
||||
# testcontainers job is created for them, so a heavy dialect costs a PR
|
||||
# nothing at all -- not even its checkout/setup/driver-install steps --
|
||||
# rather than being merely skipped at the test step. A job-level `if:`
|
||||
# can't reference `matrix` (only github/inputs/needs/vars are available
|
||||
# there), so the filtering has to happen here, before the matrix exists,
|
||||
# instead of on the testcontainers job itself.
|
||||
runs-on: ubuntu-26.04
|
||||
outputs:
|
||||
include: ${{ steps.filter.outputs.include }}
|
||||
steps:
|
||||
- name: Filter nightly-only dialects
|
||||
id: filter
|
||||
env:
|
||||
# One entry per dialect rather than one job for the whole suite: a
|
||||
# single slow container would otherwise inflate the wall-clock
|
||||
# time for every dialect, not just its own. Running in parallel
|
||||
# means the suite's total time is bounded by the slowest dialect,
|
||||
# not the sum of all of them. Db2's first-boot init is documented
|
||||
# upstream as notably slow (a real instance bring-up, not just a
|
||||
# process start) and untested locally here (no arm64 image), so
|
||||
# it gets a wider timeout margin than the rest until real CI data
|
||||
# says otherwise.
|
||||
FULL_MATRIX: |
|
||||
[
|
||||
{"dialect": "cockroachdb", "timeout": 10},
|
||||
{"dialect": "crate", "timeout": 10},
|
||||
{"dialect": "trino", "timeout": 10},
|
||||
{"dialect": "mssql", "timeout": 10},
|
||||
{"dialect": "elasticsearch", "timeout": 10},
|
||||
{"dialect": "oracle", "timeout": 15},
|
||||
{"dialect": "db2", "timeout": 25},
|
||||
{"dialect": "mariadb", "timeout": 10},
|
||||
{"dialect": "timescaledb", "timeout": 10},
|
||||
{"dialect": "yugabytedb", "timeout": 10},
|
||||
{"dialect": "monetdb", "timeout": 10},
|
||||
{"dialect": "mongodb", "timeout": 10},
|
||||
{"dialect": "postgres", "timeout": 10},
|
||||
{"dialect": "mysql", "timeout": 10},
|
||||
{"dialect": "clickhouse", "timeout": 10},
|
||||
{"dialect": "starrocks", "timeout": 15},
|
||||
{"dialect": "databend", "timeout": 10},
|
||||
{"dialect": "risingwave", "timeout": 10},
|
||||
{"dialect": "firebird", "timeout": 10},
|
||||
{"dialect": "ydb", "timeout": 10},
|
||||
{"dialect": "oceanbase", "timeout": 20, "nightly_only": true}
|
||||
]
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
include="$(jq -c '.' <<<"$FULL_MATRIX")"
|
||||
else
|
||||
include="$(jq -c '[.[] | select(.nightly_only != true)]' <<<"$FULL_MATRIX")"
|
||||
fi
|
||||
echo "include=${include}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
testcontainers:
|
||||
needs: [compute-matrix]
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.compute-matrix.outputs.include) }}
|
||||
timeout-minutes: ${{ matrix.timeout }}
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SUPERSET_TESTENV: true
|
||||
SUPERSET_SECRET_KEY: not-a-secret
|
||||
# This job's matrix installs exactly one dialect's testcontainers
|
||||
# driver for exactly this job, so treat that driver as required: a
|
||||
# broken/missing import should fail the job, not silently skip to a
|
||||
# misleadingly green, zero-tests-run result. See _driver.py.
|
||||
SUPERSET_TESTCONTAINERS_STRICT: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
with:
|
||||
python-version: current
|
||||
- name: Install db2 driver (ibm-db-sa)
|
||||
# ibm-db (the db2 DBAPI) ships no Linux arm64 wheel, so it's kept out
|
||||
# of the baseline dev install (requirements/development.in) to avoid
|
||||
# breaking the multi-platform dev Docker image build. Install it here
|
||||
# instead, only for this leg of the matrix.
|
||||
if: matrix.dialect == 'db2'
|
||||
run: uv pip install --system -e .[db2]
|
||||
- name: Install oceanbase driver (oceanbase_py)
|
||||
# oceanbase_py pins sqlalchemy-utils>=0.38.3,<0.39, which conflicts
|
||||
# outright with Superset's own sqlalchemy-utils==0.42.1 pin -- kept
|
||||
# out of the baseline dev install for the same reason as db2 above.
|
||||
# Installed as its own standalone package (not via `-e .[oceanbase]`)
|
||||
# so --no-deps only skips *oceanbase_py's* dependencies -- applied
|
||||
# to `-e .[oceanbase]` instead, --no-deps blocks pip from installing
|
||||
# anything the extras marker pulls in, including oceanbase_py
|
||||
# itself, which "succeeds" without actually installing it
|
||||
# (confirmed on real CI: the install step reported success, but the
|
||||
# module was still missing). This job only needs oceanbase_py's
|
||||
# dialect module importable, not its sqlalchemy-utils dependency
|
||||
# satisfied, since nothing here calls into it.
|
||||
if: >-
|
||||
matrix.dialect == 'oceanbase' &&
|
||||
(matrix.nightly_only != true ||
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name == 'workflow_dispatch')
|
||||
run: uv pip install --system --no-deps "oceanbase_py>=0.0.1.2"
|
||||
- name: Install Firebird client library (libfbclient2)
|
||||
# sqlalchemy-firebird's driver (firebird-driver) is a pure-Python
|
||||
# ctypes wrapper (its wheel is py3-none-any) that dynamically loads
|
||||
# the native Firebird client library from the host at import time
|
||||
# -- it doesn't bundle that library itself, so it has to come from
|
||||
# the system package manager, only for this leg of the matrix.
|
||||
if: matrix.dialect == 'firebird'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfbclient2
|
||||
- name: Run testcontainers db_engine_specs tests (${{ matrix.dialect }})
|
||||
# nightly_only dialects are already excluded from the matrix itself
|
||||
# on pull_request runs (see the compute-matrix job above), so this
|
||||
# step needs no additional gating.
|
||||
run: |
|
||||
pytest --durations-min=2 -v -m testcontainers \
|
||||
./tests/testcontainers/db_engine_specs/test_${{ matrix.dialect }}.py \
|
||||
--junit-xml=test-results/junit-testcontainers-${{ matrix.dialect }}.xml
|
||||
- name: Upload JUnit test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: junit-results-testcontainers-${{ matrix.dialect }}
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
|
||||
actions-timeline:
|
||||
needs: [testcontainers]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
|
||||
+56
-25
@@ -231,20 +231,6 @@ RUN /app/docker/apt-install.sh \
|
||||
# The database file will be created at runtime when examples are loaded from Parquet files
|
||||
RUN mkdir -p /app/data && chown -R superset:superset /app/data
|
||||
|
||||
# Copy compiled things from previous stages
|
||||
COPY --from=superset-node /app/superset/static/assets superset/static/assets
|
||||
# Copy service.worker.js optionall as it doesn't exist when DEV_MODE=true
|
||||
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
|
||||
|
||||
# TODO, when the next version comes out, use --exclude superset/translations
|
||||
COPY superset superset
|
||||
# TODO in the meantime, remove the .po files
|
||||
RUN rm superset/translations/*/*/*.po
|
||||
|
||||
# Merging translations from backend and frontend stages
|
||||
COPY --from=superset-node /app/superset/translations superset/translations
|
||||
COPY --from=python-translation-compiler /app/translations_mo superset/translations
|
||||
|
||||
# --- Realtime WebSocket server (part of the official image) ---------------
|
||||
# The realtime transport (superset-websocket) is a Node service, bundled by
|
||||
# esbuild into a single self-contained file. Copy the Node runtime plus that
|
||||
@@ -267,7 +253,10 @@ EXPOSE ${SUPERSET_PORT}
|
||||
######################################################################
|
||||
FROM python-common AS lean
|
||||
|
||||
# Install Python dependencies using docker/pip-install.sh
|
||||
# Install Python dependencies using docker/pip-install.sh.
|
||||
# Requirements are installed *before* the application source is copied
|
||||
# below so that source-only changes don't bust this (slow, network-bound)
|
||||
# cache layer or defeat --cache-from.
|
||||
COPY requirements/base.txt requirements/
|
||||
|
||||
# Copy superset-core package needed for editable install in base.txt
|
||||
@@ -275,9 +264,27 @@ COPY superset-core superset-core
|
||||
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/base.txt
|
||||
# Install the superset package
|
||||
|
||||
# Copy compiled frontend assets and application source now that
|
||||
# dependencies have been resolved and cached above.
|
||||
COPY --from=superset-node /app/superset/static/assets superset/static/assets
|
||||
# Copy service.worker.js optionally as it doesn't exist when DEV_MODE=true
|
||||
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
|
||||
|
||||
# TODO, when the next version comes out, use --exclude superset/translations
|
||||
COPY superset superset
|
||||
# TODO in the meantime, remove the .po files
|
||||
RUN rm superset/translations/*/*/*.po
|
||||
|
||||
# Merging translations from backend and frontend stages
|
||||
COPY --from=superset-node /app/superset/translations superset/translations
|
||||
COPY --from=python-translation-compiler /app/translations_mo superset/translations
|
||||
|
||||
# Install the superset package itself. --no-deps because its dependencies
|
||||
# were already installed from requirements/base.txt above, so this layer
|
||||
# stays fast even though the source copy above changes on every edit.
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
uv pip install -e .
|
||||
uv pip install -e . --no-deps
|
||||
RUN python -m compileall /app/superset
|
||||
|
||||
USER superset
|
||||
@@ -293,22 +300,46 @@ RUN /app/docker/apt-install.sh \
|
||||
pkg-config \
|
||||
default-libmysqlclient-dev
|
||||
|
||||
# Copy development requirements and install them
|
||||
# Copy development requirements and install them *before* the application
|
||||
# source is copied below, so source-only edits don't bust this cache layer.
|
||||
COPY requirements/*.txt requirements/
|
||||
|
||||
# Copy local packages needed for editable installs in development.txt
|
||||
COPY superset-core superset-core
|
||||
COPY superset-extensions-cli superset-extensions-cli
|
||||
|
||||
# Install Python dependencies using docker/pip-install.sh
|
||||
# requirements/development.txt is generated by `uv pip compile` and embeds
|
||||
# `-e .` (an editable install of this same package) as its first line. That
|
||||
# self-reference needs the full superset/ source tree, which hasn't been
|
||||
# copied in yet at this point, so it's stripped here; the real editable
|
||||
# install of `.` runs below, once the source is present.
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/development.txt
|
||||
# Install the superset package
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
uv pip install -e .
|
||||
grep -vxF -- "-e ." requirements/development.txt > requirements/development-deps.txt && \
|
||||
/app/docker/pip-install.sh --requires-build-essential -r requirements/development-deps.txt
|
||||
|
||||
RUN uv pip install .[postgres]
|
||||
RUN python -m compileall /app/superset
|
||||
# Copy compiled frontend assets and application source now that
|
||||
# dependencies have been resolved and cached above.
|
||||
COPY --from=superset-node /app/superset/static/assets superset/static/assets
|
||||
# Copy service.worker.js optionally as it doesn't exist when DEV_MODE=true
|
||||
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
|
||||
|
||||
# TODO, when the next version comes out, use --exclude superset/translations
|
||||
COPY superset superset
|
||||
# TODO in the meantime, remove the .po files
|
||||
RUN rm superset/translations/*/*/*.po
|
||||
|
||||
# Merging translations from backend and frontend stages
|
||||
COPY --from=superset-node /app/superset/translations superset/translations
|
||||
COPY --from=python-translation-compiler /app/translations_mo superset/translations
|
||||
|
||||
# Install the superset package together with its postgres extra, using the
|
||||
# same uv cache mount as the requirements install above. --no-deps because
|
||||
# all dependencies (including the postgres extra's psycopg2-binary) are
|
||||
# already installed from requirements/development.txt above.
|
||||
# NOTE: source is bind-mounted over /app/superset in DEV_MODE, so a
|
||||
# compileall pass here would be wasted work; unlike `lean`, `dev` skips it.
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
uv pip install -e .[postgres] --no-deps
|
||||
|
||||
USER superset
|
||||
|
||||
|
||||
+15
-1
@@ -24,6 +24,18 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Resample "Fill the entire time range"
|
||||
|
||||
Charts with Resample can enable **Fill the entire time range** so gap-filling
|
||||
covers the full queried window (`from_dttm` / `to_dttm`), not only between the
|
||||
first and last returned data points. Existing charts are unchanged until the
|
||||
control is turned on.
|
||||
|
||||
Resample projections remain capped by `MAX_RESAMPLE_ROWS` (default
|
||||
`1_000_000`). That cap now also covers calendar frequencies (month, quarter,
|
||||
year, …) that previously skipped the check because they have no fixed
|
||||
`Timedelta`.
|
||||
|
||||
### Tagging is on by default
|
||||
|
||||
`TAGGING_SYSTEM` now ships **on**. The Tags menu entry, the tag columns and
|
||||
@@ -232,7 +244,7 @@ unknown impact as zero. Chart and dashboard purge endpoints are unchanged.
|
||||
- The dashboard datasource-based visibility fallback now fails closed: a dashboard whose member charts’ datasources cannot be resolved (deleted datasource rows, missing `datasource_id`, or unsupported datasource types) is no longer accessible to users without explicit editor/viewer rights, and a dashboard composed of semantic-view charts now requires `datasource_access` on (at least one of) its semantic views or their parent semantic layer — previously any authenticated user could open such a dashboard’s shell. Because the fallback now considers every member chart rather than only table-backed ones, a user holding `datasource_access` on any single member datasource — including a semantic view or its parent layer — can open a mixed dashboard that previously denied them. Dashboards with no charts remain accessible, and dashboards with explicit viewers are unaffected. Conversely, holders of `all_datasource_access` now see every published no-viewer dashboard in the dashboard list — including chart-less ones previously hidden by the inner joins — matching what the object-level gate already allowed them to open.
|
||||
- Version restore (`POST /api/v1/{chart,dashboard,dataset}/<uuid>/versions/<version_uuid>/restore`) now refuses an **externally managed** entity (`is_managed_externally = True`) with HTTP 403, enforcing server-side what the docs already promised. Previously the refusal existed only in the browser, so an otherwise-authorized editor could restore such an entity by calling the endpoint directly and have the restore overwritten on the next external sync. Soft-delete recovery is deliberately unaffected — it changes visibility, not content.
|
||||
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
|
||||
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.
|
||||
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed must `pip uninstall cockroachdb` before reinstalling the extra -- both packages register the same `cockroachdb` SQLAlchemy dialect entry point, so leaving the old one in place can still load the abandoned implementation.
|
||||
|
||||
### Native Value filter "Select all" always targets the whole column
|
||||
|
||||
@@ -1233,6 +1245,8 @@ Custom time ranges that use the "Now" or "Today" anchor (for the Start, End, or
|
||||
|
||||
Charts and dashboards using these anchors will compute a different (correct) timestamp after upgrading; if a chart's filters or drill-downs were tuned to compensate for the old offset, review them after upgrading.
|
||||
|
||||
- [43916](https://github.com/apache/superset/pull/43916): The `docker-compose` dev loop now skips re-running `superset load_examples` on every `docker compose up` once the example data and dashboards are present in the databases (set `SUPERSET_FORCE_LOAD_EXAMPLES=yes` to reload them anyway), and the `superset-node` service now defaults `DISABLE_TS_CHECKER=true` like `docker-compose-light.yml` already did, skipping webpack's TypeScript type-checking pass in dev by default.
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### ClickHouse minimum driver version bump
|
||||
|
||||
@@ -138,6 +138,7 @@ services:
|
||||
condition: service_started
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
SUPERSET_FORCE_LOAD_EXAMPLES: "${SUPERSET_FORCE_LOAD_EXAMPLES:-}"
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
POSTGRES_DB: superset_light
|
||||
|
||||
@@ -183,6 +183,8 @@ services:
|
||||
condition: service_started
|
||||
user: *superset-user
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
SUPERSET_FORCE_LOAD_EXAMPLES: "${SUPERSET_FORCE_LOAD_EXAMPLES:-}"
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
@@ -202,6 +204,7 @@ services:
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
|
||||
NPM_RUN_PRUNE: false
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
|
||||
# configuring the dev-server to use the host.docker.internal to connect to the backend
|
||||
superset: "http://superset:8088"
|
||||
# Webpack dev server must bind to 0.0.0.0 to be accessible from outside the container
|
||||
|
||||
@@ -73,6 +73,12 @@ SUPERSET_ENV=development
|
||||
# Swagger UI is opt-in (off by default); enable it for local development.
|
||||
SUPERSET_ENABLE_SWAGGER_UI=true
|
||||
SUPERSET_LOAD_EXAMPLES=yes
|
||||
# Once the example data and dashboards are present in the databases,
|
||||
# `docker-init.sh` skips `superset load_examples` on later runs. Set to "yes"
|
||||
# (or run `SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up`) to reload the
|
||||
# examples anyway, e.g. after changing the example datasets or after a partial
|
||||
# load.
|
||||
#SUPERSET_FORCE_LOAD_EXAMPLES=no
|
||||
CYPRESS_CONFIG=false
|
||||
SUPERSET_PORT=8088
|
||||
MAPBOX_API_KEY=''
|
||||
|
||||
+33
-3
@@ -66,14 +66,44 @@ echo_step "3" "Starting" "Setting up roles and perms"
|
||||
superset init
|
||||
echo_step "3" "Complete" "Setting up roles and perms"
|
||||
|
||||
# Loading examples parses and inserts every example dataset, chart and
|
||||
# dashboard and is one of the slowest steps of `docker compose up`. Rather
|
||||
# than trusting a marker file (which goes stale as soon as the database volume
|
||||
# is recreated), ask the databases themselves: when both the example data and
|
||||
# the dashboards imported from it are present, the previous load completed and
|
||||
# there is nothing left to redo. Any failure here (missing tables, unreachable
|
||||
# database, import error) simply reports "not loaded" so the full load runs.
|
||||
examples_already_loaded() {
|
||||
python - <<'PY' 2>/dev/null
|
||||
import sys
|
||||
|
||||
from superset.app import create_app
|
||||
from superset.sql.parse import Table
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
from superset import db
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.utils.database import get_example_database
|
||||
|
||||
has_dashboard = (
|
||||
db.session.query(Dashboard).filter_by(slug="world_health").first() is not None
|
||||
)
|
||||
has_data = get_example_database().has_table(Table("wb_health_population"))
|
||||
sys.exit(0 if has_dashboard and has_data else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
|
||||
# Load some data to play with
|
||||
echo_step "4" "Starting" "Loading examples"
|
||||
|
||||
|
||||
# If Cypress run which consumes superset_test_config – load required data for tests
|
||||
# Cypress runs always load, since they need a distinct set of test data
|
||||
# (`--load-test-data`) in a separate database. Set
|
||||
# SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
superset load_examples --load-test-data
|
||||
elif [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
|
||||
echo "Examples already loaded, skipping (set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload them)"
|
||||
else
|
||||
superset load_examples
|
||||
fi
|
||||
|
||||
@@ -259,8 +259,8 @@ Superset automatically retries webhook deliveries on `429 Too Many Requests` and
|
||||
|
||||
### Kubernetes-specific
|
||||
|
||||
- You must have a `celery beat` pod running. If you're using the chart included in the GitHub repository under [helm/superset](https://github.com/apache/superset/tree/master/helm/superset), you need to put `supersetCeleryBeat.enabled = true` in your values override.
|
||||
- You can see the dedicated docs about [Kubernetes installation](/admin-docs/installation/kubernetes) for more details.
|
||||
- You must have a `celery beat` pod running. For Kubernetes deployments, use the [Apache Superset Kubernetes Operator documentation](https://apache.github.io/superset-kubernetes-operator/) for deployment configuration.
|
||||
- Existing legacy Helm chart deployments configure this with `supersetCeleryBeat.enabled = true` in the values override.
|
||||
|
||||
### Docker Compose specific
|
||||
|
||||
|
||||
@@ -145,6 +145,46 @@ D3_TIME_FORMAT = {
|
||||
Restart Superset after changing `superset_config.py` so the frontend receives
|
||||
the updated formatter configuration.
|
||||
|
||||
## Serving translated language packs
|
||||
|
||||
Non-English page loads need the frontend translation catalog (the "language
|
||||
pack") available before the entry bundle runs, so translations are in place
|
||||
for the very first render instead of racing a later fetch. Superset delivers
|
||||
that pack as a separate, cacheable script rather than inlining it into the
|
||||
HTML:
|
||||
|
||||
```html
|
||||
<script src="/language_pack/pt_BR/1a2b3c4d5e6f/script.js"></script>
|
||||
```
|
||||
|
||||
`spa.html` emits this tag, pointing at the `language_pack_script` view, before
|
||||
loading the entry bundle whenever the request's locale isn't English. The
|
||||
`<version>` segment is a short hash of the pack's contents, so the URL is
|
||||
content-addressed:
|
||||
|
||||
- When the version in the URL matches the server's current pack, the response
|
||||
carries `Cache-Control: public, max-age=31536000, immutable` — the browser
|
||||
fetches that language's pack once and reuses it across sessions.
|
||||
- If a cached HTML page references a version that's since changed (e.g. after
|
||||
a translation update or upgrade), the endpoint still serves the current
|
||||
pack, but with `Cache-Control: no-cache` so any copy stored under the
|
||||
now-stale URL must be revalidated with the server before it's reused.
|
||||
- English pages emit no script tag; there's no pack to load.
|
||||
|
||||
Each worker caches a locale's pack and version hash in memory for its
|
||||
lifetime, so a translation file changed on disk isn't picked up, and doesn't
|
||||
produce a new version hash, until the worker restarts. Restart (or roll)
|
||||
Superset after deploying a translation update so clients get the new pack.
|
||||
|
||||
This endpoint is intentionally unauthenticated. Translation catalogs are
|
||||
static, public content shipped in the Superset repo, and the login page and
|
||||
embedded dashboards need them to load before a user session exists.
|
||||
|
||||
If you already override the language pack via `COMMON_BOOTSTRAP_OVERRIDES_FUNC`
|
||||
(a `common.language_pack` value, historically used to work around translation
|
||||
race conditions), that override still takes precedence: `spa.html` skips the
|
||||
script tag and uses your supplied pack instead.
|
||||
|
||||
## Chart-data query timing
|
||||
|
||||
Set `CHART_DATA_INCLUDE_TIMING = True` to add an optional versioned timing object
|
||||
|
||||
@@ -13,7 +13,7 @@ your countries by province, states, or other subdivision types. It does not rely
|
||||
on any third-party map services but would require you to provide the
|
||||
[ISO-3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) codes of your country's
|
||||
top-level subdivisions. Comparing to a province or state's full names, the ISO
|
||||
code is less ambiguous and is unique to all regions in the world.
|
||||
code is less ambiguous and is unique to all regions in the world (except for [Madagascar](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries/madagascar.geojson)).
|
||||
|
||||
## Included Maps
|
||||
|
||||
|
||||
@@ -1007,3 +1007,34 @@ The callable must be cheap and in-process (consult already-loaded feature flags
|
||||
- **[MCP Integration](/developer-docs/extensions/mcp)** -- Build custom MCP tools and prompts via Superset extensions
|
||||
- **[Security](/developer-docs/extensions/security)** -- Security best practices for extensions
|
||||
- **[Deployment](/developer-docs/extensions/deployment)** -- Package and deploy Superset extensions
|
||||
|
||||
### Reporting query dates
|
||||
|
||||
`query_dataset` and `get_table` return `from_dttm` (inclusive) and `to_dttm`
|
||||
(exclusive): the primary time boundaries from the query engine's result payload,
|
||||
including cached and empty results. Callers should quote these ISO 8601 values
|
||||
when explaining the numbers, rather than calculate dates from the request again.
|
||||
For example, `previous calendar month` evaluated on July 17, 2026 resolves to
|
||||
`2026-06-01T00:00:00` through, but not including, `2026-07-01T00:00:00`.
|
||||
`Last month` is a rolling month, not the previous calendar month.
|
||||
|
||||
On a cache hit, these bounds reflect the current request, not necessarily the
|
||||
original cached execution. A cached result can outlive a relative-range rollover;
|
||||
check `cache_status.cache_hit` before describing the bounds as execution dates.
|
||||
|
||||
Relative expressions use Superset's server clock and configured
|
||||
`DEFAULT_RELATIVE_START_TIME` / `DEFAULT_RELATIVE_END_TIME` (both default to
|
||||
`today`, meaning midnight). Valid explicit dates are used as supplied; the server
|
||||
cannot determine whether a model intended a different year.
|
||||
|
||||
These fields describe the engine's **primary logical range**, not the observed
|
||||
minimum/maximum dates in the rows or every SQL predicate. A null boundary means
|
||||
no primary bound is available; it does not prove the absence of other filters.
|
||||
Additional column filters and virtual dataset SQL can further constrain the
|
||||
data. MCP validation rejects open-ended `time_range` strings such as
|
||||
`"2025-01-01 : "` or `" : 2025-02-01"` before execution; use explicit comparison
|
||||
filters for one-sided constraints. Those comparisons are not primary bounds. Naive
|
||||
boundaries are not labeled UTC: dataset timezone or legacy hour-offset settings
|
||||
can adjust the SQL comparisons. Use the applied filters and datasource settings
|
||||
as well when explaining such queries. `query_dataset.applied_filters` retains the
|
||||
original expressions so callers can compare the requested and resolved ranges.
|
||||
|
||||
@@ -18,7 +18,7 @@ Since `docker compose` is primarily designed to run a set of containers on **a s
|
||||
and can't support requirements for **high availability**, we do not support nor recommend
|
||||
using our `docker compose` constructs to support production-type use-cases. For single host
|
||||
environments, we recommend using [minikube](https://minikube.sigs.k8s.io/docs/start/) along
|
||||
with our [installing on k8s](https://superset.apache.org/admin-docs/installation/running-on-kubernetes)
|
||||
with our [Kubernetes installation](/admin-docs/installation/kubernetes)
|
||||
documentation.
|
||||
:::
|
||||
|
||||
@@ -196,7 +196,9 @@ One important variable is `SUPERSET_LOAD_EXAMPLES` which determines whether the
|
||||
container will populate example data and visualizations into the metadata database. These examples
|
||||
are helpful for learning and testing out Superset but unnecessary for experienced users and
|
||||
production deployments. The loading process can sometimes take a few minutes and a good amount of
|
||||
CPU, so you may want to disable it on a resource-constrained device.
|
||||
CPU, so you may want to disable it on a resource-constrained device. Once the example data and
|
||||
dashboards are present in the databases, later `superset_init` runs skip loading them; run
|
||||
`SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up` to reload the examples anyway.
|
||||
|
||||
For more advanced or dynamic configurations that are typically managed in a `superset_config.py` file
|
||||
located in your `PYTHONPATH`, note that it can be done by providing a
|
||||
|
||||
@@ -33,13 +33,13 @@ Ideally you will build your own image of Superset that extends `lean`, adding wh
|
||||
|
||||
**Summary:** This is the best-practice way to deploy a production instance of Superset, but has the steepest skill requirement - someone who knows Kubernetes.
|
||||
|
||||
You will deploy Superset into a K8s cluster. The most common method is using the community-maintained Helm chart, though work is now underway to implement [SIP-149 - a Kubernetes Operator for Superset](https://github.com/apache/superset/issues/31408).
|
||||
You will deploy Superset into a K8s cluster. The recommended method is the official [Apache Superset Kubernetes Operator](https://apache.github.io/superset-kubernetes-operator/). The in-tree Helm chart is deprecated and is not recommended for new deployments.
|
||||
|
||||
A K8s deployment can scale up and down based on usage and deploy rolling updates with zero downtime - features that big deployments appreciate.
|
||||
|
||||
**Responsibilities**
|
||||
|
||||
You will need to build your own Docker image, and back up your metadata DB, both as described in Docker Compose above. You'll also need to customize your Helm chart values and deploy and maintain your Kubernetes cluster.
|
||||
You will need to build your own Docker image, and back up your metadata DB, both as described in Docker Compose above. You'll also need to configure the operator's Superset resources and deploy and maintain your Kubernetes cluster.
|
||||
|
||||
## [PyPI (Python)](/admin-docs/installation/pypi)
|
||||
|
||||
|
||||
@@ -13,499 +13,35 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Running Superset on Kubernetes is supported with the provided [Helm](https://helm.sh/) chart
|
||||
found in the official [Superset helm repository](https://apache.github.io/superset/index.yaml).
|
||||
Running Superset on Kubernetes is supported through the official
|
||||
[Apache Superset Kubernetes Operator](https://apache.github.io/superset-kubernetes-operator/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Kubernetes cluster
|
||||
- Helm installed
|
||||
|
||||
:::note
|
||||
For simpler, single host environments, we recommend using
|
||||
[minikube](https://minikube.sigs.k8s.io/docs/start/) which is easy to setup on many platforms
|
||||
and works fantastically well with the Helm chart referenced here.
|
||||
:::warning
|
||||
The Superset Helm chart under
|
||||
[`helm/superset`](https://github.com/apache/superset/tree/master/helm/superset)
|
||||
is deprecated and is not recommended for new Kubernetes deployments.
|
||||
:::
|
||||
|
||||
## Running
|
||||
Use the operator documentation as the source of truth for Kubernetes installation and operations.
|
||||
It covers installing the operator, defining Superset deployments with Kubernetes custom resources,
|
||||
configuring dependencies, upgrades, and operational workflows.
|
||||
|
||||
1. Add the Superset helm repository
|
||||
## New Deployments
|
||||
|
||||
```sh
|
||||
helm repo add superset https://apache.github.io/superset
|
||||
"superset" has been added to your repositories
|
||||
```
|
||||
Start with the
|
||||
[Apache Superset Kubernetes Operator documentation](https://apache.github.io/superset-kubernetes-operator/).
|
||||
|
||||
2. View charts in repo
|
||||
## Existing Helm Deployments
|
||||
|
||||
```sh
|
||||
helm search repo superset
|
||||
NAME CHART VERSION APP VERSION DESCRIPTION
|
||||
superset/superset 0.1.1 1.0 Apache Superset is a modern, enterprise-ready b...
|
||||
```
|
||||
Existing Helm chart users should plan a migration to the operator. Follow the
|
||||
[Helm chart migration guide](https://apache.github.io/superset-kubernetes-operator/user-guide/migration/)
|
||||
for the recommended migration path.
|
||||
|
||||
3. Configure your setting overrides
|
||||
Until migration is complete, legacy chart reference material remains available in the deprecated
|
||||
[Helm chart README](https://github.com/apache/superset/tree/master/helm/superset).
|
||||
|
||||
Just like any typical Helm chart, you'll need to craft a `values.yaml` file that would define/override any of the values exposed into the default [values.yaml](https://github.com/apache/superset/tree/master/helm/superset/values.yaml), or from any of the dependent charts it depends on:
|
||||
## Resources
|
||||
|
||||
- [bitnami/redis](https://artifacthub.io/packages/helm/bitnami/redis)
|
||||
- [bitnami/postgresql](https://artifacthub.io/packages/helm/bitnami/postgresql)
|
||||
|
||||
More info down below on some important overrides you might need.
|
||||
|
||||
4. Install and run
|
||||
|
||||
```sh
|
||||
helm upgrade --install --values my-values.yaml superset superset/superset
|
||||
```
|
||||
|
||||
You should see various pods popping up, such as:
|
||||
|
||||
```sh
|
||||
kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
superset-celerybeat-7cdcc9575f-k6xmc 1/1 Running 0 119s
|
||||
superset-f5c9c667-dw9lp 1/1 Running 0 4m7s
|
||||
superset-f5c9c667-fk8bk 1/1 Running 0 4m11s
|
||||
superset-init-db-zlm9z 0/1 Completed 0 111s
|
||||
superset-postgresql-0 1/1 Running 0 6d20h
|
||||
superset-redis-master-0 1/1 Running 0 6d20h
|
||||
superset-worker-75b48bbcc-jmmjr 1/1 Running 0 4m8s
|
||||
superset-worker-75b48bbcc-qrq49 1/1 Running 0 4m12s
|
||||
```
|
||||
|
||||
The exact list will depend on some of your specific configuration overrides but you should generally expect:
|
||||
|
||||
- N `superset-xxxx-yyyy` and `superset-worker-xxxx-yyyy` pods (depending on your `supersetNode.replicaCount` and `supersetWorker.replicaCount` values)
|
||||
- 1 `superset-postgresql-0` depending on your postgres settings
|
||||
- 1 `superset-redis-master-0` depending on your redis settings
|
||||
- 1 `superset-celerybeat-xxxx-yyyy` pod if you have `supersetCeleryBeat.enabled = true` in your values overrides
|
||||
|
||||
1. Access it
|
||||
|
||||
The chart will publish appropriate services to expose the Superset UI internally within your k8s cluster. To access it externally you will have to either:
|
||||
|
||||
- Configure the Service as a `LoadBalancer` or `NodePort`
|
||||
- Set up an `Ingress` for it - the chart includes a definition, but will need to be tuned to your needs (hostname, tls, annotations etc...)
|
||||
- Set up a Gateway API `HTTPRoute` for it - see [Exposing Superset via Gateway API (HTTPRoute)](#exposing-superset-via-gateway-api-httproute) below
|
||||
- Run `kubectl port-forward superset-xxxx-yyyy :8088` to directly tunnel one pod's port into your localhost
|
||||
|
||||
Depending how you configured external access, the URL will vary. Once you've identified the appropriate URL you can log in with:
|
||||
|
||||
- user: `admin`
|
||||
- password: `admin`
|
||||
|
||||
## Important settings
|
||||
|
||||
### Security settings
|
||||
|
||||
Default security settings and passwords are included but you **MUST** update them to run `prod` instances, in particular:
|
||||
|
||||
```yaml
|
||||
postgresql:
|
||||
postgresqlPassword: superset
|
||||
```
|
||||
|
||||
Make sure, you set a unique strong complex alphanumeric string for your SECRET_KEY and use a tool to help you generate
|
||||
a sufficiently random sequence.
|
||||
|
||||
- To generate a good key you can run, `openssl rand -base64 42`
|
||||
|
||||
```yaml
|
||||
configOverrides:
|
||||
secret: |
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
```
|
||||
|
||||
If you want to change the previous secret key then you should rotate the keys.
|
||||
Default secret key for kubernetes deployment is `thisISaSECRET_1234`
|
||||
|
||||
```yaml
|
||||
configOverrides:
|
||||
my_override: |
|
||||
PREVIOUS_SECRET_KEY = 'YOUR_PREVIOUS_SECRET_KEY'
|
||||
SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY'
|
||||
init:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
. {{ .Values.configMountPath }}/superset_bootstrap.sh
|
||||
superset re-encrypt-secrets
|
||||
. {{ .Values.configMountPath }}/superset_init.sh
|
||||
```
|
||||
|
||||
:::note
|
||||
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
|
||||
|
||||
There are two independent telemetry channels:
|
||||
|
||||
- **Image pulls** (Scarf Gateway): to opt out, edit the `repository:` line in your `helm/superset/values.yaml` file, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub.
|
||||
- **The analytics pixel** rendered in the UI: to opt out, set the `SCARF_ANALYTICS` environment variable to `false` on the Superset containers via `extraEnv` in your `values.yaml`:
|
||||
|
||||
```yaml
|
||||
extraEnv:
|
||||
SCARF_ANALYTICS: 'false'
|
||||
```
|
||||
|
||||
This is read at runtime, so it takes effect on the pre-built images without rebuilding the frontend.
|
||||
:::
|
||||
|
||||
### Dependencies
|
||||
|
||||
Install additional packages and do any other bootstrap configuration in the bootstrap script.
|
||||
For production clusters it's recommended to build own image with this step done in CI.
|
||||
|
||||
:::note
|
||||
|
||||
Superset requires a Python DB-API database driver and a SQLAlchemy
|
||||
dialect to be installed for each datastore you want to connect to.
|
||||
|
||||
See [Install Database Drivers](/user-docs/databases#installing-database-drivers) for more information.
|
||||
It is recommended that you refer to versions listed in
|
||||
[pyproject.toml](https://github.com/apache/superset/blob/master/pyproject.toml)
|
||||
instead of hard-coding them in your bootstrap script, as seen below.
|
||||
|
||||
:::
|
||||
|
||||
The following example installs the drivers for BigQuery and Elasticsearch, allowing you to connect to these data sources within your Superset setup:
|
||||
|
||||
```yaml
|
||||
bootstrapScript: |
|
||||
#!/bin/bash
|
||||
uv pip install .[postgres] \
|
||||
.[bigquery] \
|
||||
.[elasticsearch] &&\
|
||||
if [ ! -f ~/bootstrap ]; then echo "Running Superset with uid {{ .Values.runAsUser }}" > ~/bootstrap; fi
|
||||
```
|
||||
|
||||
### superset_config.py
|
||||
|
||||
The default `superset_config.py` is fairly minimal and you will very likely need to extend it. This is done by specifying one or more key/value entries in `configOverrides`, e.g.:
|
||||
|
||||
```yaml
|
||||
configOverrides:
|
||||
my_override: |
|
||||
# This will make sure the redirect_uri is properly computed, even with SSL offloading
|
||||
ENABLE_PROXY_FIX = True
|
||||
FEATURE_FLAGS = {
|
||||
"DYNAMIC_PLUGINS": True
|
||||
}
|
||||
```
|
||||
|
||||
Those will be evaluated as Helm templates and therefore will be able to reference other `values.yaml` variables e.g. `{{ .Values.ingress.hosts[0] }}` will resolve to your ingress external domain.
|
||||
|
||||
The entire `superset_config.py` will be installed as a secret, so it is safe to pass sensitive parameters directly... however it might be more readable to use secret env variables for that.
|
||||
|
||||
Full python files can be provided by running `helm upgrade --install --values my-values.yaml --set-file configOverrides.oauth=set_oauth.py`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Those can be passed as key/values either with `extraEnv` or `extraSecretEnv` if they're sensitive. They can then be referenced from `superset_config.py` using e.g. `os.environ.get("VAR")`.
|
||||
|
||||
```yaml
|
||||
extraEnv:
|
||||
SMTP_HOST: smtp.gmail.com
|
||||
SMTP_USER: user@gmail.com
|
||||
SMTP_PORT: '587'
|
||||
SMTP_MAIL_FROM: user@gmail.com
|
||||
|
||||
extraSecretEnv:
|
||||
SMTP_PASSWORD: xxxx
|
||||
|
||||
configOverrides:
|
||||
smtp: |
|
||||
import ast
|
||||
SMTP_HOST = os.getenv("SMTP_HOST","localhost")
|
||||
SMTP_STARTTLS = ast.literal_eval(os.getenv("SMTP_STARTTLS", "True"))
|
||||
SMTP_SSL = ast.literal_eval(os.getenv("SMTP_SSL", "False"))
|
||||
SMTP_USER = os.getenv("SMTP_USER","superset")
|
||||
SMTP_PORT = os.getenv("SMTP_PORT",25)
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD","superset")
|
||||
```
|
||||
|
||||
### System packages
|
||||
|
||||
If new system packages are required, they can be installed before application startup by overriding the container's `command`, e.g.:
|
||||
|
||||
```yaml
|
||||
supersetWorker:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
apt update
|
||||
apt install -y somepackage
|
||||
apt autoremove -yqq --purge
|
||||
apt clean
|
||||
|
||||
# Run celery worker
|
||||
. {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker
|
||||
```
|
||||
|
||||
### Data sources
|
||||
|
||||
Data source definitions can be automatically declared by providing key/value yaml definitions in `extraConfigs`:
|
||||
|
||||
```yaml
|
||||
extraConfigs:
|
||||
import_datasources.yaml: |
|
||||
databases:
|
||||
- allow_file_upload: true
|
||||
allow_ctas: true
|
||||
allow_cvas: true
|
||||
database_name: example-db
|
||||
extra: "{\r\n \"metadata_params\": {},\r\n \"engine_params\": {},\r\n \"\
|
||||
metadata_cache_timeout\": {},\r\n \"schemas_allowed_for_file_upload\": []\r\n\
|
||||
}"
|
||||
sqlalchemy_uri: example://example-db.local
|
||||
tables: []
|
||||
```
|
||||
|
||||
Those will also be mounted as secrets and can include sensitive parameters.
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Setting up OAuth
|
||||
|
||||
:::note
|
||||
|
||||
OAuth setup requires that the [authlib](https://authlib.org/) Python library is installed. This can
|
||||
be done using `pip` by updating the `bootstrapScript`. See the [Dependencies](#dependencies) section
|
||||
for more information.
|
||||
|
||||
:::
|
||||
|
||||
```yaml
|
||||
extraEnv:
|
||||
AUTH_DOMAIN: example.com
|
||||
|
||||
extraSecretEnv:
|
||||
GOOGLE_KEY: xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
|
||||
GOOGLE_SECRET: xxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
configOverrides:
|
||||
enable_oauth: |
|
||||
# This will make sure the redirect_uri is properly computed, even with SSL offloading
|
||||
ENABLE_PROXY_FIX = True
|
||||
|
||||
from flask_appbuilder.security.manager import AUTH_OAUTH
|
||||
AUTH_TYPE = AUTH_OAUTH
|
||||
OAUTH_PROVIDERS = [
|
||||
{
|
||||
"name": "google",
|
||||
"icon": "fa-google",
|
||||
"token_key": "access_token",
|
||||
"remote_app": {
|
||||
"client_id": os.getenv("GOOGLE_KEY"),
|
||||
"client_secret": os.getenv("GOOGLE_SECRET"),
|
||||
"api_base_url": "https://www.googleapis.com/oauth2/v2/",
|
||||
"client_kwargs": {"scope": "email profile"},
|
||||
"request_token_url": None,
|
||||
"access_token_url": "https://accounts.google.com/o/oauth2/token",
|
||||
"authorize_url": "https://accounts.google.com/o/oauth2/auth",
|
||||
"authorize_params": {"hd": os.getenv("AUTH_DOMAIN", "")}
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# Map Authlib roles to superset roles
|
||||
AUTH_ROLE_ADMIN = 'Admin'
|
||||
AUTH_ROLE_PUBLIC = 'Public'
|
||||
|
||||
# Will allow user self registration, allowing to create Flask users from Authorized User
|
||||
AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
AUTH_USER_REGISTRATION_ROLE = "Admin"
|
||||
```
|
||||
|
||||
### Exposing Superset via Gateway API (HTTPRoute)
|
||||
|
||||
As an alternative to `Ingress`, the chart can create a [Gateway API](https://gateway-api.sigs.k8s.io/)
|
||||
`HTTPRoute` that attaches to a Gateway already running in your cluster. This requires the Gateway
|
||||
API CRDs serving the configured `httproute.apiVersion` (`gateway.networking.k8s.io/v1` by default)
|
||||
to be installed, along with a Gateway resource for the route to attach to. If the Gateway lives in
|
||||
a different namespace than the `HTTPRoute` (as in the
|
||||
example below), its listener's `allowedRoutes` must explicitly permit routes from this release's
|
||||
namespace, or the `HTTPRoute` will install successfully but never attach.
|
||||
|
||||
```yaml
|
||||
httproute:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
namespace: gateway-system
|
||||
hostnames:
|
||||
- superset.example.com
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
```
|
||||
|
||||
- `httproute.parentRefs` lists the Gateway(s) the route attaches to.
|
||||
- `httproute.hostnames` matches against the HTTP `Host` header; it's templated, so values like
|
||||
`{{ .Release.Name }}` can be used.
|
||||
- `httproute.rules` are routing rules backed by the Superset service; each rule accepts standard
|
||||
`matches`, `filters`, and `timeouts` fields, and an optional `weight` (defaults to `1`) applied to
|
||||
its single backend reference. Since each rule maps to one backend, `weight` has no traffic-splitting
|
||||
effect here; it only matters if you fork the template to add multiple `backendRefs` to a rule.
|
||||
`timeouts` only joined the Gateway API Standard channel in v1.2, so it requires both v1.2+ CRDs
|
||||
and a supporting controller; drop it if either predates that.
|
||||
- If `supersetWebsockets.enabled` is set, an extra rule routing `supersetWebsockets.ingress.path`
|
||||
(default `/ws`) to the `-ws` service is appended automatically, mirroring the `Ingress` behavior.
|
||||
WebSocket upgrade support is controller-dependent under Gateway API; check your Gateway
|
||||
implementation's docs in case it needs an explicit protocol opt-in for global async queries to
|
||||
keep working behind a Gateway.
|
||||
- If `supersetMcp.enabled` and `supersetMcp.httproute.enabled` are both set, an extra rule routing
|
||||
`supersetMcp.httproute.path` to the `-mcp` service is appended as well. Don't expose this route
|
||||
without first enabling MCP authentication — see the
|
||||
[MCP Server Deployment & Authentication](/admin-docs/configuration/mcp-server#authentication) doc;
|
||||
by default the MCP server runs in dev mode with auth disabled.
|
||||
- Set `httproute.apiVersion` to `gateway.networking.k8s.io/v1beta1` if your cluster's Gateway API
|
||||
installation hasn't promoted `HTTPRoute` to `v1` yet.
|
||||
|
||||
### Enable Alerts and Reports
|
||||
|
||||
For this, as per the [Alerts and Reports doc](/admin-docs/configuration/alerts-reports), you will need to:
|
||||
|
||||
#### Install a supported webdriver in the Celery worker
|
||||
|
||||
This is done either by using a custom image that has the webdriver pre-installed, or installing at startup time by overriding the `command`. Here's a working example for `chromedriver`:
|
||||
|
||||
```yaml
|
||||
supersetWorker:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
# Install chrome webdriver
|
||||
# See https://github.com/apache/superset/blob/4fa3b6c7185629b87c27fc2c0e5435d458f7b73d/docs/src/pages/admin-docs/installation/email_reports.mdx
|
||||
apt-get update
|
||||
apt-get install -y wget
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt-get install -y --no-install-recommends ./google-chrome-stable_current_amd64.deb
|
||||
wget https://chromedriver.storage.googleapis.com/88.0.4324.96/chromedriver_linux64.zip
|
||||
apt-get install -y zip
|
||||
unzip chromedriver_linux64.zip
|
||||
chmod +x chromedriver
|
||||
mv chromedriver /usr/bin
|
||||
apt-get autoremove -yqq --purge
|
||||
apt-get clean
|
||||
rm -f google-chrome-stable_current_amd64.deb chromedriver_linux64.zip
|
||||
|
||||
# Run
|
||||
. {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker
|
||||
```
|
||||
|
||||
#### Run the Celery beat
|
||||
|
||||
This pod will trigger the scheduled tasks configured in the alerts and reports UI section:
|
||||
|
||||
```yaml
|
||||
supersetCeleryBeat:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
#### Configure the appropriate Celery jobs and SMTP/Slack settings
|
||||
|
||||
```yaml
|
||||
extraEnv:
|
||||
SMTP_HOST: smtp.gmail.com
|
||||
SMTP_USER: user@gmail.com
|
||||
SMTP_PORT: '587'
|
||||
SMTP_MAIL_FROM: user@gmail.com
|
||||
|
||||
extraSecretEnv:
|
||||
SLACK_API_TOKEN: xoxb-xxxx-yyyy
|
||||
SMTP_PASSWORD: xxxx-yyyy
|
||||
|
||||
configOverrides:
|
||||
feature_flags: |
|
||||
import ast
|
||||
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True
|
||||
}
|
||||
|
||||
SMTP_HOST = os.getenv("SMTP_HOST","localhost")
|
||||
SMTP_STARTTLS = ast.literal_eval(os.getenv("SMTP_STARTTLS", "True"))
|
||||
SMTP_SSL = ast.literal_eval(os.getenv("SMTP_SSL", "False"))
|
||||
SMTP_USER = os.getenv("SMTP_USER","superset")
|
||||
SMTP_PORT = os.getenv("SMTP_PORT",25)
|
||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD","superset")
|
||||
SMTP_MAIL_FROM = os.getenv("SMTP_MAIL_FROM","superset@superset.com")
|
||||
|
||||
SLACK_API_TOKEN = os.getenv("SLACK_API_TOKEN",None)
|
||||
celery_conf: |
|
||||
from celery.schedules import crontab
|
||||
|
||||
class CeleryConfig:
|
||||
broker_url = f"redis://{env('REDIS_HOST')}:{env('REDIS_PORT')}/0"
|
||||
imports = (
|
||||
"superset.sql_lab",
|
||||
"superset.tasks.cache",
|
||||
"superset.tasks.scheduler",
|
||||
)
|
||||
result_backend = f"redis://{env('REDIS_HOST')}:{env('REDIS_PORT')}/0"
|
||||
task_annotations = {
|
||||
"sql_lab.get_sql_results": {
|
||||
"rate_limit": "100/s",
|
||||
},
|
||||
}
|
||||
beat_schedule = {
|
||||
"reports.scheduler": {
|
||||
"task": "reports.scheduler",
|
||||
"schedule": crontab(minute="*", hour="*"),
|
||||
},
|
||||
"reports.prune_log": {
|
||||
"task": "reports.prune_log",
|
||||
'schedule': crontab(minute=0, hour=0),
|
||||
},
|
||||
'cache-warmup-hourly': {
|
||||
"task": "cache-warmup",
|
||||
"schedule": crontab(minute="*/30", hour="*"),
|
||||
"kwargs": {
|
||||
"strategy_name": "top_n_dashboards",
|
||||
"top_n": 10,
|
||||
"since": "7 days ago",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
CELERY_CONFIG = CeleryConfig
|
||||
reports: |
|
||||
EMAIL_PAGE_RENDER_WAIT = 60
|
||||
WEBDRIVER_BASEURL = "http://{{ template "superset.fullname" . }}:{{ .Values.service.port }}/"
|
||||
WEBDRIVER_BASEURL_USER_FRIENDLY = "https://www.example.com/"
|
||||
WEBDRIVER_TYPE= "chrome"
|
||||
WEBDRIVER_OPTION_ARGS = [
|
||||
"--force-device-scale-factor=2.0",
|
||||
"--high-dpi-support=2.0",
|
||||
"--headless",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
# This is required because our process runs as root (in order to install pip packages)
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-extensions",
|
||||
]
|
||||
```
|
||||
|
||||
### Load the Examples data and dashboards
|
||||
|
||||
If you are trying Superset out and want some data and dashboards to explore, you can load some examples by creating a `my_values.yaml` and deploying it as described above in the **Configure your setting overrides** step of the **Running** section.
|
||||
To load the examples, add the following to the `my_values.yaml` file:
|
||||
|
||||
```yaml
|
||||
init:
|
||||
loadExamples: true
|
||||
```
|
||||
|
||||
:::resources
|
||||
|
||||
- [Tutorial: Mastering Data Visualization — Installing Superset on Kubernetes with Helm Chart](https://mahira-technology.medium.com/mastering-data-visualization-installing-superset-on-kubernetes-cluster-using-helm-chart-e4ec99199e1e)
|
||||
- [Tutorial: Installing Apache Superset in Kubernetes](https://aws.plainenglish.io/installing-apache-superset-in-kubernetes-1aec192ac495)
|
||||
:::
|
||||
- [Apache Superset Kubernetes Operator documentation](https://apache.github.io/superset-kubernetes-operator/)
|
||||
- [Apache Superset Kubernetes Operator repository](https://github.com/apache/superset-kubernetes-operator)
|
||||
- [Helm chart migration guide](https://apache.github.io/superset-kubernetes-operator/user-guide/migration/)
|
||||
|
||||
@@ -99,11 +99,16 @@ Affecting the Docker build process:
|
||||
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
|
||||
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
|
||||
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
|
||||
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
|
||||
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`. Once the example
|
||||
data and dashboards are present in the databases, later `docker compose up` runs skip loading
|
||||
them; run `SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up` to reload the examples anyway.
|
||||
- **SUPERSET_LOG_LEVEL (default=info)**: Can be set to debug, info, warning, error, critical
|
||||
for more verbose logging
|
||||
- **SUPERSET_DEBUG_ENABLED (default=false)**: Enable Werkzeug debugger with interactive console.
|
||||
Set to `true` for debugging: `SUPERSET_DEBUG_ENABLED=true docker compose up`
|
||||
- **DISABLE_TS_CHECKER (default=true)**: whether the `superset-node` webpack dev server skips
|
||||
TypeScript type-checking, which speeds up rebuilds and saves several GB of memory. Set to
|
||||
`false` to have webpack surface type errors during development.
|
||||
|
||||
For more env vars that affect your configuration, see this
|
||||
[superset_config.py](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py)
|
||||
|
||||
@@ -93,7 +93,7 @@ Look through the GitHub issues. Issues tagged with
|
||||
|
||||
Superset could always use better documentation,
|
||||
whether as part of the official Superset docs,
|
||||
in docstrings, `docs/*.rst` or even on the web as blog posts or
|
||||
in docstrings, Markdown files in `docs/`, or even on the web as blog posts or
|
||||
articles. See [Documentation](./howtos.md#contributing-to-documentation) for more details.
|
||||
|
||||
### Add Translations
|
||||
|
||||
+1
-1
@@ -414,7 +414,7 @@ This can be used, for example, to convert UTC time to local time.
|
||||
Superset uses [Scarf](https://about.scarf.sh/) by default to collect basic telemetry data upon installing and/or running Superset. This data helps the maintainers of Superset better understand which versions of Superset are being used, in order to prioritize patch/minor releases and security fixes.
|
||||
We use the [Scarf Gateway](https://docs.scarf.sh/gateway/) to sit in front of container registries, the [scarf-js](https://about.scarf.sh/package-sdks) package to track `npm` installations, and a Scarf pixel to gather anonymous analytics on Superset page views.
|
||||
Scarf purges PII and provides aggregated statistics. Superset users can easily opt out of analytics in various ways documented [here](https://docs.scarf.sh/gateway/#do-not-track) and [here](https://docs.scarf.sh/package-analytics/#as-a-user-of-a-package-using-scarf-js-how-can-i-opt-out-of-analytics).
|
||||
You can also opt out of the analytics pixel by setting the `SCARF_ANALYTICS` environment variable to `false`. This is read at runtime, so setting it on the Superset container (for example via `extraEnv` in the Helm chart, or `docker/.env` for Docker Compose) disables the pixel on the pre-built images without rebuilding the frontend. Note that this only disables the page-view pixel; the Scarf Gateway (container registry) and `scarf-js` (`npm`) channels are opted out separately, as described above.
|
||||
You can also opt out of the analytics pixel by setting the `SCARF_ANALYTICS` environment variable to `false`. This is read at runtime, so setting it on the Superset container (for example via your Kubernetes deployment configuration, or `docker/.env` for Docker Compose) disables the pixel on the pre-built images without rebuilding the frontend. Note that this only disables the page-view pixel; the Scarf Gateway (container registry) and `scarf-js` (`npm`) channels are opted out separately, as described above.
|
||||
Additional opt-out instructions are available on the [Docker Compose](/admin-docs/installation/docker-compose) and [Kubernetes](/admin-docs/installation/kubernetes) installation pages.
|
||||
|
||||
## Does Superset have an archive panel or trash bin from which a user can recover deleted assets?
|
||||
|
||||
+1
-1
@@ -260,7 +260,7 @@ Understanding the Superset Points of View
|
||||
- Deploying Superset
|
||||
|
||||
- [Official Docker image](https://hub.docker.com/r/apache/superset)
|
||||
- [Helm Chart](https://github.com/apache/superset/tree/master/helm/superset)
|
||||
- [Kubernetes Operator](https://apache.github.io/superset-kubernetes-operator/)
|
||||
|
||||
- Recordings of Past [Superset Community Events](https://preset.io/events)
|
||||
|
||||
|
||||
@@ -342,6 +342,12 @@ the median value within the seven daily data points. For more information on the
|
||||
various options in this section, refer to the
|
||||
[Pandas documentation](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.resample.html).
|
||||
|
||||
By default, resampling only fills the periods between the first and the last data point returned by
|
||||
the query. If your data starts after the beginning of the selected time range, or stops before its
|
||||
end, enable **Fill the entire time range** to pad the series so it spans the whole range. This is
|
||||
what you want when, for example, you chart hourly counts with Zero imputation and expect empty hours
|
||||
to be drawn as zero across the entire day rather than only around the hours that have data.
|
||||
|
||||
Lastly, save your chart as Tutorial Resample and add it to the Tutorial Dashboard. Go to the
|
||||
tutorial dashboard to see the four charts side by side and compare the different outputs.
|
||||
|
||||
|
||||
@@ -269,26 +269,28 @@ Ask your admin for the MCP server URL and any authentication tokens you need.
|
||||
|
||||
### Charts
|
||||
|
||||
| Tool | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list_charts` | List charts with filtering and search |
|
||||
| `get_chart_info` | Get chart metadata and configuration |
|
||||
| `get_chart_data` | Retrieve chart data (JSON, CSV, or Excel) |
|
||||
| `get_chart_preview` | Generate a chart preview (URL, ASCII, table, or Vega-Lite) |
|
||||
| `get_chart_type_schema` | Get the configuration schema for a chart type |
|
||||
| `generate_chart` | Create a new chart from a specification (defaults to preview mode — review before saving) |
|
||||
| `update_chart` | Modify an existing chart's configuration (pass `generate_preview=False` to persist immediately instead of returning a preview URL) |
|
||||
| `update_chart_preview` | Update a cached chart preview without saving |
|
||||
| `generate_explore_link` | Generate an Explore URL for interactive visualization |
|
||||
| Tool | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list_charts` | List charts with filtering and search |
|
||||
| `get_chart_info` | Get chart metadata and configuration |
|
||||
| `get_chart_data` | Retrieve chart data (JSON, CSV, or Excel) |
|
||||
| `get_chart_preview` | Generate a chart preview (URL, ASCII, table, or Vega-Lite) |
|
||||
| `get_chart_type_schema` | Get the configuration schema for a chart type |
|
||||
| `generate_chart` | Create a new chart from a specification (defaults to preview mode — review before saving) |
|
||||
| `update_chart` | Modify an existing chart's configuration (pass `generate_preview=False` to persist immediately instead of returning a preview URL) |
|
||||
| `update_chart_preview` | Update a cached chart preview without saving |
|
||||
| `generate_explore_link` | Generate an Explore URL for interactive visualization |
|
||||
| `delete_chart` | Delete a chart by ID or UUID (soft-deletes to trash when `SOFT_DELETE` is enabled; fails if alerts/reports are still attached, checked before editorship; otherwise returns `permission_denied` if the caller isn't an editor of the chart — owners, Admins, and explicitly granted editors qualify) |
|
||||
|
||||
### Dashboards
|
||||
|
||||
| Tool | Description |
|
||||
| --------------------------------- | -------------------------------------------- |
|
||||
| `list_dashboards` | List dashboards with filtering and search |
|
||||
| `get_dashboard_info` | Get dashboard metadata and layout |
|
||||
| `generate_dashboard` | Create a new dashboard with specified charts |
|
||||
| `add_chart_to_existing_dashboard` | Add a chart to an existing dashboard |
|
||||
| Tool | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `list_dashboards` | List dashboards with filtering and search |
|
||||
| `get_dashboard_info` | Get dashboard metadata and layout |
|
||||
| `generate_dashboard` | Create a new dashboard with specified charts |
|
||||
| `add_chart_to_existing_dashboard` | Add a chart to an existing dashboard |
|
||||
| `delete_dashboard` | Delete a dashboard by ID, UUID, or slug; leaves its charts intact (soft-deletes to trash when `SOFT_DELETE` is enabled; fails if alerts/reports are still attached, checked before editorship; otherwise returns `permission_denied` if the caller isn't an editor of the dashboard — owners, Admins, and explicitly granted editors qualify) |
|
||||
|
||||
### SQL
|
||||
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"oxfmt": "^0.66.0",
|
||||
"oxlint": "^1.80.0",
|
||||
"oxlint": "^1.81.0",
|
||||
"oxlint-tsgolint": "^7.0.2001",
|
||||
"typescript": "7.0.2",
|
||||
"webpack": "^5.110.3"
|
||||
|
||||
@@ -3215,7 +3215,7 @@
|
||||
"logo": "cockroachdb.png",
|
||||
"homepage_url": "https://www.cockroachlabs.com/",
|
||||
"categories": ["Traditional RDBMS", "Open Source"],
|
||||
"pypi_packages": ["cockroachdb"],
|
||||
"pypi_packages": ["sqlalchemy-cockroachdb", "psycopg2-binary"],
|
||||
"connection_string": "cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable",
|
||||
"default_port": 26257,
|
||||
"docs_url": "https://github.com/cockroachdb/sqlalchemy-cockroachdb",
|
||||
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@ Superset is designed for data exploration and visualization at scale. It feature
|
||||
- [Architecture](https://superset.apache.org/docs/installation/architecture): Production deployment architecture and components
|
||||
- [Docker Compose](https://superset.apache.org/docs/installation/docker-compose): Install Superset using Docker Compose
|
||||
- [Docker Builds](https://superset.apache.org/docs/installation/docker-builds): Building and customizing Docker images
|
||||
- [Kubernetes](https://superset.apache.org/docs/installation/kubernetes): Deploy Superset on Kubernetes with Helm
|
||||
- [Kubernetes](https://superset.apache.org/docs/installation/kubernetes): Deploy Superset on Kubernetes with the official Kubernetes Operator
|
||||
- [PyPI](https://superset.apache.org/docs/installation/pypi): Install from PyPI using pip
|
||||
- [Upgrading Superset](https://superset.apache.org/docs/installation/upgrading-superset): Upgrade between Superset versions
|
||||
|
||||
|
||||
Vendored
+73
@@ -6050,6 +6050,10 @@
|
||||
"description": "Chart count",
|
||||
"type": "integer"
|
||||
},
|
||||
"restricted_count": {
|
||||
"description": "Charts the current user cannot access",
|
||||
"type": "integer"
|
||||
},
|
||||
"result": {
|
||||
"description": "A list of dashboards",
|
||||
"items": {
|
||||
@@ -6083,6 +6087,10 @@
|
||||
"description": "Dashboard count",
|
||||
"type": "integer"
|
||||
},
|
||||
"restricted_count": {
|
||||
"description": "Dashboards the current user cannot access",
|
||||
"type": "integer"
|
||||
},
|
||||
"result": {
|
||||
"description": "A list of dashboards",
|
||||
"items": {
|
||||
@@ -14089,6 +14097,17 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"get_related_objects_ids_schema": {
|
||||
"example": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"get_related_schema": {
|
||||
"properties": {
|
||||
"filter": {
|
||||
@@ -23633,6 +23652,60 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/dataset/related_objects/": {
|
||||
"get": {
|
||||
"description": "Aggregates the charts built on any of the requested datasets and the dashboards those charts appear on. Each chart and dashboard is listed once even if it depends on several of the datasets. Requested datasets the user cannot see are ignored; the response is 404 only when none of them are visible.",
|
||||
"parameters": [
|
||||
{
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/get_related_objects_ids_schema"
|
||||
}
|
||||
}
|
||||
},
|
||||
"in": "query",
|
||||
"name": "q"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DatasetRelatedObjectsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Query result"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/400"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/404"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/500"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"jwt": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get charts and dashboards associated to multiple datasets",
|
||||
"tags": [
|
||||
"Datasets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/dataset/warm_up_cache": {
|
||||
"put": {
|
||||
"description": "Warms up the cache for the table. Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON encoded `logs.json` column associated with the `explore` action.",
|
||||
|
||||
+99
-99
@@ -3203,100 +3203,100 @@
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz#814bcdd2707fa8ab1ae0f0b51a7243b034d2833a"
|
||||
integrity sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==
|
||||
|
||||
"@oxlint/binding-android-arm-eabi@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz#924b041cbcea4e934fd9ef66a2d2b7d7463c0180"
|
||||
integrity sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==
|
||||
"@oxlint/binding-android-arm-eabi@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz#86a0305480e680431429c8259c5701ac5592e4a7"
|
||||
integrity sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==
|
||||
|
||||
"@oxlint/binding-android-arm64@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz#d1716d2be903de06b4fdb2fdaa121a2943695c35"
|
||||
integrity sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==
|
||||
"@oxlint/binding-android-arm64@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz#dba51cb2a1258f37eaca15a4e74d5d6ee19f238c"
|
||||
integrity sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==
|
||||
|
||||
"@oxlint/binding-darwin-arm64@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz#475eb4061db4d4fe9349e92f15962c0203e8ebec"
|
||||
integrity sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==
|
||||
"@oxlint/binding-darwin-arm64@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz#f384ae1eddd399429281f88a0073729e287ec0b1"
|
||||
integrity sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==
|
||||
|
||||
"@oxlint/binding-darwin-x64@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz#602fa5681dd746c0fb5ca7a0e4a81d0dd07b7f90"
|
||||
integrity sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==
|
||||
"@oxlint/binding-darwin-x64@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz#7167e3b4c18852fcd1ddade536d232d73d55d30c"
|
||||
integrity sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==
|
||||
|
||||
"@oxlint/binding-freebsd-x64@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz#4e0490c344726fd0b1a027afc129fb9c78f069ec"
|
||||
integrity sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==
|
||||
"@oxlint/binding-freebsd-x64@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz#64f230765343bfe7e2280b071bb191bb666303ac"
|
||||
integrity sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz#82d215fc05e046a0ec085821c1da8ab99d35fbdc"
|
||||
integrity sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==
|
||||
"@oxlint/binding-linux-arm-gnueabihf@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz#5095f021a28009146f7536a7ccd518cdfcbfc3bf"
|
||||
integrity sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz#92068e3b51cd50fc5a83a3ebfba7925313d6cd11"
|
||||
integrity sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==
|
||||
"@oxlint/binding-linux-arm-musleabihf@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz#d894dcfb6fb1b8b224b79e6ce07b17403f413749"
|
||||
integrity sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz#6c81ddc85dd5b79070f87401d61813172068666d"
|
||||
integrity sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==
|
||||
"@oxlint/binding-linux-arm64-gnu@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz#bf8e362c87e7828ba28c2ec36bae73ad82989ef1"
|
||||
integrity sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz#11216704b606d67e946a868850243dc2739ec92e"
|
||||
integrity sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==
|
||||
"@oxlint/binding-linux-arm64-musl@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz#2aa5214eb5b032e8f77d94a27c9bd3f6dbc446f3"
|
||||
integrity sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz#125302a0e6732e32e4a7d53f0212d58f1b13af01"
|
||||
integrity sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==
|
||||
"@oxlint/binding-linux-ppc64-gnu@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz#a0af1af6fd309214555f33b4fa0432f3ec45f242"
|
||||
integrity sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz#03846614f184ed3dcbab4bffad7417fd54cd0967"
|
||||
integrity sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==
|
||||
"@oxlint/binding-linux-riscv64-gnu@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz#60413a335a63e0911a625d047690e41c03f2a833"
|
||||
integrity sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz#49e0fb90e1c8358429b9989a407f4020c56d8ea9"
|
||||
integrity sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==
|
||||
"@oxlint/binding-linux-riscv64-musl@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz#5f1dd7bc2bdbe92fa5454df081ee91fa2c499948"
|
||||
integrity sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz#7aee2ae2426f7bdcae77969073a015a6b3f8373e"
|
||||
integrity sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==
|
||||
"@oxlint/binding-linux-s390x-gnu@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz#830c5bdd145ad39c9b5371e3214d0a9f9c1f46b1"
|
||||
integrity sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz#0c850b00faed2f884cf8665fe9c391f9f653d6ca"
|
||||
integrity sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==
|
||||
"@oxlint/binding-linux-x64-gnu@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz#7f72ea3c9ae70a76c1a92e56a5755869f335e756"
|
||||
integrity sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==
|
||||
|
||||
"@oxlint/binding-linux-x64-musl@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz#91f54f1b0cc93a7e75ce4118aa85f9fd112f25c0"
|
||||
integrity sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==
|
||||
"@oxlint/binding-linux-x64-musl@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz#758fd6ea69123c6f4c1ba160bfc28f6b84548c11"
|
||||
integrity sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==
|
||||
|
||||
"@oxlint/binding-openharmony-arm64@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz#7ab37c21e547812177bffdbd97c8a34493eedd7b"
|
||||
integrity sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==
|
||||
"@oxlint/binding-openharmony-arm64@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz#e492b7a1875b70ea55ffe13f685d4302e6b1900e"
|
||||
integrity sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz#5a470fa82339044ad9cceef2bb8c62d88695e394"
|
||||
integrity sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==
|
||||
"@oxlint/binding-win32-arm64-msvc@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz#9247fdc1b4c86ecc718747c5f8c68902969677d1"
|
||||
integrity sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz#06f6559998f1a53a8f5ace78d91f217a54f6a963"
|
||||
integrity sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==
|
||||
"@oxlint/binding-win32-ia32-msvc@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz#dfa4eed78612c5f4df788eb056fd5d1b05a75bb1"
|
||||
integrity sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc@1.80.0":
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz#d3abbf1a7a09b9039ca5ca570c9689908850efc4"
|
||||
integrity sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==
|
||||
"@oxlint/binding-win32-x64-msvc@1.81.0":
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz#27a474cc288e47a0e5c2d9c922bee64de0cfd614"
|
||||
integrity sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==
|
||||
|
||||
"@parcel/watcher-android-arm64@2.5.6":
|
||||
version "2.5.6"
|
||||
@@ -11580,30 +11580,30 @@ oxlint-tsgolint@^7.0.2001:
|
||||
"@oxlint-tsgolint/win32-arm64" "7.0.2001"
|
||||
"@oxlint-tsgolint/win32-x64" "7.0.2001"
|
||||
|
||||
oxlint@^1.80.0:
|
||||
version "1.80.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.80.0.tgz#228271087d3f04e391e383ccdc0e840458d8b653"
|
||||
integrity sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==
|
||||
oxlint@^1.81.0:
|
||||
version "1.81.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.81.0.tgz#7b20ada29a171883de4517d041ea5b057fb48ab5"
|
||||
integrity sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==
|
||||
optionalDependencies:
|
||||
"@oxlint/binding-android-arm-eabi" "1.80.0"
|
||||
"@oxlint/binding-android-arm64" "1.80.0"
|
||||
"@oxlint/binding-darwin-arm64" "1.80.0"
|
||||
"@oxlint/binding-darwin-x64" "1.80.0"
|
||||
"@oxlint/binding-freebsd-x64" "1.80.0"
|
||||
"@oxlint/binding-linux-arm-gnueabihf" "1.80.0"
|
||||
"@oxlint/binding-linux-arm-musleabihf" "1.80.0"
|
||||
"@oxlint/binding-linux-arm64-gnu" "1.80.0"
|
||||
"@oxlint/binding-linux-arm64-musl" "1.80.0"
|
||||
"@oxlint/binding-linux-ppc64-gnu" "1.80.0"
|
||||
"@oxlint/binding-linux-riscv64-gnu" "1.80.0"
|
||||
"@oxlint/binding-linux-riscv64-musl" "1.80.0"
|
||||
"@oxlint/binding-linux-s390x-gnu" "1.80.0"
|
||||
"@oxlint/binding-linux-x64-gnu" "1.80.0"
|
||||
"@oxlint/binding-linux-x64-musl" "1.80.0"
|
||||
"@oxlint/binding-openharmony-arm64" "1.80.0"
|
||||
"@oxlint/binding-win32-arm64-msvc" "1.80.0"
|
||||
"@oxlint/binding-win32-ia32-msvc" "1.80.0"
|
||||
"@oxlint/binding-win32-x64-msvc" "1.80.0"
|
||||
"@oxlint/binding-android-arm-eabi" "1.81.0"
|
||||
"@oxlint/binding-android-arm64" "1.81.0"
|
||||
"@oxlint/binding-darwin-arm64" "1.81.0"
|
||||
"@oxlint/binding-darwin-x64" "1.81.0"
|
||||
"@oxlint/binding-freebsd-x64" "1.81.0"
|
||||
"@oxlint/binding-linux-arm-gnueabihf" "1.81.0"
|
||||
"@oxlint/binding-linux-arm-musleabihf" "1.81.0"
|
||||
"@oxlint/binding-linux-arm64-gnu" "1.81.0"
|
||||
"@oxlint/binding-linux-arm64-musl" "1.81.0"
|
||||
"@oxlint/binding-linux-ppc64-gnu" "1.81.0"
|
||||
"@oxlint/binding-linux-riscv64-gnu" "1.81.0"
|
||||
"@oxlint/binding-linux-riscv64-musl" "1.81.0"
|
||||
"@oxlint/binding-linux-s390x-gnu" "1.81.0"
|
||||
"@oxlint/binding-linux-x64-gnu" "1.81.0"
|
||||
"@oxlint/binding-linux-x64-musl" "1.81.0"
|
||||
"@oxlint/binding-openharmony-arm64" "1.81.0"
|
||||
"@oxlint/binding-win32-arm64-msvc" "1.81.0"
|
||||
"@oxlint/binding-win32-ia32-msvc" "1.81.0"
|
||||
"@oxlint/binding-win32-x64-msvc" "1.81.0"
|
||||
|
||||
p-cancelable@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
apiVersion: v2
|
||||
deprecated: true
|
||||
appVersion: "6.1.0"
|
||||
description: Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
name: superset
|
||||
@@ -25,11 +26,7 @@ keywords:
|
||||
- data science
|
||||
sources:
|
||||
- https://github.com/apache/superset
|
||||
maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.22.7 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.22.8 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
+10
-1
@@ -23,7 +23,9 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||
> **:exclamation: This Helm Chart is deprecated!**
|
||||
|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -33,6 +35,13 @@ Apache Superset is a modern, enterprise-ready business intelligence web applicat
|
||||
|
||||
* <https://github.com/apache/superset>
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
> [!WARNING]
|
||||
> The Superset Helm chart is deprecated. For new Superset deployments on Kubernetes, use the official [Apache Superset Kubernetes Operator](https://github.com/apache/superset-kubernetes-operator) instead.
|
||||
>
|
||||
> Existing Helm chart users should plan a migration to the operator. Start with the [operator documentation](https://apache.github.io/superset-kubernetes-operator/) and the [Helm chart migration guide](https://apache.github.io/superset-kubernetes-operator/user-guide/migration/).
|
||||
|
||||
## TL;DR
|
||||
|
||||
```console
|
||||
|
||||
@@ -32,6 +32,13 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
> [!WARNING]
|
||||
> The Superset Helm chart is deprecated. For new Superset deployments on Kubernetes, use the official [Apache Superset Kubernetes Operator](https://github.com/apache/superset-kubernetes-operator) instead.
|
||||
>
|
||||
> Existing Helm chart users should plan a migration to the operator. Start with the [operator documentation](https://apache.github.io/superset-kubernetes-operator/) and the [Helm chart migration guide](https://apache.github.io/superset-kubernetes-operator/user-guide/migration/).
|
||||
|
||||
## TL;DR
|
||||
|
||||
```console
|
||||
|
||||
+30
-7
@@ -151,10 +151,9 @@ clickhouse = ["clickhouse-connect>=1.7.2, <2.0"]
|
||||
# 2.0). sqlalchemy-cockroachdb is the actively maintained replacement,
|
||||
# already linked from CockroachDbEngineSpec.metadata's docs_url, and
|
||||
# registers the same `cockroachdb` SQLAlchemy dialect entry point.
|
||||
# sqlalchemy-cockroachdb depends only on SQLAlchemy itself, not on a DBAPI
|
||||
# driver, so psycopg2-binary is pinned alongside it (matching the `postgres`
|
||||
# extra) to keep this extra self-contained -- CockroachDB speaks the
|
||||
# PostgreSQL wire protocol, so psycopg2 is what actually opens connections.
|
||||
# sqlalchemy-cockroachdb itself declares no DBAPI dependency (its own docs
|
||||
# require picking one), so pull in the same psycopg2-binary pin as the
|
||||
# `postgres` extra -- CockroachDB speaks the Postgres wire protocol.
|
||||
cockroachdb = ["sqlalchemy-cockroachdb>=2.0.0, <3", "psycopg2-binary==2.9.12"]
|
||||
crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
|
||||
# sqlalchemy-d1's only release (0.1.0, Nov 2025) pins sqlalchemy<2,>=1.4,
|
||||
@@ -199,7 +198,11 @@ excel = ["xlrd>=2.0.2, <2.1"]
|
||||
excel-export = ["boto3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.4.7,<4.0",
|
||||
"mcp>=1.29.1,<3.0",
|
||||
# fastmcp 3.x itself requires mcp<2.0; the upper bound here was looser
|
||||
# than that until Dependabot proposed mcp 2.1.1 and the resolver caught
|
||||
# the conflict (#43876). Capped to match what fastmcp 3.x actually
|
||||
# supports until this extra moves to fastmcp 4.x, which accepts mcp 2.x.
|
||||
"mcp>=1.29.1,<2.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
# it, the middleware falls back to a coarser character-based
|
||||
# heuristic that under-counts JSON-heavy MCP responses.
|
||||
@@ -226,6 +229,11 @@ impala = ["impyla>=0.24.0, <0.25"]
|
||||
# superset/db_engine_specs/kusto.py's known_incompatibilities metadata.
|
||||
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
|
||||
kylin = ["kylinpy>=2.8.4, <2.9"]
|
||||
# MariaDB is a MySQL fork implementing the same wire protocol - connects via
|
||||
# the plain mysql dialect, same driver as mysql.
|
||||
mariadb = ["apache-superset[mysql]"]
|
||||
monetdb = ["sqlalchemy-monetdb>=2.1.0, <3", "pymonetdb>=1.9.1, <2"]
|
||||
mongodb = ["pymongosql>=0.7.3, <1"]
|
||||
mssql = ["pymssql>=2.3.13, <3"]
|
||||
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
|
||||
motherduck = ["apache-superset[duckdb]"]
|
||||
@@ -274,18 +282,33 @@ tdengine = [
|
||||
"taos-ws-py>=0.7.0"
|
||||
]
|
||||
teradata = ["teradatasql>=20.0.0.67"]
|
||||
# TimescaleDB is a genuine Postgres extension, not a fork - connects via the
|
||||
# plain postgresql dialect, same driver as postgres.
|
||||
timescaledb = ["apache-superset[postgres]"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
|
||||
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
|
||||
starrocks = ["starrocks>=1.3.4, <2"]
|
||||
doris = ["pydoris>=1.2.0, <2.0.0"]
|
||||
oceanbase = ["oceanbase_py>=0.0.1.2"]
|
||||
# No `oceanbase` extra is published here: oceanbase_py pins
|
||||
# sqlalchemy-utils>=0.38.3,<0.39, which conflicts outright with Superset's
|
||||
# own sqlalchemy-utils==0.42.1 pin, so `pip install apache-superset[oceanbase]`
|
||||
# could never resolve. CI installs oceanbase_py as a standalone package with
|
||||
# --no-deps instead (see testcontainers.yml). Add the extra back once
|
||||
# oceanbase_py relaxes that pin.
|
||||
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.8"]
|
||||
# YugabyteDB's YSQL layer is fully Postgres-wire compatible - connects via
|
||||
# the plain postgresql dialect, same driver as postgres.
|
||||
yugabytedb = ["apache-superset[postgres]"]
|
||||
development = [
|
||||
# no bounds for apache-superset-extensions-cli until a stable version
|
||||
"apache-superset-extensions-cli",
|
||||
"boto3",
|
||||
"docker",
|
||||
# 7.0.0 raises `docker.errors.DockerException: ... Not supported URL
|
||||
# scheme http+docker` against the requests/urllib3 versions pinned
|
||||
# elsewhere in this file -- breaks testcontainers (tests/testcontainers/)
|
||||
# before any container even starts. 7.2.0 is confirmed working.
|
||||
"docker>=7.2.0",
|
||||
"flask-testing",
|
||||
"freezegun",
|
||||
"grpcio>=1.83.1",
|
||||
|
||||
+7
-1
@@ -19,7 +19,13 @@ testpaths =
|
||||
tests
|
||||
python_files = *_test.py test_*.py *_tests.py *viz/utils.py
|
||||
# `-p no:warnings` temporarily disabled in favor of more finely tuned `filterwarnings`.
|
||||
#addopts = -p no:warnings
|
||||
# `not testcontainers` excludes tests/testcontainers/ by default: those spin up
|
||||
# real Docker containers, and `testpaths = tests` would otherwise pull them into
|
||||
# every plain `pytest` run. The dedicated CI job (testcontainers.yml) overrides
|
||||
# this with an explicit `-m testcontainers` to run them.
|
||||
addopts = -m "not testcontainers"
|
||||
markers =
|
||||
testcontainers: exercises a real database via testcontainers-python (needs Docker); excluded by default, see .github/workflows/testcontainers.yml
|
||||
asyncio_mode = auto
|
||||
|
||||
# `ignore` is effectively equivalent to `-p no:warnings`.
|
||||
|
||||
@@ -16,5 +16,32 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
-e .[development,bigquery,cockroachdb,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e .[development,bigquery,clickhouse,cockroachdb,crate,databend,druid,duckdb,elasticsearch,fastmcp,firebird,gevent,gsheets,monetdb,mongodb,mssql,mysql,oracle,postgres,presto,prophet,risingwave,starrocks,trino,thumbnails,ydb]
|
||||
-e ./superset-extensions-cli[test]
|
||||
# testcontainers-backed db_engine_specs tests (tests/testcontainers/) --
|
||||
# see .github/workflows/testcontainers.yml
|
||||
#
|
||||
# `db2` (the `ibm-db-sa`/`ibm-db` driver) and `oceanbase` (the `oceanbase_py`
|
||||
# driver) are both deliberately left out of the baseline dev install above:
|
||||
# `ibm-db` ships no Linux arm64 wheel, breaking the multi-platform
|
||||
# (amd64+arm64) dev Docker image build; `oceanbase_py` pins
|
||||
# `sqlalchemy-utils>=0.38.3,<0.39`, which conflicts outright with Superset's
|
||||
# own `sqlalchemy-utils==0.42.1` pin -- there's no version of both that can
|
||||
# coexist in one resolved environment. Both testcontainers CI jobs install
|
||||
# their driver on demand instead, only for their own matrix leg -- see
|
||||
# .github/workflows/testcontainers.yml.
|
||||
#
|
||||
# mariadb/timescaledb/yugabytedb need no testcontainers extra of their own:
|
||||
# they reuse the postgres/mysql container classes pointed at a different
|
||||
# image, and psycopg2-binary/mysqlclient are already pulled in above via
|
||||
# the postgres/mysql extras. Plain postgres/mysql obviously need nothing
|
||||
# extra either. clickhouse and starrocks also need no testcontainers extra:
|
||||
# ClickHouseContainer has no driver import of its own (clickhouse-connect,
|
||||
# pulled in above via the clickhouse extra, is all the test needs), and
|
||||
# StarRocks has no dedicated testcontainers module at all -- its test uses
|
||||
# a generic DockerContainer plus the same mysqlclient the mysql extra
|
||||
# already provides. databend/risingwave/firebird/ydb are the same story:
|
||||
# none has a dedicated testcontainers module, so each test uses a generic
|
||||
# DockerContainer plus whatever driver its own extra above already
|
||||
# provides.
|
||||
testcontainers[cockroachdb,cratedb,mongodb,mssql,mysql,oracle,postgres,trino]>=4.15.0,<5
|
||||
|
||||
@@ -12,10 +12,17 @@
|
||||
# apache-superset
|
||||
aiofile==3.9.0
|
||||
# via py-key-value-aio
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
# via ydb
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
alembic==1.15.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-migrate
|
||||
# starrocks
|
||||
amqp==5.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -24,6 +31,8 @@ annotated-types==0.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pydantic
|
||||
antlr4-python3-runtime==4.13.2
|
||||
# via pymongosql
|
||||
anyio==4.11.0
|
||||
# via
|
||||
# httpx
|
||||
@@ -42,9 +51,12 @@ apsw==3.50.1.0
|
||||
# shillelagh
|
||||
astroid==3.3.10
|
||||
# via pylint
|
||||
asyncmy2==0.2.21
|
||||
# via starrocks
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# aiohttp
|
||||
# cattrs
|
||||
# cyclopts
|
||||
# jsonschema
|
||||
@@ -65,6 +77,7 @@ backports-tarfile==1.2.0
|
||||
backports-zstd==1.6.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# clickhouse-connect
|
||||
# flask-compress
|
||||
bcrypt==4.3.0
|
||||
# via
|
||||
@@ -117,8 +130,11 @@ celery==5.6.3
|
||||
certifi==2026.5.20
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# clickhouse-connect
|
||||
# elasticsearch
|
||||
# httpcore
|
||||
# httpx
|
||||
# opensearch-py
|
||||
# requests
|
||||
cffi==2.0.0
|
||||
# via
|
||||
@@ -160,6 +176,8 @@ click-repl==0.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
clickhouse-connect==1.7.2
|
||||
# via apache-superset
|
||||
cmdstanpy==1.1.0
|
||||
# via prophet
|
||||
colorama==0.4.6
|
||||
@@ -171,6 +189,8 @@ contourpy==1.0.7
|
||||
# via matplotlib
|
||||
coverage==7.6.8
|
||||
# via pytest-cov
|
||||
crate==2.2.1
|
||||
# via sqlalchemy-cratedb
|
||||
cron-descriptor==1.4.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -186,14 +206,20 @@ cryptography==50.0.1
|
||||
# authlib
|
||||
# google-auth
|
||||
# joserfc
|
||||
# oracledb
|
||||
# paramiko
|
||||
# pyjwt
|
||||
# pymysql
|
||||
# pyopenssl
|
||||
# secretstorage
|
||||
cycler==0.12.1
|
||||
# via matplotlib
|
||||
cyclopts==4.2.4
|
||||
# via fastmcp-slim
|
||||
databend-driver==0.34.2
|
||||
# via databend-sqlalchemy
|
||||
databend-sqlalchemy==0.5.5
|
||||
# via apache-superset
|
||||
db-dtypes==1.3.1
|
||||
# via pandas-gbq
|
||||
defusedxml==0.7.1
|
||||
@@ -216,8 +242,11 @@ dnspython==2.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# email-validator
|
||||
docker==7.0.0
|
||||
# via apache-superset
|
||||
# pymongo
|
||||
docker==7.2.0
|
||||
# via
|
||||
# apache-superset
|
||||
# testcontainers
|
||||
docstring-parser==0.17.0
|
||||
# via cyclopts
|
||||
docutils==0.22.2
|
||||
@@ -228,6 +257,10 @@ duckdb==1.5.5
|
||||
# duckdb-engine
|
||||
duckdb-engine==0.17.0
|
||||
# via apache-superset
|
||||
elasticsearch==7.17.13
|
||||
# via elasticsearch-dbapi
|
||||
elasticsearch-dbapi==0.2.13
|
||||
# via apache-superset
|
||||
email-validator==2.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -237,6 +270,8 @@ et-xmlfile==2.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# openpyxl
|
||||
events==0.5
|
||||
# via opensearch-py
|
||||
exceptiongroup==1.3.0
|
||||
# via fastmcp-slim
|
||||
fastmcp==3.4.7
|
||||
@@ -247,6 +282,10 @@ filelock==3.20.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# virtualenv
|
||||
firebird-base==2.0.3
|
||||
# via firebird-driver
|
||||
firebird-driver==2.0.3
|
||||
# via sqlalchemy-firebird
|
||||
flask==3.1.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -327,12 +366,18 @@ fonttools==4.60.2
|
||||
# via matplotlib
|
||||
freezegun==1.5.1
|
||||
# via apache-superset
|
||||
frozenlist==1.8.0
|
||||
# via
|
||||
# aiohttp
|
||||
# aiosignal
|
||||
future==1.0.0
|
||||
# via pyhive
|
||||
geographiclib==2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# geopy
|
||||
geojson==3.3.0
|
||||
# via sqlalchemy-cratedb
|
||||
geopy==2.4.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -389,6 +434,7 @@ grpcio==1.83.1
|
||||
# apache-superset
|
||||
# google-api-core
|
||||
# grpcio-status
|
||||
# ydb
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==26.2.0
|
||||
@@ -414,6 +460,7 @@ httpx==0.28.1
|
||||
# via
|
||||
# fastmcp-slim
|
||||
# mcp
|
||||
# testcontainers
|
||||
httpx-sse==0.4.1
|
||||
# via mcp
|
||||
humanize==4.12.3
|
||||
@@ -430,6 +477,7 @@ idna==3.15
|
||||
# httpx
|
||||
# requests
|
||||
# url-normalize
|
||||
# yarl
|
||||
importlib-metadata==8.7.0
|
||||
# via
|
||||
# keyring
|
||||
@@ -468,6 +516,7 @@ jmespath==1.1.0
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
# pymongosql
|
||||
joserfc==1.7.2
|
||||
# via fastmcp-slim
|
||||
jsonpath-ng==1.8.0
|
||||
@@ -500,6 +549,8 @@ kombu==5.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
lark==1.3.1
|
||||
# via starrocks
|
||||
lazy-object-proxy==1.10.0
|
||||
# via openapi-spec-validator
|
||||
limits==5.1.0
|
||||
@@ -507,7 +558,9 @@ limits==5.1.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-limiter
|
||||
lz4==4.4.5
|
||||
# via trino
|
||||
# via
|
||||
# clickhouse-connect
|
||||
# trino
|
||||
mako==1.4.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -568,6 +621,10 @@ msgspec==0.19.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-session
|
||||
multidict==6.7.1
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
mysqlclient==2.2.8
|
||||
# via apache-superset
|
||||
nh3==0.3.7
|
||||
@@ -606,14 +663,22 @@ openpyxl==3.1.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pandas
|
||||
opensearch-py==2.8.0
|
||||
# via elasticsearch-dbapi
|
||||
opentelemetry-api==1.39.1
|
||||
# via fastmcp-slim
|
||||
oracledb==4.0.2
|
||||
# via
|
||||
# apache-superset
|
||||
# testcontainers
|
||||
ordered-set==4.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-limiter
|
||||
orjson==3.11.9
|
||||
# via trino
|
||||
# via
|
||||
# crate
|
||||
# trino
|
||||
packaging==25.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -621,8 +686,8 @@ packaging==25.0
|
||||
# apispec
|
||||
# db-dtypes
|
||||
# deprecation
|
||||
# docker
|
||||
# duckdb-engine
|
||||
# elasticsearch-dbapi
|
||||
# fastmcp-slim
|
||||
# google-cloud-bigquery
|
||||
# kombu
|
||||
@@ -632,6 +697,8 @@ packaging==25.0
|
||||
# pytest
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-firebird
|
||||
# ydb
|
||||
pandas==2.3.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -693,16 +760,22 @@ prompt-toolkit==3.0.51
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# click-repl
|
||||
propcache==0.5.2
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
prophet==1.4.0
|
||||
# via apache-superset
|
||||
proto-plus==1.25.0
|
||||
# via google-api-core
|
||||
protobuf==5.29.6
|
||||
# via
|
||||
# firebird-base
|
||||
# google-api-core
|
||||
# googleapis-common-protos
|
||||
# grpcio-status
|
||||
# proto-plus
|
||||
# ydb
|
||||
psutil==6.1.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -776,6 +849,24 @@ pyjwt==2.13.0
|
||||
# mcp
|
||||
pylint==3.3.7
|
||||
# via apache-superset
|
||||
pymonetdb==1.9.1
|
||||
# via
|
||||
# apache-superset
|
||||
# sqlalchemy-monetdb
|
||||
pymongo==4.17.0
|
||||
# via
|
||||
# pymongosql
|
||||
# testcontainers
|
||||
pymongosql==0.7.3
|
||||
# via apache-superset
|
||||
pymssql==2.3.13
|
||||
# via
|
||||
# apache-superset
|
||||
# testcontainers
|
||||
pymysql==1.2.0
|
||||
# via
|
||||
# starrocks
|
||||
# testcontainers
|
||||
pynacl==1.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -820,11 +911,13 @@ python-dateutil==2.9.0.post0
|
||||
# botocore
|
||||
# celery
|
||||
# croniter
|
||||
# firebird-driver
|
||||
# flask-appbuilder
|
||||
# freezegun
|
||||
# google-cloud-bigquery
|
||||
# holidays
|
||||
# matplotlib
|
||||
# opensearch-py
|
||||
# pandas
|
||||
# pyhive
|
||||
# shillelagh
|
||||
@@ -835,6 +928,7 @@ python-dotenv==1.2.2
|
||||
# apache-superset
|
||||
# fastmcp-slim
|
||||
# pydantic-settings
|
||||
# testcontainers
|
||||
python-ldap==3.4.7
|
||||
# via apache-superset
|
||||
python-multipart==0.0.29
|
||||
@@ -878,6 +972,7 @@ requests==2.33.0
|
||||
# google-api-core
|
||||
# google-cloud-bigquery
|
||||
# jsonschema-path
|
||||
# opensearch-py
|
||||
# pydruid
|
||||
# pyhive
|
||||
# requests-cache
|
||||
@@ -956,7 +1051,9 @@ sqlalchemy==2.0.52
|
||||
# alembic
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
# databend-sqlalchemy
|
||||
# duckdb-engine
|
||||
# elasticsearch-dbapi
|
||||
# flask-appbuilder
|
||||
# flask-sqlalchemy
|
||||
# marshmallow-sqlalchemy
|
||||
@@ -964,7 +1061,14 @@ sqlalchemy==2.0.52
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-cockroachdb
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-cratedb
|
||||
# sqlalchemy-firebird
|
||||
# sqlalchemy-monetdb
|
||||
# sqlalchemy-risingwave
|
||||
# sqlalchemy-utils
|
||||
# starrocks
|
||||
# testcontainers
|
||||
# ydb-sqlalchemy
|
||||
sqlalchemy-bigquery==1.17.2
|
||||
# via apache-superset
|
||||
sqlalchemy-cockroachdb==2.0.4
|
||||
@@ -973,6 +1077,16 @@ sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
sqlalchemy-cratedb==0.43.1
|
||||
# via
|
||||
# apache-superset
|
||||
# testcontainers
|
||||
sqlalchemy-firebird==2.2.0
|
||||
# via apache-superset
|
||||
sqlalchemy-monetdb==2.1.0
|
||||
# via apache-superset
|
||||
sqlalchemy-risingwave==2.1.0
|
||||
# via apache-superset
|
||||
sqlalchemy-utils==0.42.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -984,6 +1098,7 @@ sqlglot==30.17.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
# ydb-sqlglot-plugin
|
||||
sqloxide==0.1.51
|
||||
# via apache-superset
|
||||
sse-starlette==3.0.2
|
||||
@@ -996,6 +1111,8 @@ starlette==1.3.1
|
||||
# via
|
||||
# fastmcp-slim
|
||||
# mcp
|
||||
starrocks==1.3.4
|
||||
# via apache-superset
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.9.0
|
||||
@@ -1004,6 +1121,8 @@ tabulate==0.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
testcontainers==4.15.0
|
||||
# via -r requirements/development.in
|
||||
tiktoken==0.14.0
|
||||
# via apache-superset
|
||||
tomli-w==1.2.0
|
||||
@@ -1015,10 +1134,14 @@ tqdm==4.67.1
|
||||
# cmdstanpy
|
||||
# prophet
|
||||
trino==0.339.0
|
||||
# via apache-superset
|
||||
# via
|
||||
# apache-superset
|
||||
# testcontainers
|
||||
typing-extensions==4.16.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# aiohttp
|
||||
# aiosignal
|
||||
# alembic
|
||||
# anyio
|
||||
# apache-superset
|
||||
@@ -1030,6 +1153,7 @@ typing-extensions==4.16.0
|
||||
# limits
|
||||
# mcp
|
||||
# opentelemetry-api
|
||||
# oracledb
|
||||
# py-key-value-aio
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
@@ -1038,6 +1162,7 @@ typing-extensions==4.16.0
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
# starlette
|
||||
# testcontainers
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
@@ -1065,13 +1190,22 @@ urllib3==2.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# botocore
|
||||
# clickhouse-connect
|
||||
# crate
|
||||
# docker
|
||||
# elasticsearch
|
||||
# opensearch-py
|
||||
# requests
|
||||
# requests-cache
|
||||
# testcontainers
|
||||
uvicorn==0.37.0
|
||||
# via
|
||||
# fastmcp-slim
|
||||
# mcp
|
||||
verlib2==0.3.2
|
||||
# via
|
||||
# crate
|
||||
# sqlalchemy-cratedb
|
||||
vine==5.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -1105,6 +1239,7 @@ wrapt==1.17.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# deprecated
|
||||
# testcontainers
|
||||
wtforms==3.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -1125,6 +1260,18 @@ xlsxwriter==3.2.9
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# pandas
|
||||
yarl==1.24.5
|
||||
# via aiohttp
|
||||
ydb==3.31.4
|
||||
# via
|
||||
# ydb-dbapi
|
||||
# ydb-sqlalchemy
|
||||
ydb-dbapi==0.1.23
|
||||
# via ydb-sqlalchemy
|
||||
ydb-sqlalchemy==0.1.22
|
||||
# via apache-superset
|
||||
ydb-sqlglot-plugin==0.2.8
|
||||
# via apache-superset
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
zope-event==5.0
|
||||
|
||||
@@ -61,6 +61,7 @@ PATTERNS = {
|
||||
"docker": [
|
||||
r"^Dockerfile$",
|
||||
r"^docker.*",
|
||||
r"^\.github/workflows/docker\.yml$",
|
||||
],
|
||||
"docs": [
|
||||
r"^docs/",
|
||||
|
||||
@@ -83,6 +83,10 @@ const disableDevModeInRules = rules =>
|
||||
};
|
||||
});
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return path.dirname(require.resolve(path.join(value, 'package.json')));
|
||||
}
|
||||
|
||||
export default {
|
||||
stories: [
|
||||
'../src/**/*.stories.tsx',
|
||||
@@ -129,7 +133,3 @@ export default {
|
||||
options: {},
|
||||
}
|
||||
};
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return path.dirname(require.resolve(path.join(value, 'package.json')));
|
||||
}
|
||||
|
||||
Generated
+139
-294
@@ -244,7 +244,7 @@
|
||||
"html-webpack-plugin": "^5.6.8",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.5.1",
|
||||
"jest-environment-jsdom": "^30.5.0",
|
||||
"jest-environment-jsdom": "^30.5.1",
|
||||
"jest-html-reporter": "^4.4.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
@@ -255,7 +255,7 @@
|
||||
"minimizer-webpack-plugin": "^5.8.0",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxfmt": "^0.66.0",
|
||||
"oxlint": "^1.80.0",
|
||||
"oxlint": "^1.81.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"process": "^0.11.10",
|
||||
@@ -5933,18 +5933,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.0.tgz",
|
||||
"integrity": "sha512-825vac4Dmysbn2kU7VUQPoKuj/HNUpSTgv98KCByMOSPvHuj1/HpVZeLRsP/itDB2HFiDcoTUrsg8fSu3PxKBw==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.1.tgz",
|
||||
"integrity": "sha512-J395vmP3Fb2Te0JmF7pe4si4jpfbXef1YsY4UpHYL6OOxS2molu9Dsie1VmIiUalXdtmz1P5QRgc+5hBD+ssBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "30.5.0",
|
||||
"@jest/fake-timers": "30.5.0",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/environment": "30.5.1",
|
||||
"@jest/fake-timers": "30.5.1",
|
||||
"@jest/types": "30.5.1",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "30.5.0",
|
||||
"jest-util": "30.5.0"
|
||||
"jest-mock": "30.5.1",
|
||||
"jest-util": "30.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
@@ -5961,34 +5961,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz",
|
||||
"integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.1.tgz",
|
||||
"integrity": "sha512-eYJAkOsrpwDXPcoLNEG6lN9Zo3Cy35pxnVQD74vyIfsi/Q8wB/lZFsEjU4wrecOgT4ZviQDZaX/cFIJyMdMxTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/fake-timers": "30.5.0",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/fake-timers": "30.5.1",
|
||||
"@jest/types": "30.5.1",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "30.5.0"
|
||||
"jest-mock": "30.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.0.tgz",
|
||||
"integrity": "sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.1.tgz",
|
||||
"integrity": "sha512-rEkV6YzpBXo/L9cnj2ibyuYZuyGiNWaPUsyCu3HYJbqCV4jnEiKmf7hId20z8eKjZ0JQ9jtIYKxRSmYB/OYSsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/types": "30.5.1",
|
||||
"@sinonjs/fake-timers": "^15.4.0",
|
||||
"@types/node": "*",
|
||||
"jest-message-util": "30.5.0",
|
||||
"jest-mock": "30.5.0",
|
||||
"jest-util": "30.5.0"
|
||||
"jest-message-util": "30.5.1",
|
||||
"jest-mock": "30.5.1",
|
||||
"jest-util": "30.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
@@ -6028,20 +6028,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz",
|
||||
"integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz",
|
||||
"integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/types": "30.5.1",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-util": "30.5.0",
|
||||
"jest-util": "30.5.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"pretty-format": "30.5.0",
|
||||
"pretty-format": "30.5.1",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.6"
|
||||
},
|
||||
@@ -6063,9 +6063,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz",
|
||||
"integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz",
|
||||
"integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10276,9 +10276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz",
|
||||
"integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz",
|
||||
"integrity": "sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -10293,9 +10293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm64": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz",
|
||||
"integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz",
|
||||
"integrity": "sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10310,9 +10310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-arm64": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz",
|
||||
"integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz",
|
||||
"integrity": "sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10327,9 +10327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-x64": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz",
|
||||
"integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz",
|
||||
"integrity": "sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10344,9 +10344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-freebsd-x64": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz",
|
||||
"integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz",
|
||||
"integrity": "sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10361,9 +10361,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz",
|
||||
"integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz",
|
||||
"integrity": "sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -10378,9 +10378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz",
|
||||
"integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz",
|
||||
"integrity": "sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -10395,9 +10395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-gnu": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz",
|
||||
"integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz",
|
||||
"integrity": "sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10415,9 +10415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-musl": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz",
|
||||
"integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz",
|
||||
"integrity": "sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10435,9 +10435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz",
|
||||
"integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz",
|
||||
"integrity": "sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -10455,9 +10455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz",
|
||||
"integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz",
|
||||
"integrity": "sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -10475,9 +10475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-musl": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz",
|
||||
"integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz",
|
||||
"integrity": "sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -10495,9 +10495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-s390x-gnu": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz",
|
||||
"integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz",
|
||||
"integrity": "sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -10515,9 +10515,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-gnu": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz",
|
||||
"integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz",
|
||||
"integrity": "sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10535,9 +10535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-musl": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz",
|
||||
"integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz",
|
||||
"integrity": "sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10555,9 +10555,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-openharmony-arm64": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz",
|
||||
"integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz",
|
||||
"integrity": "sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10572,9 +10572,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-arm64-msvc": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz",
|
||||
"integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz",
|
||||
"integrity": "sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -10589,9 +10589,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-ia32-msvc": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz",
|
||||
"integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz",
|
||||
"integrity": "sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -10606,9 +10606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-x64-msvc": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz",
|
||||
"integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz",
|
||||
"integrity": "sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -27311,32 +27311,6 @@
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/@jest/transform": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.0.tgz",
|
||||
"integrity": "sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.4",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"babel-plugin-istanbul": "^8.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-haste-map": "30.5.0",
|
||||
"jest-regex-util": "30.5.0",
|
||||
"jest-util": "30.5.0",
|
||||
"pirates": "^4.0.7",
|
||||
"slash": "^3.0.0",
|
||||
"write-file-atomic": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
|
||||
@@ -27685,48 +27659,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/babel-jest": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.0.tgz",
|
||||
"integrity": "sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/transform": "30.5.0",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"babel-plugin-istanbul": "^8.0.0",
|
||||
"babel-preset-jest": "30.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/babel-plugin-istanbul": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz",
|
||||
"integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"workspaces": [
|
||||
"test/babel-8"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@istanbuljs/load-nyc-config": "^1.0.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"istanbul-lib-instrument": "^6.0.2",
|
||||
"test-exclude": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -27964,93 +27896,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
|
||||
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^10.4.1",
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude/node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jest-config/node_modules/test-exclude/node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/unrs-resolver": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
|
||||
@@ -28213,14 +28058,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.0.tgz",
|
||||
"integrity": "sha512-VVHN/G3zrxsQR398jvMalM76ALX6YBAftsYLCGtTeKRmz4f42YJAP05AGpk0VF5SLtoHdkfKunYGfnKPnmcEOA==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.1.tgz",
|
||||
"integrity": "sha512-8lzKbC/SRbQE24wr1OOJV+aYtDAuVNKBryN6YcFiCcaZZ3I7grcZY7w91BwNvGET0ubKDmomEHZFHMaF+6pAlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/environment": "30.5.0",
|
||||
"@jest/environment-jsdom-abstract": "30.5.0",
|
||||
"@jest/environment": "30.5.1",
|
||||
"@jest/environment-jsdom-abstract": "30.5.1",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"jsdom": "^26.1.0"
|
||||
},
|
||||
@@ -28237,34 +28082,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/@jest/environment": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz",
|
||||
"integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.1.tgz",
|
||||
"integrity": "sha512-eYJAkOsrpwDXPcoLNEG6lN9Zo3Cy35pxnVQD74vyIfsi/Q8wB/lZFsEjU4wrecOgT4ZviQDZaX/cFIJyMdMxTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/fake-timers": "30.5.0",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/fake-timers": "30.5.1",
|
||||
"@jest/types": "30.5.1",
|
||||
"@types/node": "*",
|
||||
"jest-mock": "30.5.0"
|
||||
"jest-mock": "30.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.0.tgz",
|
||||
"integrity": "sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.1.tgz",
|
||||
"integrity": "sha512-rEkV6YzpBXo/L9cnj2ibyuYZuyGiNWaPUsyCu3HYJbqCV4jnEiKmf7hId20z8eKjZ0JQ9jtIYKxRSmYB/OYSsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/types": "30.5.1",
|
||||
"@sinonjs/fake-timers": "^15.4.0",
|
||||
"@types/node": "*",
|
||||
"jest-message-util": "30.5.0",
|
||||
"jest-mock": "30.5.0",
|
||||
"jest-util": "30.5.0"
|
||||
"jest-message-util": "30.5.1",
|
||||
"jest-mock": "30.5.1",
|
||||
"jest-util": "30.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
@@ -28331,20 +28176,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/jest-message-util": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz",
|
||||
"integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz",
|
||||
"integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.5.0",
|
||||
"@jest/types": "30.5.1",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-util": "30.5.0",
|
||||
"jest-util": "30.5.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"pretty-format": "30.5.0",
|
||||
"pretty-format": "30.5.1",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.6"
|
||||
},
|
||||
@@ -28406,9 +28251,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/pretty-format": {
|
||||
"version": "30.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz",
|
||||
"integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==",
|
||||
"version": "30.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz",
|
||||
"integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -36118,9 +35963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.80.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz",
|
||||
"integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==",
|
||||
"version": "1.81.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.81.0.tgz",
|
||||
"integrity": "sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -36130,28 +35975,28 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxlint/binding-android-arm-eabi": "1.80.0",
|
||||
"@oxlint/binding-android-arm64": "1.80.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.80.0",
|
||||
"@oxlint/binding-darwin-x64": "1.80.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.80.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.80.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.80.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.80.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.80.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.80.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.80.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.80.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.80.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.80.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.80.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.80.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.80.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.80.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.80.0"
|
||||
"@oxlint/binding-android-arm-eabi": "1.81.0",
|
||||
"@oxlint/binding-android-arm64": "1.81.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.81.0",
|
||||
"@oxlint/binding-darwin-x64": "1.81.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.81.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.81.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.81.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.81.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.81.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.81.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.81.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.81.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.81.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.81.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.81.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.81.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.81.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.81.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.81.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"oxlint-tsgolint": ">=7.0.2001",
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
"html-webpack-plugin": "^5.6.8",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.5.1",
|
||||
"jest-environment-jsdom": "^30.5.0",
|
||||
"jest-environment-jsdom": "^30.5.1",
|
||||
"jest-html-reporter": "^4.4.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
@@ -332,7 +332,7 @@
|
||||
"minimizer-webpack-plugin": "^5.8.0",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxfmt": "^0.66.0",
|
||||
"oxlint": "^1.80.0",
|
||||
"oxlint": "^1.81.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"process": "^0.11.10",
|
||||
|
||||
+3
@@ -35,6 +35,9 @@ export const resampleOperator: PostProcessingFactory<PostProcessingResample> = (
|
||||
method: resampleMethod,
|
||||
rule: resampleRule,
|
||||
fill_value: resampleZeroFill ? 0 : null,
|
||||
...(formData.resample_fill_time_range
|
||||
? { fill_time_range: true }
|
||||
: undefined),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+24
@@ -217,5 +217,29 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'resample_fill_time_range',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Fill the entire time range'),
|
||||
default: false,
|
||||
description: t(
|
||||
'Fill missing periods across the whole time range of the chart ' +
|
||||
'instead of only between the first and the last data point. ' +
|
||||
'Useful to keep a series anchored to the selected time range ' +
|
||||
'when the data starts late or ends early.',
|
||||
),
|
||||
visibility: ({ controls }, { name }) => {
|
||||
// `_b` suffixed controls refer to Query B in mixed timeseries
|
||||
const suffix = name.endsWith('_b') ? '_b' : '';
|
||||
return Boolean(
|
||||
controls[`resample_rule${suffix}`]?.value &&
|
||||
controls[`resample_method${suffix}`]?.value,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
+43
@@ -114,3 +114,46 @@ test('should do zerofill resample', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should request filling the entire time range', () => {
|
||||
expect(
|
||||
resampleOperator(
|
||||
{
|
||||
...formData,
|
||||
resample_method: 'zerofill',
|
||||
resample_rule: '1D',
|
||||
resample_fill_time_range: true,
|
||||
},
|
||||
queryObject,
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'resample',
|
||||
options: {
|
||||
method: 'asfreq',
|
||||
rule: '1D',
|
||||
fill_value: 0,
|
||||
fill_time_range: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should omit fill_time_range when the control is off', () => {
|
||||
expect(
|
||||
resampleOperator(
|
||||
{
|
||||
...formData,
|
||||
resample_method: 'zerofill',
|
||||
resample_rule: '1D',
|
||||
resample_fill_time_range: false,
|
||||
},
|
||||
queryObject,
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'resample',
|
||||
options: {
|
||||
method: 'asfreq',
|
||||
rule: '1D',
|
||||
fill_value: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,8 +113,19 @@ export interface BackendOwnState {
|
||||
* Each chart plugin can implement this to convert its internal state representation
|
||||
* to the standardized backend format.
|
||||
*/
|
||||
export interface ChartStateConverterOptions {
|
||||
// Set when converting for a download/export query rather than the chart's
|
||||
// live (re-)query. Some chart-specific state (e.g. AG Grid's client-side
|
||||
// sort/filter) is normally excluded from the live query's ownState to
|
||||
// avoid triggering an unnecessary requery, but a downloaded file has no
|
||||
// client-side pass to apply that state, so it still needs to be converted
|
||||
// for exports to reproduce the displayed view.
|
||||
forExport?: boolean;
|
||||
}
|
||||
|
||||
export type ChartStateConverter<TChartState = JsonObject> = (
|
||||
chartState: TChartState,
|
||||
options?: ChartStateConverterOptions,
|
||||
) => Partial<BackendOwnState>;
|
||||
|
||||
export interface PlainObject {
|
||||
|
||||
+23
@@ -175,3 +175,26 @@ test('closes modal when onHide is called', () => {
|
||||
// Modal should be hidden (not visible)
|
||||
expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('keeps the confirm button disabled while disablePrimaryButton is set', () => {
|
||||
const { getByTestId, getByRole, rerender } = render(
|
||||
<ConfirmStatusChange {...mockedProps} disablePrimaryButton>
|
||||
{confirm => <Button data-test="trigger" onClick={confirm} />}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('trigger'));
|
||||
fireEvent.change(getByTestId('delete-modal-input'), {
|
||||
target: { value: 'DELETE' },
|
||||
});
|
||||
|
||||
expect(getByRole('button', { name: 'Delete' })).toBeDisabled();
|
||||
|
||||
rerender(
|
||||
<ConfirmStatusChange {...mockedProps} disablePrimaryButton={false}>
|
||||
{confirm => <Button data-test="trigger" onClick={confirm} />}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
expect(getByRole('button', { name: 'Delete' })).toBeEnabled();
|
||||
});
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ export function ConfirmStatusChange({
|
||||
onConfirm,
|
||||
children,
|
||||
recoverable,
|
||||
disablePrimaryButton,
|
||||
}: ConfirmStatusChangeProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentCallbackArgs, setCurrentCallbackArgs] = useState<any[]>([]);
|
||||
@@ -69,6 +70,7 @@ export function ConfirmStatusChange({
|
||||
name="please confirm"
|
||||
title={title}
|
||||
recoverable={recoverable}
|
||||
disablePrimaryButton={disablePrimaryButton}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+6
@@ -30,4 +30,10 @@ export interface ConfirmStatusChangeProps {
|
||||
* drops the "type DELETE to confirm" step and uses a primary confirm button.
|
||||
*/
|
||||
recoverable?: boolean;
|
||||
/**
|
||||
* Forwarded to the underlying DeleteModal: keeps the confirm button disabled
|
||||
* regardless of the typed-text gate, e.g. while the caller is still loading
|
||||
* information the user needs before confirming.
|
||||
*/
|
||||
disablePrimaryButton?: boolean;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ export type {
|
||||
GridState,
|
||||
GridReadyEvent,
|
||||
CellClickedEvent,
|
||||
CellContextMenuEvent,
|
||||
CellKeyDownEvent,
|
||||
CellClassParams,
|
||||
IMenuActionParams,
|
||||
|
||||
@@ -196,6 +196,13 @@ interface _PostProcessingResample {
|
||||
method: string;
|
||||
rule: string;
|
||||
fill_value?: number | null;
|
||||
/**
|
||||
* Pad the result so it covers the whole time range of the query instead of
|
||||
* only the span between the first and last data point. The boundaries are
|
||||
* resolved server side, since a time range may be expressed in natural
|
||||
* language (e.g. `Last week`).
|
||||
*/
|
||||
fill_time_range?: boolean;
|
||||
};
|
||||
}
|
||||
export type PostProcessingResample =
|
||||
|
||||
@@ -20,6 +20,15 @@ import type { DataRecordValue } from '../query/types/QueryResponse';
|
||||
import type { TimeFormatFunction } from './types';
|
||||
import normalizeTimestamp from './utils/normalizeTimestamp';
|
||||
|
||||
/**
|
||||
* A missing date can arrive as either `null`/`undefined` or an empty string
|
||||
* (e.g. a blank cell in an otherwise-numeric epoch column, which also has the
|
||||
* side effect of degrading the whole column's formatter to `String` - see
|
||||
* `isNumeric` in transformProps.ts). Both should be treated as "no value".
|
||||
*/
|
||||
export const isEmptyDateInput = (input: DataRecordValue): boolean =>
|
||||
input === null || input === undefined || input === '';
|
||||
|
||||
/**
|
||||
* Extended Date object with a custom formatter, and retains the original input
|
||||
* when the formatter is simple `String(..)`.
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
|
||||
export { default as TimeFormats, LOCAL_PREFIX } from './TimeFormats';
|
||||
export { default as TimeFormatter, PREVIEW_TIME } from './TimeFormatter';
|
||||
export { default as DateWithFormatter } from './DateWithFormatter';
|
||||
export {
|
||||
default as DateWithFormatter,
|
||||
isEmptyDateInput,
|
||||
} from './DateWithFormatter';
|
||||
export { DEFAULT_D3_TIME_FORMAT } from './D3FormatConfig';
|
||||
|
||||
export {
|
||||
|
||||
@@ -79,4 +79,5 @@ export interface AgGridChartState {
|
||||
columnOrder?: string[];
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
serverPagination?: boolean;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
GridReadyEvent,
|
||||
GridState,
|
||||
CellClickedEvent,
|
||||
CellContextMenuEvent,
|
||||
CellKeyDownEvent,
|
||||
SelectionChangedEvent,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
@@ -59,9 +60,13 @@ import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
|
||||
import getInitialFilterModel from '../utils/getInitialFilterModel';
|
||||
import reconcileColumnState from '../utils/reconcileColumnState';
|
||||
import getColumnStateSignature from '../utils/getColumnStateSignature';
|
||||
import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||
import { getCompleteFilterState } from '../utils/filterStateManager';
|
||||
import { PAGE_SIZE_OPTIONS, ROW_NUMBER_COL_ID } from '../consts';
|
||||
import {
|
||||
getCompleteFilterState,
|
||||
type FilterState,
|
||||
} from '../utils/filterStateManager';
|
||||
import { copyCellValueOnKeyDown } from '../utils/copyCellValue';
|
||||
import type { ClientViewSnapshot } from '../utils/externalAPIs';
|
||||
|
||||
export interface AgGridState extends Partial<GridState> {
|
||||
timestamp?: number;
|
||||
@@ -77,7 +82,6 @@ export type AgGridChartStateWithMetadata = Partial<AgGridChartState> & {
|
||||
export interface AgGridTableProps {
|
||||
gridTheme?: string;
|
||||
isDarkMode?: boolean;
|
||||
gridHeight?: number;
|
||||
updateInterval?: number;
|
||||
data?: any[];
|
||||
onGridReady?: (params: GridReadyEvent) => void;
|
||||
@@ -100,17 +104,20 @@ export interface AgGridTableProps {
|
||||
serverPageLength: number;
|
||||
hasServerPageLengthChanged: boolean;
|
||||
handleCellClicked: (event: CellClickedEvent) => void;
|
||||
handleCellContextMenu?: (event: CellContextMenuEvent) => void;
|
||||
handleSelectionChanged: (event: SelectionChangedEvent) => void;
|
||||
filters?: Record<string, DataRecordValue[]> | null;
|
||||
isActiveFilterValue?: (key: string, val: DataRecordValue) => boolean;
|
||||
renderTimeComparisonDropdown: () => JSX.Element | null;
|
||||
cleanedTotals: DataRecord;
|
||||
showTotals: boolean;
|
||||
width: number;
|
||||
onColumnStateChange?: (state: AgGridChartStateWithMetadata) => void;
|
||||
onFilterChanged?: (filterModel: Record<string, any>) => void;
|
||||
onFilterChanged?: (completeFilterState: FilterState) => void;
|
||||
metricColumns?: string[];
|
||||
gridRef?: RefObject<AgGridReact>;
|
||||
chartState?: AgGridChartState;
|
||||
onClientViewChange?: (snapshot: ClientViewSnapshot) => void;
|
||||
}
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule, ClientSideRowModelModule]);
|
||||
@@ -119,7 +126,6 @@ const isSearchFocused = new Map<string, boolean>();
|
||||
|
||||
const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
({
|
||||
gridHeight,
|
||||
data = [],
|
||||
colDefsFromProps,
|
||||
includeSearch,
|
||||
@@ -140,8 +146,10 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
serverPageLength,
|
||||
hasServerPageLengthChanged,
|
||||
handleCellClicked,
|
||||
handleCellContextMenu,
|
||||
handleSelectionChanged,
|
||||
filters,
|
||||
isActiveFilterValue,
|
||||
renderTimeComparisonDropdown,
|
||||
cleanedTotals,
|
||||
showTotals,
|
||||
@@ -150,12 +158,14 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
onFilterChanged,
|
||||
metricColumns = [],
|
||||
chartState,
|
||||
onClientViewChange,
|
||||
}) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const rowData = useMemo(() => data, [data]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const lastCapturedStateRef = useRef<string | null>(null);
|
||||
const hasCapturedInitialGridStateRef = useRef(false);
|
||||
const filterOperationVersionRef = useRef(0);
|
||||
|
||||
const searchId = `search-${id}`;
|
||||
@@ -189,13 +199,26 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
[],
|
||||
);
|
||||
|
||||
// Memoize container style
|
||||
// Fills the full height allotted by the chart container (StyledChartContainer);
|
||||
// the search/time-comparison controls and pagination bar take their natural
|
||||
// height and the grid flexes into whatever space remains (see gridFlexStyles),
|
||||
// instead of a hardcoded pixel height that drifts from the actual chrome height.
|
||||
const containerStyles = useMemo(
|
||||
() => ({
|
||||
height: gridHeight,
|
||||
height: '100%',
|
||||
width,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
}),
|
||||
[gridHeight, width],
|
||||
[width],
|
||||
);
|
||||
|
||||
const gridFlexStyles = useMemo(
|
||||
() => ({
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [quickFilterText, setQuickFilterText] = useState<string>();
|
||||
@@ -293,6 +316,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
sortModel,
|
||||
filterModel,
|
||||
timestamp: Date.now(),
|
||||
serverPagination: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,49 +345,85 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
[serverPagination, gridInitialState, percentMetrics, onSortChange],
|
||||
);
|
||||
|
||||
const handleGridStateChange = useCallback(
|
||||
const captureGridState = useCallback(() => {
|
||||
const { api } = gridRef.current ?? {};
|
||||
if (!api) return null;
|
||||
|
||||
const columnState = api.getColumnState ? api.getColumnState() : [];
|
||||
const filterModel = api.getFilterModel ? api.getFilterModel() : {};
|
||||
const sortModel = columnState
|
||||
.filter(col => col.sort)
|
||||
.map(col => ({
|
||||
colId: col.colId,
|
||||
sort: col.sort as 'asc' | 'desc',
|
||||
sortIndex: col.sortIndex || 0,
|
||||
}))
|
||||
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
|
||||
|
||||
return {
|
||||
stateToSave: {
|
||||
columnState,
|
||||
sortModel,
|
||||
filterModel,
|
||||
timestamp: Date.now(),
|
||||
serverPagination: !!serverPagination,
|
||||
},
|
||||
stateHash: getColumnStateSignature(columnState, sortModel, filterModel),
|
||||
};
|
||||
}, [serverPagination]);
|
||||
|
||||
const persistGridStateChange = useCallback(
|
||||
debounce(() => {
|
||||
if (onColumnStateChange && gridRef.current?.api) {
|
||||
try {
|
||||
const { api } = gridRef.current;
|
||||
if (!onColumnStateChange) return;
|
||||
try {
|
||||
const captured = captureGridState();
|
||||
if (!captured) return;
|
||||
const { stateToSave, stateHash } = captured;
|
||||
|
||||
const columnState = api.getColumnState ? api.getColumnState() : [];
|
||||
if (stateHash !== lastCapturedStateRef.current) {
|
||||
lastCapturedStateRef.current = stateHash;
|
||||
|
||||
const filterModel = api.getFilterModel ? api.getFilterModel() : {};
|
||||
|
||||
const sortModel = columnState
|
||||
.filter(col => col.sort)
|
||||
.map(col => ({
|
||||
colId: col.colId,
|
||||
sort: col.sort as 'asc' | 'desc',
|
||||
sortIndex: col.sortIndex || 0,
|
||||
}))
|
||||
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
|
||||
|
||||
const stateToSave = {
|
||||
columnState,
|
||||
sortModel,
|
||||
filterModel,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const stateHash = getColumnStateSignature(
|
||||
columnState,
|
||||
sortModel,
|
||||
filterModel,
|
||||
);
|
||||
|
||||
if (stateHash !== lastCapturedStateRef.current) {
|
||||
lastCapturedStateRef.current = stateHash;
|
||||
|
||||
onColumnStateChange(stateToSave);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error capturing AG Grid state:', error);
|
||||
onColumnStateChange(stateToSave);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error capturing AG Grid state:', error);
|
||||
}
|
||||
}, Constants.SLOW_DEBOUNCE),
|
||||
[onColumnStateChange],
|
||||
[onColumnStateChange, captureGridState],
|
||||
);
|
||||
|
||||
const handleGridStateChange = useCallback(() => {
|
||||
// AG Grid fires onStateUpdated once as it applies the initial
|
||||
// column/sort/filter state on mount, before any user interaction.
|
||||
// That first event just reflects the state the grid was initialized
|
||||
// with (chartState/gridInitialState) - not a user-driven change - so
|
||||
// it's captured synchronously as the baseline rather than persisted.
|
||||
// This check runs on every raw call, before debouncing, so a real
|
||||
// user action that lands inside the same debounce window as this
|
||||
// first call is never coalesced into it and dropped.
|
||||
if (!hasCapturedInitialGridStateRef.current) {
|
||||
hasCapturedInitialGridStateRef.current = true;
|
||||
try {
|
||||
const captured = captureGridState();
|
||||
if (captured) {
|
||||
lastCapturedStateRef.current = captured.stateHash;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error capturing AG Grid state:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
persistGridStateChange();
|
||||
}, [captureGridState, persistGridStateChange]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
// Cleanup debounced grid-state capture
|
||||
() => {
|
||||
persistGridStateChange.cancel();
|
||||
},
|
||||
[persistGridStateChange],
|
||||
);
|
||||
|
||||
const handleFilterChanged = useCallback(async () => {
|
||||
@@ -416,6 +476,81 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
serverPaginationData?.agGridFilterModel,
|
||||
]);
|
||||
|
||||
// Captures the "current view" (post-filter/sort, all rows across all
|
||||
// pages) for the "Export Current View" menu, mirroring Table V1's
|
||||
// clientView snapshot. Client-side mode only: in server pagination mode
|
||||
// the grid only ever holds a single page's rows, so a client-derived
|
||||
// snapshot can't represent the full filtered/sorted result and export
|
||||
// falls back to a fresh backend query instead (see useExploreAdditionalActionsMenu).
|
||||
const lastClientViewSignatureRef = useRef<string | null>(null);
|
||||
// Unlike handleGridStateChange's columnState/sortModel/filterModel,
|
||||
// clientView is excluded from ownState re-query comparisons on both the
|
||||
// Explore (ExploreViewContainer) and dashboard (activeAllDashboardFilters)
|
||||
// paths, so publishing it - including the very first snapshot right
|
||||
// after mount - can't trigger a requery/remount loop. It's therefore
|
||||
// always persisted below rather than having its initial value skipped;
|
||||
// skipping it would leave "Export Current View" without a snapshot to
|
||||
// export until some later grid event changes the signature.
|
||||
// Debounced (like handleGridStateChange below) because the full
|
||||
// filtered+sorted traversal is O(n) and onModelUpdated can fire rapidly
|
||||
// in succession (e.g. while typing into a quick filter); only the
|
||||
// trailing update needs to recompute the snapshot.
|
||||
const handleModelUpdated = useCallback(
|
||||
debounce(() => {
|
||||
if (serverPagination || !onClientViewChange || !gridRef.current?.api) {
|
||||
return;
|
||||
}
|
||||
const { api } = gridRef.current;
|
||||
const displayedColumns = api
|
||||
.getAllDisplayedColumns()
|
||||
.filter(column => column.getColId() !== ROW_NUMBER_COL_ID);
|
||||
const columns = displayedColumns.map(column => {
|
||||
const colDef = column.getColDef();
|
||||
// For comparison columns, colId has "Main " stripped for display,
|
||||
// but row data is still keyed by the unstripped original field
|
||||
// (colDef.context.dataKey, set in useColDefs) -- use that to read
|
||||
// row values so exported rows aren't blank for the main metric.
|
||||
const dataKey = colDef.context?.dataKey ?? column.getColId();
|
||||
return {
|
||||
key: dataKey,
|
||||
label: colDef.headerName || column.getColId(),
|
||||
};
|
||||
});
|
||||
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
api.forEachNodeAfterFilterAndSort(node => {
|
||||
if (node.data) {
|
||||
rows.push(node.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Without a getRowId callback, AG Grid's node ids are purely
|
||||
// positional and reset to 0..n-1 on every setRowData call, so they
|
||||
// don't identify a row's content across a data refresh — hashing
|
||||
// the actual filtered+sorted row content (which this function
|
||||
// already has to visit to build `rows`) is what actually detects
|
||||
// both value changes (e.g. a refresh with the same row count) and
|
||||
// order changes (e.g. a pure sort), not just count/column changes.
|
||||
const signature = `${JSON.stringify(rows)}|${columns.map(c => c.key).join(',')}`;
|
||||
|
||||
if (signature === lastClientViewSignatureRef.current) {
|
||||
return;
|
||||
}
|
||||
lastClientViewSignatureRef.current = signature;
|
||||
onClientViewChange({ rows, columns, count: rows.length });
|
||||
}, Constants.SLOW_DEBOUNCE),
|
||||
[serverPagination, onClientViewChange],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
// Cleanup debounced client-view snapshot capture
|
||||
() => {
|
||||
handleModelUpdated.cancel();
|
||||
},
|
||||
[handleModelUpdated],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
hasServerPageLengthChanged &&
|
||||
@@ -436,14 +571,32 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
}
|
||||
}, [width]);
|
||||
|
||||
// Row highlighting must reflect the active cross filter regardless of how
|
||||
// it was applied (cell click, context menu, or an external dashboard
|
||||
// filter), so it survives re-renders and server-side re-queries rather
|
||||
// than only reflecting whichever handler last called setSelected.
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!filters || Object.keys(filters).length === 0) &&
|
||||
gridRef.current?.api?.getSelectedRows().length
|
||||
) {
|
||||
gridRef.current.api.deselectAll();
|
||||
const api = gridRef.current?.api;
|
||||
if (!api) return;
|
||||
|
||||
if (!filters || Object.keys(filters).length === 0) {
|
||||
if (api.getSelectedRows().length) {
|
||||
api.deselectAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
if (!isActiveFilterValue) return;
|
||||
|
||||
api.forEachNode(node => {
|
||||
const matches = Object.keys(filters).some(key =>
|
||||
isActiveFilterValue(key, node.data?.[key] as DataRecordValue),
|
||||
);
|
||||
if (node.isSelected() !== matches) {
|
||||
node.setSelected(matches, false, 'api');
|
||||
}
|
||||
});
|
||||
}, [filters, isActiveFilterValue, rowData]);
|
||||
|
||||
const onGridReady = (params: GridReadyEvent) => {
|
||||
// This will make columns fill the grid width
|
||||
@@ -511,126 +664,130 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ThemedAgGridReact
|
||||
ref={gridRef}
|
||||
onGridReady={onGridReady}
|
||||
className="ag-container"
|
||||
rowData={rowData}
|
||||
headerHeight={36}
|
||||
rowHeight={30}
|
||||
columnDefs={colDefsFromProps}
|
||||
defaultColDef={defaultColDef}
|
||||
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
|
||||
rowSelection="multiple"
|
||||
animateRows
|
||||
onCellClicked={handleCellClicked}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onStateUpdated={handleGridStateChange}
|
||||
initialState={gridInitialState}
|
||||
maintainColumnOrder
|
||||
suppressAggFuncInHeader
|
||||
// Clicking a cell should select (focus) the cell rather than select
|
||||
// its text content (#106389). enableCellTextSelection forces browser
|
||||
// text selection on click, which suppresses the cell-focus behavior.
|
||||
// Because the Enterprise clipboard module isn't registered, native
|
||||
// text selection was the only way to copy a value, so onCellKeyDown
|
||||
// (above) restores Ctrl/Cmd+C copy for the focused cell. Full
|
||||
// multi-cell range selection still requires AG Grid Enterprise, which
|
||||
// is not available in the Community build used here.
|
||||
enableCellTextSelection={false}
|
||||
quickFilterText={serverPagination ? '' : quickFilterText}
|
||||
suppressMovableColumns={!allowRearrangeColumns}
|
||||
pagination={pagination}
|
||||
paginationPageSize={pageSize}
|
||||
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
|
||||
suppressDragLeaveHidesColumns
|
||||
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
|
||||
tooltipShowDelay={500}
|
||||
localeText={{
|
||||
// Pagination controls
|
||||
next: t('Next'),
|
||||
previous: t('Previous'),
|
||||
page: t('Page'),
|
||||
more: t('More'),
|
||||
to: t('to'),
|
||||
of: t('of'),
|
||||
first: t('First'),
|
||||
last: t('Last'),
|
||||
loadingOoo: t('Loading...'),
|
||||
// Set Filter
|
||||
selectAll: t('Select All'),
|
||||
searchOoo: t('Search...'),
|
||||
blanks: t('Blanks'),
|
||||
// Filter operations
|
||||
filterOoo: t('Filter'),
|
||||
applyFilter: t('Apply Filter'),
|
||||
equals: t('Equals'),
|
||||
notEqual: t('Not Equal'),
|
||||
lessThan: t('Less Than'),
|
||||
greaterThan: t('Greater Than'),
|
||||
lessThanOrEqual: t('Less Than or Equal'),
|
||||
greaterThanOrEqual: t('Greater Than or Equal'),
|
||||
inRange: t('In Range'),
|
||||
contains: t('Contains'),
|
||||
notContains: t('Not Contains'),
|
||||
startsWith: t('Starts With'),
|
||||
endsWith: t('Ends With'),
|
||||
// Logical conditions
|
||||
andCondition: t('AND'),
|
||||
orCondition: t('OR'),
|
||||
// Panel and group labels
|
||||
group: t('Group'),
|
||||
columns: t('Columns'),
|
||||
filters: t('Filters'),
|
||||
valueColumns: t('Value Columns'),
|
||||
pivotMode: t('Pivot Mode'),
|
||||
groups: t('Groups'),
|
||||
values: t('Values'),
|
||||
pivots: t('Pivots'),
|
||||
toolPanelButton: t('Tool Panel'),
|
||||
// Enterprise menu items
|
||||
pinColumn: t('Pin Column'),
|
||||
valueAggregation: t('Value Aggregation'),
|
||||
autosizeThiscolumn: t('Autosize This Column'),
|
||||
autosizeAllColumns: t('Autosize All Columns'),
|
||||
groupBy: t('Group By'),
|
||||
ungroupBy: t('Ungroup By'),
|
||||
resetColumns: t('Reset Columns'),
|
||||
expandAll: t('Expand All'),
|
||||
collapseAll: t('Collapse All'),
|
||||
toolPanel: t('Tool Panel'),
|
||||
export: t('Export'),
|
||||
csvExport: t('CSV Export'),
|
||||
excelExport: t('Excel Export'),
|
||||
excelXmlExport: t('Excel XML Export'),
|
||||
// Aggregation functions
|
||||
sum: t('Sum'),
|
||||
min: t('Min'),
|
||||
max: t('Max'),
|
||||
none: t('None'),
|
||||
count: t('Count'),
|
||||
average: t('Average'),
|
||||
// Standard menu items
|
||||
copy: t('Copy'),
|
||||
copyWithHeaders: t('Copy with Headers'),
|
||||
paste: t('Paste'),
|
||||
// Column menu and sorting
|
||||
sortAscending: t('Sort Ascending'),
|
||||
sortDescending: t('Sort Descending'),
|
||||
sortUnSort: t('Clear Sort'),
|
||||
}}
|
||||
context={{
|
||||
onColumnHeaderClicked: handleColumnHeaderClick,
|
||||
initialSortState: getInitialSortState(
|
||||
serverPaginationData?.sortBy || [],
|
||||
),
|
||||
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
|
||||
lastFilteredInputPosition:
|
||||
serverPaginationData?.lastFilteredInputPosition,
|
||||
}}
|
||||
/>
|
||||
<div style={gridFlexStyles}>
|
||||
<ThemedAgGridReact
|
||||
ref={gridRef}
|
||||
onGridReady={onGridReady}
|
||||
className="ag-container"
|
||||
rowData={rowData}
|
||||
headerHeight={36}
|
||||
rowHeight={30}
|
||||
columnDefs={colDefsFromProps}
|
||||
defaultColDef={defaultColDef}
|
||||
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
|
||||
rowSelection="multiple"
|
||||
animateRows
|
||||
onCellClicked={handleCellClicked}
|
||||
onCellContextMenu={handleCellContextMenu}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onModelUpdated={handleModelUpdated}
|
||||
onStateUpdated={handleGridStateChange}
|
||||
initialState={gridInitialState}
|
||||
maintainColumnOrder
|
||||
suppressAggFuncInHeader
|
||||
// Clicking a cell should select (focus) the cell rather than select
|
||||
// its text content (#106389). enableCellTextSelection forces browser
|
||||
// text selection on click, which suppresses the cell-focus behavior.
|
||||
// Because the Enterprise clipboard module isn't registered, native
|
||||
// text selection was the only way to copy a value, so onCellKeyDown
|
||||
// (above) restores Ctrl/Cmd+C copy for the focused cell. Full
|
||||
// multi-cell range selection still requires AG Grid Enterprise, which
|
||||
// is not available in the Community build used here.
|
||||
enableCellTextSelection={false}
|
||||
quickFilterText={serverPagination ? '' : quickFilterText}
|
||||
suppressMovableColumns={!allowRearrangeColumns}
|
||||
pagination={pagination}
|
||||
paginationPageSize={pageSize}
|
||||
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
|
||||
suppressDragLeaveHidesColumns
|
||||
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
|
||||
tooltipShowDelay={500}
|
||||
localeText={{
|
||||
// Pagination controls
|
||||
next: t('Next'),
|
||||
previous: t('Previous'),
|
||||
page: t('Page'),
|
||||
more: t('More'),
|
||||
to: t('to'),
|
||||
of: t('of'),
|
||||
first: t('First'),
|
||||
last: t('Last'),
|
||||
loadingOoo: t('Loading...'),
|
||||
// Set Filter
|
||||
selectAll: t('Select All'),
|
||||
searchOoo: t('Search...'),
|
||||
blanks: t('Blanks'),
|
||||
// Filter operations
|
||||
filterOoo: t('Filter'),
|
||||
applyFilter: t('Apply Filter'),
|
||||
equals: t('Equals'),
|
||||
notEqual: t('Not Equal'),
|
||||
lessThan: t('Less Than'),
|
||||
greaterThan: t('Greater Than'),
|
||||
lessThanOrEqual: t('Less Than or Equal'),
|
||||
greaterThanOrEqual: t('Greater Than or Equal'),
|
||||
inRange: t('In Range'),
|
||||
contains: t('Contains'),
|
||||
notContains: t('Not Contains'),
|
||||
startsWith: t('Starts With'),
|
||||
endsWith: t('Ends With'),
|
||||
// Logical conditions
|
||||
andCondition: t('AND'),
|
||||
orCondition: t('OR'),
|
||||
// Panel and group labels
|
||||
group: t('Group'),
|
||||
columns: t('Columns'),
|
||||
filters: t('Filters'),
|
||||
valueColumns: t('Value Columns'),
|
||||
pivotMode: t('Pivot Mode'),
|
||||
groups: t('Groups'),
|
||||
values: t('Values'),
|
||||
pivots: t('Pivots'),
|
||||
toolPanelButton: t('Tool Panel'),
|
||||
// Enterprise menu items
|
||||
pinColumn: t('Pin Column'),
|
||||
valueAggregation: t('Value Aggregation'),
|
||||
autosizeThiscolumn: t('Autosize This Column'),
|
||||
autosizeAllColumns: t('Autosize All Columns'),
|
||||
groupBy: t('Group By'),
|
||||
ungroupBy: t('Ungroup By'),
|
||||
resetColumns: t('Reset Columns'),
|
||||
expandAll: t('Expand All'),
|
||||
collapseAll: t('Collapse All'),
|
||||
toolPanel: t('Tool Panel'),
|
||||
export: t('Export'),
|
||||
csvExport: t('CSV Export'),
|
||||
excelExport: t('Excel Export'),
|
||||
excelXmlExport: t('Excel XML Export'),
|
||||
// Aggregation functions
|
||||
sum: t('Sum'),
|
||||
min: t('Min'),
|
||||
max: t('Max'),
|
||||
none: t('None'),
|
||||
count: t('Count'),
|
||||
average: t('Average'),
|
||||
// Standard menu items
|
||||
copy: t('Copy'),
|
||||
copyWithHeaders: t('Copy with Headers'),
|
||||
paste: t('Paste'),
|
||||
// Column menu and sorting
|
||||
sortAscending: t('Sort Ascending'),
|
||||
sortDescending: t('Sort Descending'),
|
||||
sortUnSort: t('Clear Sort'),
|
||||
}}
|
||||
context={{
|
||||
onColumnHeaderClicked: handleColumnHeaderClick,
|
||||
initialSortState: getInitialSortState(
|
||||
serverPaginationData?.sortBy || [],
|
||||
),
|
||||
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
|
||||
lastFilteredInputPosition:
|
||||
serverPaginationData?.lastFilteredInputPosition,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{serverPagination && (
|
||||
<Pagination
|
||||
currentPage={serverPaginationData?.currentPage || 0}
|
||||
|
||||
@@ -18,16 +18,28 @@
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
BinaryQueryObjectFilterClause,
|
||||
DataRecord,
|
||||
DataRecordValue,
|
||||
DateWithFormatter,
|
||||
extractTextFromHTML,
|
||||
getTimeFormatterForGranularity,
|
||||
isEmptyDateInput,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { debounce, isEqual } from 'lodash-es';
|
||||
|
||||
import {
|
||||
CellClickedEvent,
|
||||
CellContextMenuEvent,
|
||||
SelectionChangedEvent,
|
||||
} from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import {
|
||||
@@ -37,20 +49,18 @@ import {
|
||||
SortByItem,
|
||||
} from './types';
|
||||
import AgGridDataTable from './AgGridTable';
|
||||
import { updateTableOwnState } from './utils/externalAPIs';
|
||||
import { updateTableOwnState, ClientViewSnapshot } from './utils/externalAPIs';
|
||||
import TimeComparisonVisibility from './AgGridTable/components/TimeComparisonVisibility';
|
||||
import { useColDefs } from './utils/useColDefs';
|
||||
import { buildSelectionCrossFilterDataMask } from './utils/getCrossFilterDataMask';
|
||||
import {
|
||||
buildSelectionCrossFilterDataMask,
|
||||
getCrossFilterDataMask,
|
||||
} from './utils/getCrossFilterDataMask';
|
||||
import { StyledChartContainer } from './styles';
|
||||
import type { FilterState } from './utils/filterStateManager';
|
||||
|
||||
const getGridHeight = (height: number, includeSearch: boolean | undefined) => {
|
||||
let calculatedGridHeight = height;
|
||||
if (includeSearch) {
|
||||
calculatedGridHeight -= 16;
|
||||
}
|
||||
return calculatedGridHeight - 80;
|
||||
};
|
||||
import { formatColumnValue } from './utils/formatValue';
|
||||
import getTimeRangeFromGranularity from './utils/getTimeRangeFromGranularity';
|
||||
import getScrollBarSize from './utils/getScrollBarSize';
|
||||
|
||||
export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
props: AgGridTableChartTransformedProps<D> & {},
|
||||
@@ -61,6 +71,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
data,
|
||||
includeSearch,
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
pageSize,
|
||||
serverPagination,
|
||||
rowCount,
|
||||
@@ -88,8 +99,60 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
metricSqlExpressions,
|
||||
rawSummaryColumns,
|
||||
showNumberedColumn,
|
||||
onContextMenu,
|
||||
formData,
|
||||
} = props;
|
||||
|
||||
// The dashboard's layout engine reports a burst of close-but-not-identical
|
||||
// width/height values while it settles on initial load. Committing each
|
||||
// intermediate value resizes the chart container and re-fits AG Grid's
|
||||
// columns once per value; for any column with wrapText/autoHeight (the
|
||||
// default - see useColDefs), each re-fit can flip a borderline cell across
|
||||
// its wrap boundary and change that row's height, which is what actually
|
||||
// reads as "flicker" rather than the container resize itself.
|
||||
//
|
||||
// A scrollbar-sized threshold (matching plugin-chart-table/v1's guard)
|
||||
// filters out sub-pixel noise, but genuine multi-step settling still gets
|
||||
// through as several real width values in quick succession. Debouncing
|
||||
// every commit after the first collapses that burst into the single final
|
||||
// value once it stops changing, while still painting the first available
|
||||
// size immediately so the chart isn't blank while it waits.
|
||||
const [tableSize, setTableSize] = useState({ width: 0, height: 0 });
|
||||
const hasCommittedInitialSize = useRef(false);
|
||||
|
||||
const debouncedSetTableSize = useMemo(
|
||||
() =>
|
||||
debounce((size: { width: number; height: number }) => {
|
||||
setTableSize(size);
|
||||
}, 250),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
// Cleanup debounced size commit
|
||||
() => {
|
||||
debouncedSetTableSize.cancel();
|
||||
},
|
||||
[debouncedSetTableSize],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const scrollBarSize = getScrollBarSize();
|
||||
const sizeChanged =
|
||||
Math.abs(width - tableSize.width) > scrollBarSize ||
|
||||
Math.abs(height - tableSize.height) > scrollBarSize;
|
||||
if (!sizeChanged) {
|
||||
return;
|
||||
}
|
||||
if (!hasCommittedInitialSize.current) {
|
||||
hasCommittedInitialSize.current = true;
|
||||
setTableSize({ width, height });
|
||||
} else {
|
||||
debouncedSetTableSize({ width, height });
|
||||
}
|
||||
}, [width, height, tableSize, debouncedSetTableSize]);
|
||||
|
||||
const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
|
||||
|
||||
// Extract metric column names for SQL conversion
|
||||
@@ -114,6 +177,27 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
// Tracks the most recently written ownState so that writes triggered
|
||||
// asynchronously (e.g. clientView from AG Grid's onModelUpdated, which can
|
||||
// fire with a stale closure) merge onto the latest known state instead of
|
||||
// a stale render-time serverPaginationData snapshot. updateTableOwnState
|
||||
// replaces ownState wholesale, so merging at write time - rather than at
|
||||
// render time - is what keeps concurrent writers from clobbering one
|
||||
// another's keys.
|
||||
const ownStateRef = useRef(serverPaginationData);
|
||||
useEffect(() => {
|
||||
ownStateRef.current = serverPaginationData;
|
||||
}, [serverPaginationData]);
|
||||
|
||||
const writeOwnState = useCallback(
|
||||
(patch: Record<string, unknown>) => {
|
||||
const nextOwnState = { ...ownStateRef.current, ...patch };
|
||||
ownStateRef.current = nextOwnState;
|
||||
updateTableOwnState(setDataMask, nextOwnState);
|
||||
},
|
||||
[setDataMask],
|
||||
);
|
||||
|
||||
// A single effect owns every ownState write derived from render state.
|
||||
// updateTableOwnState replaces ownState wholesale, so separate effects that
|
||||
// each spread serverPaginationData in the same render would clobber one
|
||||
@@ -121,7 +205,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
// columns and nudging a re-query for missing totals must be one combined
|
||||
// delta.
|
||||
useEffect(() => {
|
||||
const nextOwnState = { ...serverPaginationData };
|
||||
const patch: Record<string, unknown> = {};
|
||||
let changed = false;
|
||||
|
||||
if (serverPagination && serverPaginationData && rowCount !== undefined) {
|
||||
@@ -132,7 +216,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
// last remaining page.
|
||||
const clampedPage = Math.max(0, Math.min(currentPage, totalPages - 1));
|
||||
if (clampedPage !== currentPage) {
|
||||
nextOwnState.currentPage = clampedPage;
|
||||
patch.currentPage = clampedPage;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -140,22 +224,22 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
const primed = (serverPaginationData?.rawSummaryColumns ?? []) as string[];
|
||||
const requested = Boolean(serverPaginationData?.totalsRequested);
|
||||
if (isRawRecords && showTotals && !isEqual(primed, rawSummaryColumns)) {
|
||||
nextOwnState.rawSummaryColumns = rawSummaryColumns;
|
||||
patch.rawSummaryColumns = rawSummaryColumns;
|
||||
changed = true;
|
||||
}
|
||||
// A renderTrigger toggle re-renders without re-querying; requesting totals
|
||||
// through ownState dispatches the standard re-query whose buildQuery
|
||||
// carries the totals query for the active mode.
|
||||
if (showTotals && totals === undefined && !requested) {
|
||||
nextOwnState.totalsRequested = true;
|
||||
patch.totalsRequested = true;
|
||||
changed = true;
|
||||
} else if (!showTotals && requested) {
|
||||
nextOwnState.totalsRequested = false;
|
||||
patch.totalsRequested = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updateTableOwnState(setDataMask, nextOwnState);
|
||||
writeOwnState(patch);
|
||||
}
|
||||
}, [
|
||||
serverPagination,
|
||||
@@ -166,7 +250,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
totals,
|
||||
rawSummaryColumns,
|
||||
serverPaginationData,
|
||||
setDataMask,
|
||||
writeOwnState,
|
||||
]);
|
||||
|
||||
const comparisonColumns = [
|
||||
@@ -209,8 +293,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
|
||||
// Prepare modified own state for server pagination
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
writeOwnState({
|
||||
agGridFilterModel:
|
||||
completeFilterState.originalFilterModel &&
|
||||
Object.keys(completeFilterState.originalFilterModel).length > 0
|
||||
@@ -223,14 +306,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
lastFilteredInputPosition: completeFilterState.inputPosition,
|
||||
currentPage: 0, // Reset to first page when filtering
|
||||
metricSqlExpressions,
|
||||
};
|
||||
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
},
|
||||
[
|
||||
setDataMask,
|
||||
writeOwnState,
|
||||
serverPagination,
|
||||
serverPaginationData,
|
||||
onChartStateChange,
|
||||
chartState,
|
||||
metricSqlExpressions,
|
||||
@@ -273,15 +353,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
colorPositiveNegative,
|
||||
columnColorFormatters,
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
basicColorFormatters,
|
||||
isUsingTimeComparison,
|
||||
emitCrossFilters,
|
||||
alignPositiveNegative,
|
||||
slice_id,
|
||||
conditionalFormatting: formData?.conditional_formatting,
|
||||
comparisonColorEnabled: formData?.comparison_color_enabled,
|
||||
comparisonColorScheme: formData?.comparison_color_scheme,
|
||||
});
|
||||
|
||||
const gridHeight = getGridHeight(height, includeSearch);
|
||||
|
||||
const isActiveFilterValue = useCallback(
|
||||
function isActiveFilterValue(key: string, val: DataRecordValue) {
|
||||
if (!filters || !filters[key]) return false;
|
||||
@@ -348,7 +430,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
|
||||
const handleSelectionChanged = useCallback(
|
||||
(event: SelectionChangedEvent) => {
|
||||
if (!emitCrossFilters || !activeColumnRef.current) return;
|
||||
// Selection changes triggered by the highlight-sync effect (source
|
||||
// 'api') reflect a filter that was already applied elsewhere (context
|
||||
// menu, dashboard filter, etc.), so re-deriving and re-dispatching a
|
||||
// mask from them here would use a stale activeColumnRef and could
|
||||
// clobber that filter with the wrong column.
|
||||
if (
|
||||
!emitCrossFilters ||
|
||||
!activeColumnRef.current ||
|
||||
event.source === 'api'
|
||||
)
|
||||
return;
|
||||
|
||||
const key = activeColumnRef.current;
|
||||
const selectedRows = event.api.getSelectedRows();
|
||||
@@ -368,75 +460,204 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[emitCrossFilters, setDataMask, timeGrain, timestampFormatter],
|
||||
);
|
||||
|
||||
const drillColumns = isUsingTimeComparison
|
||||
? (filteredColumns as InputColumn[])
|
||||
: (columns as InputColumn[]);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event: CellContextMenuEvent) => {
|
||||
if (!onContextMenu || isRawRecords || !event.column || !event.data) {
|
||||
return;
|
||||
}
|
||||
const nativeEvent = event.event as MouseEvent | null | undefined;
|
||||
if (!nativeEvent) return;
|
||||
nativeEvent.preventDefault();
|
||||
nativeEvent.stopPropagation();
|
||||
|
||||
const rowData = event.data as Record<string, DataRecordValue>;
|
||||
const key = event.column.getColId();
|
||||
const cellValue = event.value as DataRecordValue;
|
||||
const colDef = event.column.getColDef();
|
||||
const isMetric = Boolean(
|
||||
colDef.context?.isMetric || colDef.context?.isPercentMetric,
|
||||
);
|
||||
|
||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
drillColumns.forEach(col => {
|
||||
if (col.isMetric || col.isPercentMetric) return;
|
||||
const dataRecordValue = rowData[col.key];
|
||||
|
||||
if (
|
||||
dataRecordValue == null ||
|
||||
(dataRecordValue instanceof DateWithFormatter &&
|
||||
isEmptyDateInput(dataRecordValue.input))
|
||||
) {
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: 'IS NULL' as any,
|
||||
val: null,
|
||||
});
|
||||
} else if (col.dataType === GenericDataType.Temporal && timeGrain) {
|
||||
const startTime =
|
||||
dataRecordValue instanceof Date
|
||||
? dataRecordValue
|
||||
: new Date(dataRecordValue as string | number);
|
||||
|
||||
if (Number.isNaN(startTime.getTime())) {
|
||||
// Malformed temporal value: fall back to an equality filter
|
||||
// instead of building a TEMPORAL_RANGE, since toISOString()
|
||||
// throws on an Invalid Date and would crash the context menu.
|
||||
const sanitizedValue = extractTextFromHTML(dataRecordValue);
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: '==',
|
||||
val: sanitizedValue as string | number | boolean,
|
||||
formattedVal: formatColumnValue(col, sanitizedValue)[1],
|
||||
});
|
||||
} else {
|
||||
const [rangeStartTime, rangeEndTime] = getTimeRangeFromGranularity(
|
||||
startTime,
|
||||
timeGrain,
|
||||
);
|
||||
const timeRangeValue = `${rangeStartTime.toISOString()} : ${rangeEndTime.toISOString()}`;
|
||||
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: 'TEMPORAL_RANGE',
|
||||
val: timeRangeValue,
|
||||
grain: timeGrain,
|
||||
formattedVal: formatColumnValue(col, dataRecordValue)[1],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const sanitizedValue = extractTextFromHTML(dataRecordValue);
|
||||
drillToDetailFilters.push({
|
||||
col: col.key,
|
||||
op: '==',
|
||||
val: sanitizedValue as string | number | boolean,
|
||||
formattedVal: formatColumnValue(col, sanitizedValue)[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const isCellValueNull =
|
||||
cellValue == null ||
|
||||
(cellValue instanceof DateWithFormatter &&
|
||||
isEmptyDateInput(cellValue.input));
|
||||
|
||||
onContextMenu(nativeEvent.clientX, nativeEvent.clientY, {
|
||||
drillToDetail: drillToDetailFilters,
|
||||
crossFilter: isMetric
|
||||
? undefined
|
||||
: getCrossFilterDataMask({
|
||||
key,
|
||||
value: cellValue,
|
||||
filters,
|
||||
timeGrain,
|
||||
isActiveFilterValue,
|
||||
timestampFormatter,
|
||||
}),
|
||||
drillBy: isMetric
|
||||
? undefined
|
||||
: {
|
||||
filters: [
|
||||
isCellValueNull
|
||||
? { col: key, op: 'IS NULL' as any, val: null }
|
||||
: {
|
||||
col: key,
|
||||
op: '==' as any,
|
||||
val: extractTextFromHTML(cellValue),
|
||||
},
|
||||
],
|
||||
groupbyFieldName: 'groupby',
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
onContextMenu,
|
||||
isRawRecords,
|
||||
drillColumns,
|
||||
timeGrain,
|
||||
filters,
|
||||
isActiveFilterValue,
|
||||
timestampFormatter,
|
||||
],
|
||||
);
|
||||
|
||||
const handleServerPaginationChange = useCallback(
|
||||
(pageNumber: number, pageSize: number) => {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
writeOwnState({
|
||||
currentPage: pageNumber,
|
||||
pageSize,
|
||||
lastFilteredColumn: undefined,
|
||||
lastFilteredInputPosition: undefined,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
},
|
||||
[setDataMask],
|
||||
[writeOwnState],
|
||||
);
|
||||
|
||||
const handlePageSizeChange = useCallback(
|
||||
(pageSize: number) => {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
writeOwnState({
|
||||
currentPage: 0,
|
||||
pageSize,
|
||||
lastFilteredColumn: undefined,
|
||||
lastFilteredInputPosition: undefined,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
},
|
||||
[setDataMask],
|
||||
[writeOwnState],
|
||||
);
|
||||
|
||||
const handleChangeSearchCol = (searchCol: string) => {
|
||||
if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
if (!isEqual(searchCol, ownStateRef.current?.searchColumn)) {
|
||||
writeOwnState({
|
||||
searchColumn: searchCol,
|
||||
searchText: '',
|
||||
currentPage: 0, // Reset to first page when the search column changes
|
||||
lastFilteredColumn: undefined,
|
||||
lastFilteredInputPosition: undefined,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchText: string) => {
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
writeOwnState({
|
||||
searchColumn:
|
||||
serverPaginationData?.searchColumn || searchOptions[0]?.value,
|
||||
(ownStateRef.current?.searchColumn as string | undefined) ||
|
||||
searchOptions[0]?.value,
|
||||
searchText,
|
||||
currentPage: 0, // Reset to first page when searching
|
||||
lastFilteredColumn: undefined,
|
||||
lastFilteredInputPosition: undefined,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
},
|
||||
[setDataMask, searchOptions],
|
||||
[writeOwnState, searchOptions],
|
||||
);
|
||||
|
||||
const handleSortByChange = useCallback(
|
||||
(sortBy: SortByItem[]) => {
|
||||
if (!serverPagination) return;
|
||||
const modifiedOwnState = {
|
||||
...serverPaginationData,
|
||||
writeOwnState({
|
||||
sortBy,
|
||||
lastFilteredColumn: undefined,
|
||||
lastFilteredInputPosition: undefined,
|
||||
};
|
||||
updateTableOwnState(setDataMask, modifiedOwnState);
|
||||
});
|
||||
},
|
||||
[setDataMask, serverPagination],
|
||||
[writeOwnState, serverPagination],
|
||||
);
|
||||
|
||||
// Feeds the "Export Current View" menu item (EXPORT_CURRENT_VIEW behavior),
|
||||
// mirroring Table V1's clientView snapshot on ownState. Written through
|
||||
// writeOwnState (rather than spreading serverPaginationData directly)
|
||||
// because onModelUpdated can fire with a stale closure relative to other
|
||||
// ownState writers (e.g. a just-applied filter), and updateTableOwnState
|
||||
// replaces ownState wholesale.
|
||||
const handleClientViewChange = useCallback(
|
||||
(clientView: ClientViewSnapshot) => {
|
||||
writeOwnState({ clientView });
|
||||
},
|
||||
[writeOwnState],
|
||||
);
|
||||
|
||||
const renderTimeComparisonVisibility = (): JSX.Element => (
|
||||
@@ -455,9 +676,22 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
.join('|');
|
||||
|
||||
return (
|
||||
<StyledChartContainer height={height}>
|
||||
<StyledChartContainer
|
||||
height={tableSize.height}
|
||||
onContextMenu={event => {
|
||||
// Safety net: AG Grid only calls handleContextMenu (which calls
|
||||
// preventDefault) when it resolves the native contextmenu event to
|
||||
// a cell. If that per-cell resolution ever misses - e.g. a second,
|
||||
// near-duplicate contextmenu event dispatched in quick succession by
|
||||
// some mice's right-button switches - the event still bubbles
|
||||
// through this container, so the browser's native menu is
|
||||
// suppressed here regardless of whether AG Grid's own handler ran.
|
||||
if (!isRawRecords) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgGridDataTable
|
||||
gridHeight={gridHeight}
|
||||
key={descriptionsKey}
|
||||
data={data || []}
|
||||
colDefsFromProps={colDefs}
|
||||
@@ -478,8 +712,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
metricColumns={metricColumns}
|
||||
id={slice_id}
|
||||
handleCellClicked={handleCellClicked}
|
||||
handleCellContextMenu={handleContextMenu}
|
||||
handleSelectionChanged={handleSelectionChanged}
|
||||
filters={filters}
|
||||
isActiveFilterValue={isActiveFilterValue}
|
||||
percentMetrics={percentMetrics}
|
||||
serverPageLength={serverPageLength}
|
||||
hasServerPageLengthChanged={hasServerPageLengthChanged}
|
||||
@@ -490,9 +726,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
showTotals={
|
||||
showTotals && totals !== undefined && Object.keys(totals).length > 0
|
||||
}
|
||||
width={width}
|
||||
width={tableSize.width}
|
||||
onColumnStateChange={handleColumnStateChange}
|
||||
chartState={chartState}
|
||||
onClientViewChange={handleClientViewChange}
|
||||
/>
|
||||
</StyledChartContainer>
|
||||
);
|
||||
|
||||
@@ -674,6 +674,29 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
|
||||
}
|
||||
}
|
||||
|
||||
// Build the "all records" percent-metric denominator query AFTER all
|
||||
// filter mutations (interactive group-by, search, AG Grid WHERE/HAVING)
|
||||
// above, so its denominator reflects the same filtered result set as the
|
||||
// main query instead of a stale pre-filter snapshot.
|
||||
const calculationMode = formData.percent_metric_calculation || 'row_limit';
|
||||
|
||||
if (
|
||||
calculationMode === 'all_records' &&
|
||||
percentMetrics &&
|
||||
percentMetrics.length > 0
|
||||
) {
|
||||
extraQueries.push({
|
||||
...queryObject,
|
||||
columns: [],
|
||||
metrics: percentMetrics,
|
||||
post_processing: [],
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
orderby: [],
|
||||
is_timeseries: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Create totals query AFTER all filters (including AG Grid filters) are applied
|
||||
// This ensures we can properly exclude AG Grid WHERE filters from the totals
|
||||
// In raw records mode the summary is a SUM over the numeric columns primed
|
||||
@@ -714,33 +737,21 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
|
||||
: undefined;
|
||||
|
||||
if (showAggregateTotals || rawSummaryColumns.length > 0) {
|
||||
// Create a copy of extras without the AG Grid WHERE clause
|
||||
// AG Grid filters in extras.where can reference calculated columns
|
||||
// which aren't available in the totals subquery
|
||||
const totalsExtras = { ...queryObject.extras };
|
||||
if (ownState.agGridComplexWhere) {
|
||||
// Remove AG Grid WHERE clause from totals query
|
||||
const whereClause = totalsExtras.where;
|
||||
if (whereClause) {
|
||||
// Remove the AG Grid filter part from the WHERE clause using string methods
|
||||
const agGridWhere = ownState.agGridComplexWhere;
|
||||
let newWhereClause = whereClause;
|
||||
|
||||
// Try to remove with " AND " before
|
||||
newWhereClause = newWhereClause.replace(` AND ${agGridWhere}`, '');
|
||||
// Try to remove with " AND " after
|
||||
newWhereClause = newWhereClause.replace(`${agGridWhere} AND `, '');
|
||||
// If it's the only clause, remove it entirely
|
||||
if (newWhereClause === agGridWhere) {
|
||||
newWhereClause = '';
|
||||
}
|
||||
|
||||
if (newWhereClause.trim()) {
|
||||
totalsExtras.where = newWhereClause;
|
||||
} else {
|
||||
delete totalsExtras.where;
|
||||
}
|
||||
}
|
||||
// Start from the original, pre-filter extras (captured before any
|
||||
// AG Grid WHERE/HAVING or download sqlClauses were merged in above)
|
||||
// rather than trying to subtract those fragments back out of the
|
||||
// now-combined `queryObject.extras` string. AG Grid filters can
|
||||
// reference calculated columns that aren't available once the
|
||||
// totals subquery drops all grouping columns (columns: []), and that
|
||||
// applies to HAVING just as much as WHERE, and to the download
|
||||
// sqlClauses path just as much as the live agGridComplexWhere path —
|
||||
// starting clean avoids having to special-case each source.
|
||||
const totalsExtras = { ...extras };
|
||||
if (!totalsExtras.where) {
|
||||
delete totalsExtras.where;
|
||||
}
|
||||
if (!totalsExtras.having) {
|
||||
delete totalsExtras.having;
|
||||
}
|
||||
|
||||
extraQueries.push({
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
shouldSkipMetricColumn,
|
||||
isRegularMetric,
|
||||
isPercentMetric,
|
||||
ConditionalFormattingConfig,
|
||||
ObjectFormattingEnum,
|
||||
ColorSchemeEnum,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
@@ -194,6 +196,23 @@ const percentMetricsControl: typeof sharedControls.metrics = {
|
||||
validators: [],
|
||||
};
|
||||
|
||||
const percentMetricCalculationControl: ControlConfig<'SelectControl'> = {
|
||||
type: 'SelectControl',
|
||||
label: t('Percentage metric calculation'),
|
||||
description: t(
|
||||
'Row Limit: percentages are calculated based on the subset of data retrieved, respecting the row limit. ' +
|
||||
'All Records: Percentages are calculated based on the total dataset, ignoring the row limit.',
|
||||
),
|
||||
default: 'row_limit',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['row_limit', t('Row limit')],
|
||||
['all_records', t('All records')],
|
||||
],
|
||||
visibility: isAggMode,
|
||||
renderTrigger: false,
|
||||
};
|
||||
|
||||
/*
|
||||
Options for row limit control
|
||||
*/
|
||||
@@ -431,6 +450,12 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'percent_metric_calculation',
|
||||
config: percentMetricCalculationControl,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -481,6 +506,36 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'allow_rearrange_columns',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Allow columns to be rearranged'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
"Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.",
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isEmpty(controls?.time_compare?.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'allow_render_html',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Render columns in HTML format'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Renders table cells as HTML when applicable. For example, HTML <a> tags will be rendered as hyperlinks.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -573,11 +628,14 @@ const config: ControlPanelConfig = {
|
||||
const updatedColtypes: GenericDataType[] = [];
|
||||
|
||||
colnames
|
||||
.map(
|
||||
(colname, index) => [colname, index] as [string, number],
|
||||
)
|
||||
.filter(
|
||||
colname =>
|
||||
([colname]) =>
|
||||
last(colname.split('__')) !== timeComparisonValue,
|
||||
)
|
||||
.forEach((colname, index) => {
|
||||
.forEach(([colname, originalIndex]) => {
|
||||
if (
|
||||
shouldSkipMetricColumn({
|
||||
colname,
|
||||
@@ -614,7 +672,12 @@ const config: ControlPanelConfig = {
|
||||
});
|
||||
} else {
|
||||
updatedColnames.push(colname);
|
||||
updatedColtypes.push(coltypes[index]);
|
||||
// Look up by the column's original position in
|
||||
// colnames/coltypes, not its position after the
|
||||
// filter above — those diverge whenever any
|
||||
// earlier column is a comparison-suffixed one that
|
||||
// got filtered out.
|
||||
updatedColtypes.push(coltypes[originalIndex]);
|
||||
childColumnMap[colname] = false;
|
||||
timeComparisonColumnMap[colname] = false;
|
||||
}
|
||||
@@ -749,24 +812,71 @@ const config: ControlPanelConfig = {
|
||||
: [];
|
||||
|
||||
const chartStatus = chart?.chartStatus;
|
||||
// Normalize legacy `toAllRow`/`toTextColor` flags saved before
|
||||
// `columnFormatting`/`objectFormatting` existed, so "entire row"
|
||||
// formatters set under the old schema keep working.
|
||||
const value = _?.value ?? [];
|
||||
if (value && Array.isArray(value)) {
|
||||
value.forEach(
|
||||
(item: ConditionalFormattingConfig, index, array) => {
|
||||
if (
|
||||
item.colorScheme &&
|
||||
(typeof item.colorScheme !== 'string' ||
|
||||
!['Green', 'Red'].includes(item.colorScheme))
|
||||
) {
|
||||
if (item.columnFormatting === undefined) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
array[index] = {
|
||||
...item,
|
||||
...(item.toTextColor === true && {
|
||||
objectFormatting: ObjectFormattingEnum.TEXT_COLOR,
|
||||
}),
|
||||
...(item.toAllRow === true && {
|
||||
columnFormatting: ObjectFormattingEnum.ENTIRE_ROW,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
const { colnames, coltypes } =
|
||||
chart?.queriesResponse?.[0] ?? {};
|
||||
const numericColumns =
|
||||
Array.isArray(colnames) && Array.isArray(coltypes)
|
||||
? colnames
|
||||
.filter(
|
||||
(colname: string, index: number) =>
|
||||
coltypes[index] === GenericDataType.Numeric,
|
||||
)
|
||||
.map((colname: string) => ({
|
||||
value: colname,
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap[colname] ?? colname),
|
||||
dataType:
|
||||
colnames && coltypes[colnames?.indexOf(colname)],
|
||||
}))
|
||||
: [];
|
||||
const hasColumns =
|
||||
Array.isArray(colnames) && Array.isArray(coltypes);
|
||||
const allColumns = hasColumns
|
||||
? [
|
||||
{
|
||||
value: ObjectFormattingEnum.ENTIRE_ROW,
|
||||
label: t('entire row'),
|
||||
dataType: GenericDataType.String,
|
||||
},
|
||||
...colnames.map((colname: string, index: number) => ({
|
||||
value: colname,
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap?.[colname] ?? colname),
|
||||
dataType: coltypes[index],
|
||||
})),
|
||||
]
|
||||
: [];
|
||||
const numericColumns = hasColumns
|
||||
? colnames
|
||||
.filter(
|
||||
(colname: string, index: number) =>
|
||||
coltypes[index] === GenericDataType.Numeric,
|
||||
)
|
||||
.map((colname: string) => ({
|
||||
value: colname,
|
||||
label: Array.isArray(verboseMap)
|
||||
? colname
|
||||
: (verboseMap?.[colname] ?? colname),
|
||||
// Every entry here already passed the Numeric filter
|
||||
// above, so the type is always Numeric — no need to
|
||||
// re-look it up (which breaks on duplicate colnames).
|
||||
dataType: GenericDataType.Numeric,
|
||||
}))
|
||||
: [];
|
||||
const columnOptions = hasTimeComparison
|
||||
? processComparisonColumns(
|
||||
numericColumns || [],
|
||||
@@ -778,6 +888,7 @@ const config: ControlPanelConfig = {
|
||||
removeIrrelevantConditions: chartStatus === 'success',
|
||||
columnOptions,
|
||||
verboseMap,
|
||||
allColumns,
|
||||
extraColorChoices,
|
||||
serverPagination: Boolean(
|
||||
explore?.controls?.server_pagination?.value,
|
||||
|
||||
@@ -44,6 +44,7 @@ const metadata = new ChartMetadata({
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
Behavior.DrillBy,
|
||||
'EXPORT_CURRENT_VIEW' as Behavior,
|
||||
],
|
||||
category: t('Table'),
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import {
|
||||
BackendOwnState,
|
||||
ChartStateConverterOptions,
|
||||
QuerySortBy,
|
||||
type AgGridChartState,
|
||||
type AgGridSortModel,
|
||||
@@ -353,7 +354,24 @@ export function convertFilterModel(
|
||||
*/
|
||||
export function convertAgGridStateToOwnState(
|
||||
agGridState: AgGridChartState,
|
||||
options: ChartStateConverterOptions = {},
|
||||
): Partial<BackendOwnState> {
|
||||
// In client mode, AG Grid handles sort/filter/pagination locally, so for
|
||||
// the *live* query none of it needs to reach the backend -- folding it
|
||||
// into ownState there would only trigger an unnecessary requery/remount.
|
||||
// A *download* query has no client-side pass to apply that state though:
|
||||
// dashboard doesn't consume the Explore-only clientView snapshot, so
|
||||
// exports still need it converted to reproduce the displayed
|
||||
// sort/filter/columns (options.forExport).
|
||||
//
|
||||
// Only an explicit `false` is treated as "definitely client mode":
|
||||
// legacy persisted table_state/permalinks predate serverPagination and
|
||||
// have it `undefined`, and treating that the same as `false` would
|
||||
// silently drop their persisted server-side sort/filter on restore.
|
||||
if (agGridState.serverPagination === false && !options.forExport) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ownState: Partial<BackendOwnState> = {};
|
||||
|
||||
const sortBy = convertSortModel(agGridState.sortModel);
|
||||
|
||||
@@ -184,6 +184,7 @@ export const PaginationContainer = styled.div`
|
||||
color: ${theme.colorTextBase};
|
||||
transform: translateY(-${theme.sizeUnit}px);
|
||||
background: ${theme.colorBgBase};
|
||||
flex-shrink: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -353,6 +354,7 @@ export const StyledChartContainer = styled.div<{
|
||||
.dropdown-controls-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-comparison-dropdown {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
getTimeFormatterForGranularity,
|
||||
normalizeCurrency,
|
||||
NumberFormats,
|
||||
QueryMode,
|
||||
SMART_DATE_ID,
|
||||
@@ -60,7 +61,11 @@ const { DATABASE_DATETIME } = TimeFormats;
|
||||
|
||||
function isNumeric(key: string, data: DataRecord[] = []) {
|
||||
return data.every(
|
||||
x => x[key] === null || x[key] === undefined || typeof x[key] === 'number',
|
||||
x =>
|
||||
x[key] === null ||
|
||||
x[key] === undefined ||
|
||||
x[key] === '' ||
|
||||
typeof x[key] === 'number',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +173,33 @@ const getComparisonColFormatter = (
|
||||
return formatter;
|
||||
};
|
||||
|
||||
const processComparisonDataRecords = memoizeOne(
|
||||
// transformProps is a single module-level function shared by every mounted
|
||||
// instance of this chart plugin on a dashboard (one plugin registration,
|
||||
// not one per chart). memoizeOne only remembers the single most-recent
|
||||
// call, so wrapping a function in it directly here means unrelated chart
|
||||
// instances evict each other's cached result whenever they render in the
|
||||
// same tick, forcing a full rebuild - with brand-new array/object
|
||||
// references - even when a given chart's own inputs are unchanged. AG
|
||||
// Grid treats a new colDefs identity as "columns changed" and re-measures
|
||||
// autoHeight/wrapText rows, which is what actually reads as a layout
|
||||
// flicker on a chart that never changed. Keying a separate memoized
|
||||
// function per chart id isolates each chart's cache from its siblings.
|
||||
function memoizePerChart<Args extends unknown[], R>(
|
||||
fn: (...args: Args) => R,
|
||||
isEqual?: (newArgs: Args, lastArgs: Args) => boolean,
|
||||
) {
|
||||
const memoizedByChart = new Map<number, (...args: Args) => R>();
|
||||
return (sliceId: number, ...args: Args): R => {
|
||||
let fnForChart = memoizedByChart.get(sliceId);
|
||||
if (!fnForChart) {
|
||||
fnForChart = isEqual ? memoizeOne(fn, isEqual) : memoizeOne(fn);
|
||||
memoizedByChart.set(sliceId, fnForChart);
|
||||
}
|
||||
return fnForChart(...args);
|
||||
};
|
||||
}
|
||||
|
||||
const processComparisonDataRecords = memoizePerChart(
|
||||
function processComparisonDataRecords(
|
||||
originalData: DataRecord[] | undefined,
|
||||
originalColumns: DataColumnMeta[],
|
||||
@@ -309,7 +340,7 @@ const processComparisonColumns = (
|
||||
|
||||
const serverPageLengthMap = new Map();
|
||||
|
||||
const processDataRecords = memoizeOne(function processDataRecords(
|
||||
const processDataRecords = memoizePerChart(function processDataRecords(
|
||||
data: DataRecord[] | undefined,
|
||||
columns: DataColumnMeta[],
|
||||
) {
|
||||
@@ -336,11 +367,16 @@ const processDataRecords = memoizeOne(function processDataRecords(
|
||||
return data;
|
||||
});
|
||||
|
||||
const processColumns = memoizeOne(function processColumns(
|
||||
const processColumns = memoizePerChart(function processColumns(
|
||||
props: TableChartProps,
|
||||
) {
|
||||
const {
|
||||
datasource: { columnFormats, currencyFormats, verboseMap },
|
||||
datasource: {
|
||||
columnFormats,
|
||||
currencyFormats,
|
||||
verboseMap,
|
||||
currencyCodeColumn,
|
||||
},
|
||||
rawFormData: {
|
||||
table_timestamp_format: tableTimestampFormat,
|
||||
metrics: metrics_,
|
||||
@@ -352,7 +388,12 @@ const processColumns = memoizeOne(function processColumns(
|
||||
queriesData,
|
||||
} = props;
|
||||
const granularity = extractTimegrain(props.rawFormData);
|
||||
const { data: records, colnames, coltypes } = queriesData[0] || {};
|
||||
const {
|
||||
data: records,
|
||||
colnames,
|
||||
coltypes,
|
||||
detected_currency: detectedCurrency,
|
||||
} = queriesData[0] || {};
|
||||
// convert `metrics` and `percentMetrics` to the key names in `data.records`
|
||||
const metrics = (metrics_ ?? []).map(getMetricLabel);
|
||||
const rawPercentMetrics = (percentMetrics_ ?? []).map(getMetricLabel);
|
||||
@@ -363,13 +404,18 @@ const processColumns = memoizeOne(function processColumns(
|
||||
const rawPercentMetricsSet = new Set(rawPercentMetrics);
|
||||
|
||||
const columns: DataColumnMeta[] = (colnames || [])
|
||||
.map((key: string, originalIndex: number) => ({ key, originalIndex }))
|
||||
.filter(
|
||||
key =>
|
||||
({ key }) =>
|
||||
// if a metric was only added to percent_metrics, they should not show up in the table.
|
||||
!(rawPercentMetricsSet.has(key) && !metricsSet.has(key)),
|
||||
)
|
||||
.map((key: string, i) => {
|
||||
const dataType = coltypes[i];
|
||||
.map(({ key, originalIndex }) => {
|
||||
// Look up by the column's original position in colnames/coltypes,
|
||||
// not its position after the filter above — those diverge whenever
|
||||
// an earlier column (e.g. a percent-metric-only one) got filtered
|
||||
// out, which would otherwise shift every later column's dataType.
|
||||
const dataType = coltypes[originalIndex];
|
||||
const config = columnConfig[key] || {};
|
||||
// for the purpose of presentation, only numeric values are treated as metrics
|
||||
// because users can also add things like `MAX(str_col)` as a metric.
|
||||
@@ -431,10 +477,25 @@ const processColumns = memoizeOne(function processColumns(
|
||||
// percent metrics have a default format
|
||||
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
|
||||
} else if (isMetric || (isNumber && (numberFormat || currency))) {
|
||||
formatter = currency?.symbol
|
||||
// Resolve AUTO currency when currency column isn't in query results
|
||||
let resolvedCurrency = currency;
|
||||
if (
|
||||
currency?.symbol === 'AUTO' &&
|
||||
detectedCurrency &&
|
||||
(!currencyCodeColumn || !colnames?.includes(currencyCodeColumn))
|
||||
) {
|
||||
const normalizedCurrency = normalizeCurrency(detectedCurrency);
|
||||
if (normalizedCurrency) {
|
||||
resolvedCurrency = {
|
||||
...currency,
|
||||
symbol: normalizedCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
formatter = resolvedCurrency?.symbol
|
||||
? new CurrencyFormatter({
|
||||
d3Format: numberFormat,
|
||||
currency,
|
||||
currency: resolvedCurrency,
|
||||
})
|
||||
: getNumberFormatter(numberFormat);
|
||||
}
|
||||
@@ -448,6 +509,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
formatter,
|
||||
config,
|
||||
description,
|
||||
currencyCodeColumn,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
@@ -494,7 +556,7 @@ const transformProps = (
|
||||
queriesData = [],
|
||||
ownState: serverPaginationData,
|
||||
filterState,
|
||||
hooks: { setDataMask = () => {}, onChartStateChange },
|
||||
hooks: { setDataMask = () => {}, onChartStateChange, onContextMenu },
|
||||
emitCrossFilters,
|
||||
theme,
|
||||
} = chartProps;
|
||||
@@ -526,10 +588,10 @@ const transformProps = (
|
||||
comparison_color_enabled: comparisonColorEnabled = false,
|
||||
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
|
||||
show_numbered_column: showNumberedColumn = false,
|
||||
allow_rearrange_columns: allowRearrangeColumns = true,
|
||||
allow_render_html: allowRenderHtml = true,
|
||||
} = formData;
|
||||
|
||||
const allowRearrangeColumns = true;
|
||||
|
||||
// Calculate time comparison settings early since they're used in multiple places
|
||||
const isUsingTimeComparison =
|
||||
!isEmpty(time_compare) &&
|
||||
@@ -682,7 +744,7 @@ const transformProps = (
|
||||
hasServerPageLengthChanged = true;
|
||||
}
|
||||
|
||||
const [, percentMetrics, columns] = processColumns(chartProps);
|
||||
const [, percentMetrics, columns] = processColumns(slice_id, chartProps);
|
||||
|
||||
const timeGrain = extractTimegrain(formData);
|
||||
|
||||
@@ -700,20 +762,34 @@ const transformProps = (
|
||||
);
|
||||
}
|
||||
|
||||
// buildQuery.ts can append an "all records" percent-metric denominator
|
||||
// query *and* a totals query, independently of each other, both landing
|
||||
// in extraQueries before the totals one. A fixed totalQuery index would
|
||||
// silently bind to the wrong query's data (or drop the totals query
|
||||
// entirely) whenever both are present, so replicate buildQuery.ts's own
|
||||
// gating condition here to know whether to skip that extra slot.
|
||||
const hasAllRecordsExtraQuery = Boolean(
|
||||
formData.percent_metrics?.length &&
|
||||
(formData.percent_metric_calculation || 'row_limit') === 'all_records',
|
||||
);
|
||||
|
||||
let baseQuery;
|
||||
let countQuery;
|
||||
let rowCount;
|
||||
let totalQuery;
|
||||
if (serverPagination) {
|
||||
[baseQuery, countQuery, totalQuery] = queriesData;
|
||||
[baseQuery, countQuery] = queriesData;
|
||||
totalQuery = hasAllRecordsExtraQuery ? queriesData[3] : queriesData[2];
|
||||
rowCount = (countQuery?.data?.[0]?.rowcount as number) ?? 0;
|
||||
} else {
|
||||
[baseQuery, totalQuery] = queriesData;
|
||||
[baseQuery] = queriesData;
|
||||
totalQuery = hasAllRecordsExtraQuery ? queriesData[2] : queriesData[1];
|
||||
rowCount = baseQuery?.rowcount ?? 0;
|
||||
}
|
||||
|
||||
const data = processDataRecords(baseQuery?.data, columns);
|
||||
const data = processDataRecords(slice_id, baseQuery?.data, columns);
|
||||
const comparisonData = processComparisonDataRecords(
|
||||
slice_id,
|
||||
baseQuery?.data,
|
||||
columns,
|
||||
comparisonSuffix,
|
||||
@@ -793,12 +869,12 @@ const transformProps = (
|
||||
|
||||
// Map saved metric/calculated column labels to their SQL expressions for filter resolution
|
||||
const metricSqlExpressions: Record<string, string> = {};
|
||||
chartProps.datasource.metrics.forEach(metric => {
|
||||
(chartProps.datasource?.metrics ?? []).forEach(metric => {
|
||||
if (metric.metric_name && metric.expression) {
|
||||
metricSqlExpressions[metric.metric_name] = metric.expression;
|
||||
}
|
||||
});
|
||||
chartProps.datasource.columns.forEach(col => {
|
||||
(chartProps.datasource?.columns ?? []).forEach(col => {
|
||||
if (col.column_name && col.expression) {
|
||||
metricSqlExpressions[col.column_name] = col.expression;
|
||||
if (col.verbose_name && col.verbose_name !== col.column_name) {
|
||||
@@ -811,7 +887,7 @@ const transformProps = (
|
||||
// backed by a dataset (physical or calculated) column can be summed
|
||||
// server-side; free-form SQL expression columns are excluded.
|
||||
const datasetColumnNames = new Set(
|
||||
chartProps.datasource.columns
|
||||
(chartProps.datasource?.columns ?? [])
|
||||
.map(col => col.column_name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
);
|
||||
@@ -849,6 +925,7 @@ const transformProps = (
|
||||
filters: filterState.filters,
|
||||
emitCrossFilters,
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
slice_id,
|
||||
serverPagination,
|
||||
rowCount,
|
||||
@@ -873,6 +950,7 @@ const transformProps = (
|
||||
chartState,
|
||||
onChartStateChange,
|
||||
showNumberedColumn,
|
||||
onContextMenu,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
JsonObject,
|
||||
Metric,
|
||||
AgGridChartState,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ColDef,
|
||||
@@ -81,6 +82,7 @@ export type TableChartFormData = QueryFormData & {
|
||||
time_grain_sqla?: TimeGranularity;
|
||||
column_config?: Record<string, TableColumnConfig>;
|
||||
allow_rearrange_columns?: boolean;
|
||||
allow_render_html?: boolean;
|
||||
show_numbered_column?: boolean;
|
||||
};
|
||||
|
||||
@@ -134,6 +136,11 @@ export interface AgGridTableChartTransformedProps<
|
||||
onChartStateChange?: (chartState: JsonObject) => void;
|
||||
chartState?: AgGridChartState;
|
||||
showNumberedColumn: boolean;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface SortState {
|
||||
@@ -196,6 +203,7 @@ export interface InputColumn {
|
||||
originalLabel?: string;
|
||||
metricName?: string;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
}
|
||||
|
||||
export type ValueRange = [number, number] | null;
|
||||
|
||||
@@ -20,6 +20,17 @@
|
||||
import { SetDataMaskHook } from '@superset-ui/core';
|
||||
import { SortByItem } from '../types';
|
||||
|
||||
export interface ClientViewColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ClientViewSnapshot {
|
||||
rows: Record<string, unknown>[];
|
||||
columns: ClientViewColumn[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface TableOwnState {
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
@@ -29,6 +40,7 @@ interface TableOwnState {
|
||||
sortBy?: SortByItem[];
|
||||
rawSummaryColumns?: string[];
|
||||
totalsRequested?: boolean;
|
||||
clientView?: ClientViewSnapshot;
|
||||
}
|
||||
|
||||
export const updateTableOwnState = (
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
CurrencyFormatter,
|
||||
DataRecordValue,
|
||||
getSmallNumberFormatter,
|
||||
isDefined,
|
||||
isEmptyDateInput,
|
||||
isProbablyHTML,
|
||||
sanitizeHtml,
|
||||
DateWithFormatter,
|
||||
@@ -37,6 +39,8 @@ import { DataColumnMeta, InputColumn } from '../types';
|
||||
function formatValue(
|
||||
formatter: DataColumnMeta['formatter'],
|
||||
value: DataRecordValue,
|
||||
rowData?: Record<string, DataRecordValue>,
|
||||
currencyColumn?: string,
|
||||
): [boolean, string] {
|
||||
// render undefined as empty string
|
||||
if (value === undefined) {
|
||||
@@ -45,13 +49,17 @@ function formatValue(
|
||||
// render null as `N/A`
|
||||
if (
|
||||
value === null ||
|
||||
// null values in temporal columns are wrapped in a Date object, so make sure we
|
||||
// handle them here too
|
||||
(value instanceof DateWithFormatter && value.input === null)
|
||||
// null/empty values in temporal columns are wrapped in a Date object, so make
|
||||
// sure we handle them here too
|
||||
(value instanceof DateWithFormatter && isEmptyDateInput(value.input))
|
||||
) {
|
||||
return [false, 'N/A'];
|
||||
}
|
||||
if (formatter) {
|
||||
// If formatter is a CurrencyFormatter, pass row context for AUTO mode
|
||||
if (formatter instanceof CurrencyFormatter) {
|
||||
return [false, formatter(value as number, rowData, currencyColumn)];
|
||||
}
|
||||
return [false, formatter(value as number)];
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
@@ -63,8 +71,9 @@ function formatValue(
|
||||
export function formatColumnValue(
|
||||
column: DataColumnMeta,
|
||||
value: DataRecordValue,
|
||||
rowData?: Record<string, DataRecordValue>,
|
||||
) {
|
||||
const { dataType, formatter, config = {} } = column;
|
||||
const { dataType, formatter, config = {}, currencyCodeColumn } = column;
|
||||
const isNumber = dataType === GenericDataType.Numeric;
|
||||
const smallNumberFormatter = getSmallNumberFormatter(
|
||||
formatter,
|
||||
@@ -76,6 +85,8 @@ export function formatColumnValue(
|
||||
? smallNumberFormatter
|
||||
: formatter,
|
||||
value,
|
||||
rowData,
|
||||
currencyCodeColumn,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,13 +94,24 @@ export const valueFormatter = (
|
||||
params: ValueFormatterParams,
|
||||
col: InputColumn,
|
||||
): string => {
|
||||
const { value, node } = params;
|
||||
const { value, node, data } = params;
|
||||
if (
|
||||
isDefined(value) &&
|
||||
value !== '' &&
|
||||
!(value instanceof DateWithFormatter && value.input === null)
|
||||
!(value instanceof DateWithFormatter && isEmptyDateInput(value.input))
|
||||
) {
|
||||
return col.formatter?.(value) || value;
|
||||
// Fall back to String(value) rather than the raw value: value can be a
|
||||
// DateWithFormatter/Date (or other object) when col.formatter is unset or
|
||||
// returns a falsy result, and returning that raw object here - though it
|
||||
// satisfies this function's `: string` signature at compile time since
|
||||
// `value`'s param type is loosely typed - crashes React with "Objects are
|
||||
// not valid as a React child" once a cell renderer renders it directly.
|
||||
if (col.formatter instanceof CurrencyFormatter) {
|
||||
return (
|
||||
col.formatter(value, data, col.currencyCodeColumn) || String(value)
|
||||
);
|
||||
}
|
||||
return col.formatter?.(value) || String(value);
|
||||
}
|
||||
if (node?.level === -1) {
|
||||
return '';
|
||||
|
||||
@@ -55,29 +55,81 @@ const getCellStyle = (params: CellStyleParams) => {
|
||||
let backgroundColor;
|
||||
let color;
|
||||
if (hasColumnColorFormatters) {
|
||||
columnColorFormatters!
|
||||
.filter(formatter => {
|
||||
const colTitle = formatter?.column?.includes('Main')
|
||||
? formatter?.column?.replace('Main', '').trim()
|
||||
: formatter?.column;
|
||||
return colTitle === colDef.field;
|
||||
})
|
||||
.forEach(formatter => {
|
||||
const formatterResult =
|
||||
value || value === 0 ? formatter.getColorFromValue(value) : false;
|
||||
if (formatterResult) {
|
||||
if (
|
||||
formatter.objectFormatting === ObjectFormattingEnum.TEXT_COLOR ||
|
||||
formatter.toTextColor
|
||||
) {
|
||||
color = formatterResult;
|
||||
} else if (
|
||||
formatter.objectFormatting !== ObjectFormattingEnum.CELL_BAR
|
||||
) {
|
||||
backgroundColor = formatterResult;
|
||||
}
|
||||
const applyFormatter = (
|
||||
formatter: ColorFormatters[number],
|
||||
valueToFormat: typeof value,
|
||||
) => {
|
||||
const formatterResult =
|
||||
valueToFormat || valueToFormat === 0
|
||||
? formatter.getColorFromValue(valueToFormat)
|
||||
: false;
|
||||
if (formatterResult) {
|
||||
if (
|
||||
formatter.objectFormatting === ObjectFormattingEnum.TEXT_COLOR ||
|
||||
formatter.toTextColor
|
||||
) {
|
||||
color = formatterResult;
|
||||
} else if (
|
||||
formatter.objectFormatting !== ObjectFormattingEnum.CELL_BAR
|
||||
) {
|
||||
backgroundColor = formatterResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// formatter.column can be a legacy display label ("Main colname") for
|
||||
// time-comparison columns rather than the row's actual data key, so
|
||||
// resolve it to the real field id before using it to read row values.
|
||||
const resolveColumnKey = (columnKey: string) =>
|
||||
columnKey.startsWith('Main ')
|
||||
? columnKey.slice('Main '.length)
|
||||
: columnKey;
|
||||
|
||||
// Formatters with no formatting target color their own source column,
|
||||
// keyed off this cell's own value. Excludes legacy v1 `toAllRow` rules,
|
||||
// which are entire-row formatters handled below.
|
||||
columnColorFormatters!
|
||||
.filter(
|
||||
formatter =>
|
||||
!formatter.columnFormatting &&
|
||||
!formatter.toAllRow &&
|
||||
resolveColumnKey(formatter.column) === colDef.field,
|
||||
)
|
||||
.forEach(formatter => applyFormatter(formatter, value));
|
||||
|
||||
// Formatters with a real target column color that target column,
|
||||
// keyed off the value in the formatter's own (source) column.
|
||||
columnColorFormatters!
|
||||
.filter(
|
||||
formatter =>
|
||||
formatter.columnFormatting &&
|
||||
formatter.columnFormatting !== ObjectFormattingEnum.ENTIRE_ROW &&
|
||||
resolveColumnKey(formatter.columnFormatting) === colDef.field,
|
||||
)
|
||||
.forEach(formatter =>
|
||||
applyFormatter(
|
||||
formatter,
|
||||
node?.data?.[resolveColumnKey(formatter.column)],
|
||||
),
|
||||
);
|
||||
|
||||
// Entire-row formatters apply to every cell in the row, keyed off the
|
||||
// value in the formatter's own column rather than this cell's column.
|
||||
// `toAllRow` is the legacy v1 flag for the same behavior; migrated
|
||||
// charts carry it over unchanged rather than being rewritten to
|
||||
// `columnFormatting: ENTIRE_ROW`, so both are honored here.
|
||||
columnColorFormatters!
|
||||
.filter(
|
||||
formatter =>
|
||||
formatter.columnFormatting === ObjectFormattingEnum.ENTIRE_ROW ||
|
||||
formatter.toAllRow,
|
||||
)
|
||||
.forEach(formatter =>
|
||||
applyFormatter(
|
||||
formatter,
|
||||
node?.data?.[resolveColumnKey(formatter.column)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
let cached: number | undefined;
|
||||
|
||||
const css = (x: TemplateStringsArray) => x.join('\n');
|
||||
|
||||
export default function getScrollBarSize(forceRefresh = false) {
|
||||
if (typeof document === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
if (cached === undefined || forceRefresh) {
|
||||
const inner = document.createElement('div');
|
||||
const outer = document.createElement('div');
|
||||
inner.style.cssText = css`
|
||||
width: auto;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
`;
|
||||
outer.style.cssText = css`
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
`;
|
||||
outer.append(inner);
|
||||
document.body.append(outer);
|
||||
cached = outer.clientWidth - inner.clientWidth;
|
||||
outer.remove();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { TimeGranularity } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Calculates the inclusive/exclusive temporal range for a bucket.
|
||||
* standard SQL range pattern: [start, end)
|
||||
*/
|
||||
export default function getTimeRangeFromGranularity(
|
||||
startTime: Date,
|
||||
granularity: TimeGranularity,
|
||||
): [Date, Date] {
|
||||
const time = startTime.getTime();
|
||||
const date = startTime.getUTCDate();
|
||||
const month = startTime.getUTCMonth();
|
||||
const year = startTime.getUTCFullYear();
|
||||
|
||||
// Constants
|
||||
const MS_IN_SECOND = 1000;
|
||||
const MS_IN_MINUTE = 60 * MS_IN_SECOND;
|
||||
const MS_IN_HOUR = 60 * MS_IN_MINUTE;
|
||||
|
||||
switch (granularity) {
|
||||
case TimeGranularity.SECOND:
|
||||
return [startTime, new Date(time + MS_IN_SECOND)];
|
||||
case TimeGranularity.MINUTE:
|
||||
return [startTime, new Date(time + MS_IN_MINUTE)];
|
||||
case TimeGranularity.FIVE_MINUTES:
|
||||
return [startTime, new Date(time + MS_IN_MINUTE * 5)];
|
||||
case TimeGranularity.TEN_MINUTES:
|
||||
return [startTime, new Date(time + MS_IN_MINUTE * 10)];
|
||||
case TimeGranularity.FIFTEEN_MINUTES:
|
||||
return [startTime, new Date(time + MS_IN_MINUTE * 15)];
|
||||
case TimeGranularity.THIRTY_MINUTES:
|
||||
return [startTime, new Date(time + MS_IN_MINUTE * 30)];
|
||||
case TimeGranularity.HOUR:
|
||||
return [startTime, new Date(time + MS_IN_HOUR)];
|
||||
case TimeGranularity.DAY:
|
||||
case TimeGranularity.DATE:
|
||||
return [startTime, new Date(Date.UTC(year, month, date + 1))];
|
||||
case TimeGranularity.WEEK:
|
||||
case TimeGranularity.WEEK_STARTING_SUNDAY:
|
||||
case TimeGranularity.WEEK_STARTING_MONDAY:
|
||||
return [startTime, new Date(Date.UTC(year, month, date + 7))];
|
||||
case TimeGranularity.WEEK_ENDING_SATURDAY:
|
||||
case TimeGranularity.WEEK_ENDING_SUNDAY:
|
||||
// Week-ending buckets are labeled by the bucket's final day.
|
||||
return [
|
||||
new Date(Date.UTC(year, month, date - 6)),
|
||||
new Date(Date.UTC(year, month, date + 1)),
|
||||
];
|
||||
case TimeGranularity.MONTH:
|
||||
return [startTime, new Date(Date.UTC(year, month + 1, 1))];
|
||||
case TimeGranularity.QUARTER:
|
||||
return [
|
||||
startTime,
|
||||
new Date(Date.UTC(year, Math.floor(month / 3) * 3 + 3, 1)),
|
||||
];
|
||||
case TimeGranularity.YEAR:
|
||||
return [startTime, new Date(Date.UTC(year + 1, 0, 1))];
|
||||
default:
|
||||
return [startTime, new Date(Date.UTC(year, month, date + 1))];
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { isEqualArray } from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { TableChartProps } from '../types';
|
||||
|
||||
const getDescriptions = (props: TableChartProps) => {
|
||||
@@ -47,23 +48,55 @@ export default function isEqualColumns(
|
||||
|
||||
const descA = getDescriptions(a);
|
||||
const descB = getDescriptions(b);
|
||||
return (
|
||||
a.datasource.columnFormats === b.datasource.columnFormats &&
|
||||
a.datasource.currencyFormats === b.datasource.currencyFormats &&
|
||||
a.datasource.verboseMap === b.datasource.verboseMap &&
|
||||
a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
|
||||
a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
|
||||
JSON.stringify(a.formData.columnConfig || null) ===
|
||||
JSON.stringify(b.formData.columnConfig || null) &&
|
||||
isEqualArray(a.formData.metrics, b.formData.metrics) &&
|
||||
isEqualArray(a.queriesData?.[0]?.colnames, b.queriesData?.[0]?.colnames) &&
|
||||
isEqualArray(a.queriesData?.[0]?.coltypes, b.queriesData?.[0]?.coltypes) &&
|
||||
JSON.stringify(a.formData.extraFilters || null) ===
|
||||
JSON.stringify(b.formData.extraFilters || null) &&
|
||||
JSON.stringify(a.formData.extraFormData || null) ===
|
||||
JSON.stringify(b.formData.extraFormData || null) &&
|
||||
JSON.stringify(a.rawFormData.column_config || null) ===
|
||||
JSON.stringify(b.rawFormData.column_config || null) &&
|
||||
JSON.stringify(descA) === JSON.stringify(descB)
|
||||
);
|
||||
|
||||
// Every field below is read with optional chaining because this comparator
|
||||
// also runs against partial/mock props in tests; production TableChartProps
|
||||
// always has these populated.
|
||||
const checks = {
|
||||
// These three are plain, serializable per-column config maps. Superset's
|
||||
// core datasource pipeline can rebuild them with a new object reference
|
||||
// on renders that don't actually change any formatting, so compare by
|
||||
// value here - otherwise an incidental new reference looks like a real
|
||||
// change and forces a full AG Grid column/row rebuild downstream.
|
||||
columnFormats: isEqual(
|
||||
a.datasource?.columnFormats,
|
||||
b.datasource?.columnFormats,
|
||||
),
|
||||
currencyFormats: isEqual(
|
||||
a.datasource?.currencyFormats,
|
||||
b.datasource?.currencyFormats,
|
||||
),
|
||||
verboseMap: isEqual(a.datasource?.verboseMap, b.datasource?.verboseMap),
|
||||
currencyCodeColumn:
|
||||
a.datasource?.currencyCodeColumn === b.datasource?.currencyCodeColumn,
|
||||
detectedCurrency:
|
||||
a.queriesData?.[0]?.detected_currency ===
|
||||
b.queriesData?.[0]?.detected_currency,
|
||||
tableTimestampFormat:
|
||||
a.formData?.tableTimestampFormat === b.formData?.tableTimestampFormat,
|
||||
timeGrainSqla: a.formData?.timeGrainSqla === b.formData?.timeGrainSqla,
|
||||
columnConfig:
|
||||
JSON.stringify(a.formData?.columnConfig || null) ===
|
||||
JSON.stringify(b.formData?.columnConfig || null),
|
||||
metrics: isEqualArray(a.formData?.metrics, b.formData?.metrics),
|
||||
colnames: isEqualArray(
|
||||
a.queriesData?.[0]?.colnames,
|
||||
b.queriesData?.[0]?.colnames,
|
||||
),
|
||||
coltypes: isEqualArray(
|
||||
a.queriesData?.[0]?.coltypes,
|
||||
b.queriesData?.[0]?.coltypes,
|
||||
),
|
||||
extraFilters:
|
||||
JSON.stringify(a.formData?.extraFilters || null) ===
|
||||
JSON.stringify(b.formData?.extraFilters || null),
|
||||
extraFormData:
|
||||
JSON.stringify(a.formData?.extraFormData || null) ===
|
||||
JSON.stringify(b.formData?.extraFormData || null),
|
||||
rawColumnConfig:
|
||||
JSON.stringify(a.rawFormData?.column_config || null) ===
|
||||
JSON.stringify(b.rawFormData?.column_config || null),
|
||||
descriptions: JSON.stringify(descA) === JSON.stringify(descB),
|
||||
};
|
||||
return Object.values(checks).every(Boolean);
|
||||
}
|
||||
|
||||
@@ -28,11 +28,15 @@ import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
DataRecordValue,
|
||||
DateWithFormatter,
|
||||
isEmptyDateInput,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { ColorFormatters } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ColorFormatters,
|
||||
ConditionalFormattingConfig,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { extent as d3Extent, max as d3Max } from 'd3-array';
|
||||
import {
|
||||
BasicColorFormatterType,
|
||||
@@ -71,11 +75,15 @@ type UseColDefsProps = {
|
||||
colorPositiveNegative: boolean;
|
||||
columnColorFormatters: ColorFormatters;
|
||||
allowRearrangeColumns?: boolean;
|
||||
allowRenderHtml?: boolean;
|
||||
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
isUsingTimeComparison?: boolean;
|
||||
emitCrossFilters?: boolean;
|
||||
alignPositiveNegative: boolean;
|
||||
slice_id: number;
|
||||
conditionalFormatting?: ConditionalFormattingConfig[];
|
||||
comparisonColorEnabled?: boolean;
|
||||
comparisonColorScheme?: string;
|
||||
};
|
||||
|
||||
function getValueRange(
|
||||
@@ -131,7 +139,7 @@ const getFilterType = (col: InputColumn) => {
|
||||
|
||||
/**
|
||||
* Filter value getter for temporal columns.
|
||||
* Returns null for DateWithFormatter objects with null input,
|
||||
* Returns null for DateWithFormatter objects with null/empty input,
|
||||
* enabling AG Grid's blank filter to correctly identify null dates.
|
||||
*/
|
||||
const dateFilterValueGetter = (params: {
|
||||
@@ -139,8 +147,8 @@ const dateFilterValueGetter = (params: {
|
||||
colDef: { field?: string };
|
||||
}) => {
|
||||
const value = params.data?.[params.colDef.field as string];
|
||||
// Return null for DateWithFormatter with null input so AG Grid blank filter works
|
||||
if (value instanceof DateWithFormatter && value.input === null) {
|
||||
// Return null for DateWithFormatter with null/empty input so AG Grid blank filter works
|
||||
if (value instanceof DateWithFormatter && isEmptyDateInput(value.input)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
@@ -236,13 +244,37 @@ export const useColDefs = ({
|
||||
colorPositiveNegative,
|
||||
columnColorFormatters,
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
basicColorFormatters,
|
||||
isUsingTimeComparison,
|
||||
emitCrossFilters,
|
||||
alignPositiveNegative,
|
||||
slice_id,
|
||||
conditionalFormatting,
|
||||
comparisonColorEnabled,
|
||||
comparisonColorScheme,
|
||||
}: UseColDefsProps) => {
|
||||
const theme = useTheme();
|
||||
// transformProps.ts computes these fresh on every call (no memoization),
|
||||
// so a reference-based dependency here would recreate getCommonColProps -
|
||||
// and therefore colDefs - on every render regardless of whether the
|
||||
// formatting actually changed. Compare by content instead.
|
||||
//
|
||||
// columnColorFormatters/basicColorFormatters can't be stringified directly:
|
||||
// each entry's getColorFromValue closes over the rule's operator/
|
||||
// thresholds/gradient/color, none of which are mirrored as serializable
|
||||
// fields on the entry itself, so JSON.stringify drops them and two
|
||||
// differently-configured rules for the same column serialize identically.
|
||||
// Depend on the raw, fully-serializable formData that produced those
|
||||
// formatters instead.
|
||||
const stringifiedColumnColorFormatters = JSON.stringify(
|
||||
conditionalFormatting,
|
||||
);
|
||||
const stringifiedBasicColorFormatters = JSON.stringify([
|
||||
conditionalFormatting,
|
||||
comparisonColorEnabled,
|
||||
comparisonColorScheme,
|
||||
]);
|
||||
const getCommonColProps = useCallback(
|
||||
(
|
||||
col: InputColumn,
|
||||
@@ -387,7 +419,7 @@ export const useColDefs = ({
|
||||
cellRenderer: (p: CellRendererProps) =>
|
||||
isTextColumn ? TextCellRenderer(p) : NumericCellRenderer(p),
|
||||
cellRendererParams: {
|
||||
allowRenderHtml: true,
|
||||
allowRenderHtml,
|
||||
columns,
|
||||
hasBasicColorFormatters,
|
||||
col,
|
||||
@@ -401,6 +433,12 @@ export const useColDefs = ({
|
||||
isMetric,
|
||||
isPercentMetric,
|
||||
isNumeric,
|
||||
// colId (`field` above) has "Main " stripped for comparison
|
||||
// columns, but row data is still keyed by the unstripped
|
||||
// originalKey -- consumers reading row values by column (e.g. the
|
||||
// "Export Current View" snapshot) need this to look values up
|
||||
// correctly.
|
||||
dataKey: originalKey,
|
||||
},
|
||||
lockPinned: !allowRearrangeColumns,
|
||||
sortable: !serverPagination || !isPercentMetric,
|
||||
@@ -427,14 +465,15 @@ export const useColDefs = ({
|
||||
columns,
|
||||
data,
|
||||
defaultAlignPN,
|
||||
columnColorFormatters,
|
||||
basicColorFormatters,
|
||||
stringifiedColumnColorFormatters,
|
||||
stringifiedBasicColorFormatters,
|
||||
showCellBars,
|
||||
colorPositiveNegative,
|
||||
isUsingTimeComparison,
|
||||
isRawRecords,
|
||||
emitCrossFilters,
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
serverPagination,
|
||||
alignPositiveNegative,
|
||||
theme.colorBgBase,
|
||||
|
||||
+140
-2
@@ -17,7 +17,14 @@
|
||||
* under the License.
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen, waitFor } from '@superset-ui/core/spec';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
within,
|
||||
userEvent,
|
||||
} from '@superset-ui/core/spec';
|
||||
import { QueryMode, TimeGranularity, SMART_DATE_ID } from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import {
|
||||
@@ -259,6 +266,59 @@ test('AgGridTableChart renders Search by dropdown if includeSearch is true and t
|
||||
expect(screen.getByText(/Search by/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('AgGridTableChart resets currentPage when the search column changes', async () => {
|
||||
const props = transformProps({
|
||||
...testData.basic,
|
||||
rawFormData: {
|
||||
...testData.basic.rawFormData,
|
||||
server_pagination: true,
|
||||
include_search: true,
|
||||
},
|
||||
});
|
||||
props.serverPagination = true;
|
||||
props.includeSearch = true;
|
||||
props.rowCount = 50;
|
||||
props.serverPaginationData = {
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
};
|
||||
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<AgGridTableChart
|
||||
{...props}
|
||||
setDataMask={mockSetDataMask}
|
||||
slice_id={1}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const searchByContainer = await waitFor(() => {
|
||||
const container = document.querySelector('.search-select');
|
||||
expect(container).toBeInTheDocument();
|
||||
return container as HTMLElement;
|
||||
});
|
||||
const searchByDropdown = within(searchByContainer).getByRole('combobox');
|
||||
await userEvent.click(searchByDropdown);
|
||||
const otherOption = await waitFor(() =>
|
||||
within(screen.getByRole('listbox')).getByText('abc.com'),
|
||||
);
|
||||
await userEvent.click(otherOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetDataMask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ownState: expect.objectContaining({
|
||||
searchColumn: 'abc.com',
|
||||
currentPage: 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('AgGridTableChart does not render Search by dropdown if includeSearch is true but searchOptions is empty', async () => {
|
||||
const noStringColumnsData = {
|
||||
...testData.basic,
|
||||
@@ -873,9 +933,24 @@ test('AgGridTableChart emits column state with aggFunc through the debounced sav
|
||||
expect(document.querySelector('.ag-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The very first onStateUpdated after mount just reflects the chartState
|
||||
// the grid was initialized with, so it must not trigger a save on its own
|
||||
// (persisting it unconditionally caused a mount -> save -> remount ->
|
||||
// mount loop). Let that initial debounced capture settle before
|
||||
// simulating a real user action - clicking a sortable header - so it
|
||||
// isn't coalesced into the same debounce window and mistaken for the
|
||||
// initial, ignorable capture.
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
const sortableHeaderLabel = document.querySelector(
|
||||
'.ag-header-cell-sortable .ag-header-cell-label',
|
||||
);
|
||||
expect(sortableHeaderLabel).toBeTruthy();
|
||||
fireEvent.click(sortableHeaderLabel!);
|
||||
|
||||
// The save path is debounced (SLOW_DEBOUNCE = 500ms); wait for a capture.
|
||||
await waitFor(() => expect(onChartStateChange).toHaveBeenCalled(), {
|
||||
timeout: 3000,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const savedState =
|
||||
@@ -889,3 +964,66 @@ test('AgGridTableChart emits column state with aggFunc through the debounced sav
|
||||
// (SharedAggregation) module; the community modules always report null.
|
||||
expect(savedColumn).toMatchObject({ aggFunc: null });
|
||||
});
|
||||
|
||||
test('AgGridTableChart renders a temporal column with a blank row without crashing', async () => {
|
||||
// Regression test: a raw-mode temporal column backed by numeric epoch
|
||||
// values, where one row's raw value is '' rather than null/undefined/a
|
||||
// number, used to flip isNumeric() false for the whole column (see
|
||||
// transformProps.ts), degrading its formatter to plain `String`. That made
|
||||
// DateWithFormatter.toString() return String('') for the blank row, which
|
||||
// is falsy - and valueFormatter's old `|| value` fallback then rendered the
|
||||
// raw Date object directly, crashing React with "Objects are not valid as
|
||||
// a React child (found: [object Date])".
|
||||
const props = transformProps({
|
||||
...testData.basic,
|
||||
rawFormData: {
|
||||
...testData.basic.rawFormData,
|
||||
query_mode: QueryMode.Raw,
|
||||
table_timestamp_format: SMART_DATE_ID,
|
||||
server_pagination: false,
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
...testData.basic.queriesData[0],
|
||||
colnames: ['__timestamp', 'name'],
|
||||
coltypes: [GenericDataType.Temporal, GenericDataType.String],
|
||||
data: [
|
||||
{ __timestamp: 1069113600000, name: 'foo' },
|
||||
{ __timestamp: 1057016400000, name: 'bar' },
|
||||
{ __timestamp: '', name: 'baz' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<AgGridTableChart
|
||||
{...props}
|
||||
setDataMask={mockSetDataMask}
|
||||
slice_id={1}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.ag-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const reactChildError = errorSpy.mock.calls
|
||||
.map(call => call.join(' '))
|
||||
.find(message =>
|
||||
message.includes('Objects are not valid as a React child'),
|
||||
);
|
||||
errorSpy.mockRestore();
|
||||
expect(reactChildError).toBeUndefined();
|
||||
|
||||
const cells = document.querySelectorAll('[col-id="__timestamp"]');
|
||||
const cellText = Array.from(cells).map(cell => cell.textContent);
|
||||
expect(cellText).toContain('N/A');
|
||||
expect(cellText).not.toContain('');
|
||||
});
|
||||
|
||||
@@ -834,6 +834,51 @@ describe('plugin-chart-ag-grid-table', () => {
|
||||
expect(totalsQuery.extras).toBeDefined();
|
||||
});
|
||||
|
||||
test('should exclude AG Grid HAVING filters from totals query', () => {
|
||||
const { queries } = buildQuery(
|
||||
{
|
||||
...basicFormData,
|
||||
server_pagination: true,
|
||||
show_totals: true,
|
||||
query_mode: QueryMode.Aggregate,
|
||||
},
|
||||
{
|
||||
ownState: {
|
||||
agGridHavingClause: 'count > 10',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mainQuery = queries[0];
|
||||
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
||||
|
||||
expect(mainQuery.extras?.having).toBe('count > 10');
|
||||
expect(totalsQuery.extras?.having).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should exclude download HAVING filters (sqlClauses) from totals query', () => {
|
||||
const { queries } = buildQuery(
|
||||
{
|
||||
...basicFormData,
|
||||
show_totals: true,
|
||||
query_mode: QueryMode.Aggregate,
|
||||
result_format: 'csv',
|
||||
},
|
||||
{
|
||||
ownState: {
|
||||
sqlClauses: { count: 'count > 10' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mainQuery = queries[0];
|
||||
// Downloads never get a rowcount query, so totals is queries[1].
|
||||
const totalsQuery = queries[1];
|
||||
|
||||
expect(mainQuery.extras?.having).toBe('count > 10');
|
||||
expect(totalsQuery.extras?.having).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not modify totals query when no AG Grid filters applied', () => {
|
||||
const { queries } = buildQuery(
|
||||
{
|
||||
@@ -853,6 +898,43 @@ describe('plugin-chart-ag-grid-table', () => {
|
||||
expect(totalsQuery.row_limit).toBe(0);
|
||||
});
|
||||
|
||||
test('all_records percent-metric denominator reflects AG Grid filters but totals do not', () => {
|
||||
// Regression test: the all_records denominator query is built from
|
||||
// the post-filter queryObject (so it matches the main query's result
|
||||
// set), while the totals query intentionally strips AG Grid
|
||||
// WHERE/HAVING so it summarizes the unfiltered chart-level data.
|
||||
const { queries } = buildQuery(
|
||||
{
|
||||
...basicFormData,
|
||||
metrics: ['count'],
|
||||
percent_metrics: ['count'],
|
||||
percent_metric_calculation: 'all_records',
|
||||
show_totals: true,
|
||||
server_pagination: true,
|
||||
query_mode: QueryMode.Aggregate,
|
||||
},
|
||||
{
|
||||
ownState: {
|
||||
agGridComplexWhere: 'age > 18',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// [main, rowcount, all_records denominator, totals]
|
||||
const allRecordsQuery = queries[2];
|
||||
const totalsQuery = queries[3];
|
||||
|
||||
expect(allRecordsQuery.extras?.where).toBe('age > 18');
|
||||
expect(allRecordsQuery.columns).toEqual([]);
|
||||
expect(allRecordsQuery.metrics).toEqual(['count']);
|
||||
expect(allRecordsQuery.row_limit).toBe(0);
|
||||
expect(allRecordsQuery.row_offset).toBe(0);
|
||||
expect(allRecordsQuery.orderby).toEqual([]);
|
||||
expect(allRecordsQuery.is_timeseries).toBe(false);
|
||||
|
||||
expect(totalsQuery.extras?.where).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should reapply percent-metric contribution op to totals query', () => {
|
||||
// Regression test for #37627: when a percent metric is configured and
|
||||
// Show Summary (show_totals) is enabled, the totals query must rename
|
||||
|
||||
@@ -184,3 +184,45 @@ test('every Visual formatting control is a renderTrigger', () => {
|
||||
expect(control.config.renderTrigger).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function findControl(
|
||||
panel: ControlPanelConfig,
|
||||
controlName: string,
|
||||
): CustomControlItem {
|
||||
const item = (panel.controlPanelSections || [])
|
||||
.flatMap(section => section?.controlSetRows || [])
|
||||
.flat()
|
||||
.find(c => isCustomControlItem(c) && c.name === controlName);
|
||||
|
||||
if (!item || !isCustomControlItem(item)) {
|
||||
throw new Error(`Control "${controlName}" not found`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
test('allow_rearrange_columns defaults to false, matching v1, and hides while time_compare is set', () => {
|
||||
const control = findControl(config, 'allow_rearrange_columns');
|
||||
expect(control.config.type).toBe('CheckboxControl');
|
||||
expect(control.config.default).toBe(false);
|
||||
expect(control.config.renderTrigger).toBe(true);
|
||||
|
||||
const vis = control.config.visibility as VisibilityFn;
|
||||
expect(
|
||||
vis({
|
||||
controls: { time_compare: { value: [] } },
|
||||
} as unknown as ControlPanelsContainerProps),
|
||||
).toBe(true);
|
||||
expect(
|
||||
vis({
|
||||
controls: { time_compare: { value: ['1 year ago'] } },
|
||||
} as unknown as ControlPanelsContainerProps),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('allow_render_html defaults to true, matching v1, and has no visibility gate', () => {
|
||||
const control = findControl(config, 'allow_render_html');
|
||||
expect(control.config.type).toBe('CheckboxControl');
|
||||
expect(control.config.default).toBe(true);
|
||||
expect(control.config.renderTrigger).toBe(true);
|
||||
expect(control.config.visibility).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -178,6 +178,38 @@ test('extraColorChoices not included when time_compare is empty array', () => {
|
||||
expect(result.extraColorChoices).toEqual([]);
|
||||
});
|
||||
|
||||
test('numericColumns resolves dataType by position, not a stale name lookup', () => {
|
||||
const controlConfig = findConditionalFormattingControl();
|
||||
expect(controlConfig).toBeTruthy();
|
||||
|
||||
const explore = createMockExplore(undefined);
|
||||
// Two columns share the name "metric" (e.g. a dimension and a metric
|
||||
// both aliased the same way); only the second occurrence is Numeric.
|
||||
const chart = {
|
||||
chartStatus: 'success' as const,
|
||||
queriesResponse: [
|
||||
{
|
||||
colnames: ['metric', 'metric'],
|
||||
coltypes: [GenericDataType.String, GenericDataType.Numeric],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = controlConfig!.mapStateToProps!(
|
||||
explore,
|
||||
createMockControlStateForConditionalFormatting(),
|
||||
chart,
|
||||
);
|
||||
|
||||
// Resolving dataType via `colnames.indexOf(colname)` would always find
|
||||
// the first "metric" (String) and misclassify this numeric column.
|
||||
expect(result.columnOptions).toEqual([
|
||||
expect.objectContaining({
|
||||
value: 'metric',
|
||||
dataType: GenericDataType.Numeric,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('consistency between extraColorChoices and columnOptions', () => {
|
||||
const controlConfig = findConditionalFormattingControl();
|
||||
expect(controlConfig).toBeTruthy();
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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, waitFor } from '@superset-ui/core/spec';
|
||||
import { DateWithFormatter, TimeGranularity } from '@superset-ui/core';
|
||||
import { ProviderWrapper } from '../../plugin-chart-table/test/testHelpers';
|
||||
import testData from '../../plugin-chart-table/test/testData';
|
||||
|
||||
// Only the context-menu handler is exercised below; the mock below fakes
|
||||
// its event argument rather than a real ag-grid CellContextMenuEvent, so
|
||||
// it's typed loosely (unknown) rather than pinned to that library type.
|
||||
interface CapturedGridProps {
|
||||
onCellContextMenu?: (event: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
// Capture the props the grid is rendered with, so we can invoke the
|
||||
// onCellContextMenu handler directly without depending on AG Grid's DOM
|
||||
// rendering or the (unregistered) Enterprise context-menu module.
|
||||
const captured: { props?: CapturedGridProps } = {};
|
||||
|
||||
jest.mock('@superset-ui/core/components/ThemedAgGridReact', () => ({
|
||||
__esModule: true,
|
||||
ThemedAgGridReact: (props: CapturedGridProps) => {
|
||||
captured.props = props;
|
||||
return null;
|
||||
},
|
||||
AgGridReact: function AgGridReact() {
|
||||
return null;
|
||||
},
|
||||
AllCommunityModule: {},
|
||||
ClientSideRowModelModule: {},
|
||||
ModuleRegistry: { registerModules: () => undefined },
|
||||
setupAGGridModules: () => undefined,
|
||||
defaultModules: [],
|
||||
themeQuartz: {},
|
||||
colorSchemeDark: {},
|
||||
colorSchemeLight: {},
|
||||
}));
|
||||
|
||||
// Imported after the mock is declared (jest.mock is hoisted above imports).
|
||||
// eslint-disable-next-line import/first
|
||||
import AgGridTableChart from '../src/AgGridTableChart';
|
||||
// eslint-disable-next-line import/first
|
||||
import transformProps from '../src/transformProps';
|
||||
|
||||
function renderChart(
|
||||
onContextMenu: jest.Mock,
|
||||
propsOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
captured.props = undefined;
|
||||
const props = {
|
||||
...transformProps({
|
||||
...testData.basic,
|
||||
hooks: { ...testData.basic.hooks, onContextMenu },
|
||||
emitCrossFilters: true,
|
||||
}),
|
||||
...propsOverrides,
|
||||
};
|
||||
render(
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<AgGridTableChart {...props} setDataMask={jest.fn()} slice_id={1} />
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function makeColumn(colId: string, context: Record<string, unknown> = {}) {
|
||||
return {
|
||||
getColId: () => colId,
|
||||
getColDef: () => ({ context }),
|
||||
};
|
||||
}
|
||||
|
||||
test('wires an onCellContextMenu handler when onContextMenu is provided', async () => {
|
||||
renderChart(jest.fn());
|
||||
await waitFor(() => expect(captured.props).toBeDefined());
|
||||
|
||||
expect(typeof captured.props?.onCellContextMenu).toBe('function');
|
||||
});
|
||||
|
||||
test('right-clicking a dimension cell emits drillToDetail, crossFilter and drillBy', async () => {
|
||||
const onContextMenu = jest.fn();
|
||||
renderChart(onContextMenu);
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
const preventDefault = jest.fn();
|
||||
const stopPropagation = jest.fn();
|
||||
const rowData = {
|
||||
__timestamp: null,
|
||||
name: 'Michael',
|
||||
sum__num: 2467063,
|
||||
'abc.com': 'foo',
|
||||
};
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('name'),
|
||||
data: rowData,
|
||||
value: 'Michael',
|
||||
event: {
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
clientX: 10,
|
||||
clientY: 20,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
expect(onContextMenu).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [clientX, clientY, filters] = onContextMenu.mock.calls[0];
|
||||
expect(clientX).toBe(10);
|
||||
expect(clientY).toBe(20);
|
||||
|
||||
// Non-temporal, non-null column → exact-match filter.
|
||||
expect(filters.drillToDetail).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ col: 'name', op: '==', val: 'Michael' }),
|
||||
expect.objectContaining({ col: 'abc.com', op: '==', val: 'foo' }),
|
||||
]),
|
||||
);
|
||||
// Null column → IS NULL filter, not an exact match on null.
|
||||
expect(filters.drillToDetail).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ col: '__timestamp', op: 'IS NULL' }),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(filters.crossFilter).toBeDefined();
|
||||
expect(filters.drillBy).toEqual({
|
||||
filters: [{ col: 'name', op: '==', val: 'Michael' }],
|
||||
groupbyFieldName: 'groupby',
|
||||
});
|
||||
});
|
||||
|
||||
test('right-clicking a null cell emits an IS NULL drillBy filter with a null val', async () => {
|
||||
const onContextMenu = jest.fn();
|
||||
renderChart(onContextMenu);
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('__timestamp'),
|
||||
data: { __timestamp: null, name: 'Michael', sum__num: 2467063 },
|
||||
value: null,
|
||||
event: {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [, , filters] = onContextMenu.mock.calls[0];
|
||||
// op and val must agree: IS NULL must carry a null val, not the clicked
|
||||
// cell's (possibly wrapped) value.
|
||||
expect(filters.drillBy).toEqual({
|
||||
filters: [{ col: '__timestamp', op: 'IS NULL', val: null }],
|
||||
groupbyFieldName: 'groupby',
|
||||
});
|
||||
});
|
||||
|
||||
test('right-clicking a blank (empty-string) date cell emits IS NULL, not an equality filter on an invalid date', async () => {
|
||||
// A blank temporal value arrives wrapped as DateWithFormatter(input: ''),
|
||||
// not null/undefined -- the null checks below must treat that the same
|
||||
// as null rather than falling through to the temporal/equality branches,
|
||||
// which would build an invalid Date or serialize the filter value as null
|
||||
// under an '==' op instead of an 'IS NULL' op.
|
||||
const onContextMenu = jest.fn();
|
||||
const blankDate = new DateWithFormatter('');
|
||||
renderChart(onContextMenu);
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('__timestamp'),
|
||||
data: { __timestamp: blankDate, name: 'Michael', sum__num: 2467063 },
|
||||
value: blankDate,
|
||||
event: {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [, , filters] = onContextMenu.mock.calls[0];
|
||||
expect(filters.drillToDetail).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ col: '__timestamp', op: 'IS NULL' }),
|
||||
]),
|
||||
);
|
||||
expect(filters.drillBy).toEqual({
|
||||
filters: [{ col: '__timestamp', op: 'IS NULL', val: null }],
|
||||
groupbyFieldName: 'groupby',
|
||||
});
|
||||
});
|
||||
|
||||
test('right-clicking a metric cell omits crossFilter and drillBy', async () => {
|
||||
const onContextMenu = jest.fn();
|
||||
renderChart(onContextMenu);
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('sum__num', { isMetric: true }),
|
||||
data: { name: 'Michael', sum__num: 2467063 },
|
||||
value: 2467063,
|
||||
event: {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [, , filters] = onContextMenu.mock.calls[0];
|
||||
expect(filters.crossFilter).toBeUndefined();
|
||||
expect(filters.drillBy).toBeUndefined();
|
||||
// drillToDetail is still populated from the row's dimension columns.
|
||||
expect(filters.drillToDetail.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('right-clicking a temporal cell with a time grain emits a TEMPORAL_RANGE filter', async () => {
|
||||
const onContextMenu = jest.fn();
|
||||
renderChart(onContextMenu, { timeGrain: TimeGranularity.DAY });
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('name'),
|
||||
data: {
|
||||
__timestamp: '2020-01-01T12:34:56.000Z',
|
||||
name: 'Michael',
|
||||
sum__num: 2467063,
|
||||
},
|
||||
value: 'Michael',
|
||||
event: {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [, , filters] = onContextMenu.mock.calls[0];
|
||||
const timestampFilter = filters.drillToDetail.find(
|
||||
(f: { col: string }) => f.col === '__timestamp',
|
||||
);
|
||||
expect(timestampFilter.op).toBe('TEMPORAL_RANGE');
|
||||
// DAY granularity's range starts at the row's own timestamp (not
|
||||
// truncated to midnight) and ends at the start of the next UTC day.
|
||||
expect(timestampFilter.val).toBe(
|
||||
'2020-01-01T12:34:56.000Z : 2020-01-02T00:00:00.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not call onContextMenu in raw records mode', async () => {
|
||||
const onContextMenu = jest.fn();
|
||||
// isRawRecords is derived from query_mode inside transformProps; force it
|
||||
// here to isolate the handler's own guard from that derivation.
|
||||
renderChart(onContextMenu, { isRawRecords: true });
|
||||
await waitFor(() => expect(captured.props?.onCellContextMenu).toBeDefined());
|
||||
|
||||
captured.props?.onCellContextMenu?.({
|
||||
column: makeColumn('name'),
|
||||
data: { name: 'Michael' },
|
||||
value: 'Michael',
|
||||
event: {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(onContextMenu).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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 { CurrencyFormatter, DateWithFormatter } from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { ValueFormatterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import {
|
||||
formatColumnValue,
|
||||
valueFormatter,
|
||||
valueGetter,
|
||||
} from '../src/utils/formatValue';
|
||||
import { DataColumnMeta, InputColumn } from '../src/types';
|
||||
|
||||
const baseCol: InputColumn = {
|
||||
key: 'order_date',
|
||||
label: 'order_date',
|
||||
dataType: GenericDataType.Temporal,
|
||||
isNumeric: false,
|
||||
isMetric: false,
|
||||
isPercentMetric: false,
|
||||
config: {},
|
||||
};
|
||||
|
||||
function makeParams(value: unknown, node?: { level?: number }) {
|
||||
return {
|
||||
value,
|
||||
node,
|
||||
data: {},
|
||||
} as unknown as ValueFormatterParams;
|
||||
}
|
||||
|
||||
test('valueFormatter never returns a raw Date/object when col.formatter is unset', () => {
|
||||
// Regression test: order_date (or any temporal column) is wrapped into a
|
||||
// DateWithFormatter instance before reaching this function. If col.formatter
|
||||
// is undefined - or returns a falsy result - the old `|| value` fallback
|
||||
// returned that raw object, which crashes React with "Objects are not valid
|
||||
// as a React child" once a cell renderer renders it directly.
|
||||
const date = new DateWithFormatter(1069113600000);
|
||||
const result = valueFormatter(makeParams(date), {
|
||||
...baseCol,
|
||||
formatter: undefined,
|
||||
});
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).not.toBe(date);
|
||||
});
|
||||
|
||||
test('valueFormatter falls back to a string when the formatter returns a falsy result', () => {
|
||||
const date = new DateWithFormatter(1069113600000);
|
||||
const formatter = jest.fn().mockReturnValue('');
|
||||
const result = valueFormatter(makeParams(date), {
|
||||
...baseCol,
|
||||
formatter: formatter as unknown as InputColumn['formatter'],
|
||||
});
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).not.toBe(date);
|
||||
});
|
||||
|
||||
test('valueFormatter falls back to a string when the CurrencyFormatter returns a falsy result', () => {
|
||||
const currencyFormatter = new CurrencyFormatter({
|
||||
currency: { symbol: 'USD', symbolPosition: 'prefix' },
|
||||
});
|
||||
jest.spyOn(currencyFormatter, 'format').mockReturnValue('');
|
||||
|
||||
const result = valueFormatter(makeParams(42), {
|
||||
...baseCol,
|
||||
dataType: GenericDataType.Numeric,
|
||||
formatter: currencyFormatter,
|
||||
});
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toBe('42');
|
||||
});
|
||||
|
||||
test('valueFormatter uses the formatter result when it is truthy', () => {
|
||||
const formatter = jest.fn().mockReturnValue('2003-11-18');
|
||||
const result = valueFormatter(
|
||||
makeParams(new DateWithFormatter(1069113600000)),
|
||||
{
|
||||
...baseCol,
|
||||
formatter: formatter as unknown as InputColumn['formatter'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe('2003-11-18');
|
||||
});
|
||||
|
||||
test('valueFormatter returns N/A for a DateWithFormatter wrapping a null input', () => {
|
||||
const nullDate = new DateWithFormatter(null);
|
||||
const result = valueFormatter(makeParams(nullDate), baseCol);
|
||||
|
||||
expect(result).toBe('N/A');
|
||||
});
|
||||
|
||||
test('valueFormatter returns empty string for the root aggregation row', () => {
|
||||
const result = valueFormatter(makeParams(undefined, { level: -1 }), baseCol);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('valueGetter returns the main column value when colDef.isMain is set', () => {
|
||||
const params = {
|
||||
colDef: { isMain: true },
|
||||
column: { getColId: () => 'sum__num' },
|
||||
data: { 'Main sum__num': 42 },
|
||||
} as unknown as Parameters<typeof valueGetter>[0];
|
||||
|
||||
expect(valueGetter(params, baseCol)).toBe(42);
|
||||
});
|
||||
|
||||
test('valueGetter returns undefined for missing numeric column values', () => {
|
||||
const params = {
|
||||
column: { getColId: () => 'sum__num' },
|
||||
data: {},
|
||||
} as unknown as Parameters<typeof valueGetter>[0];
|
||||
|
||||
expect(valueGetter(params, { ...baseCol, isNumeric: true })).toBeUndefined();
|
||||
});
|
||||
|
||||
test('valueGetter returns empty string for missing non-numeric column values', () => {
|
||||
const params = {
|
||||
column: { getColId: () => 'name' },
|
||||
data: {},
|
||||
} as unknown as Parameters<typeof valueGetter>[0];
|
||||
|
||||
expect(valueGetter(params, baseCol)).toBe('');
|
||||
});
|
||||
|
||||
test('formatColumnValue applies the small-number formatter for values under 1 in AUTO currency mode', () => {
|
||||
const column: DataColumnMeta = {
|
||||
key: 'pct',
|
||||
label: 'pct',
|
||||
dataType: GenericDataType.Numeric,
|
||||
isNumeric: true,
|
||||
isMetric: true,
|
||||
isPercentMetric: false,
|
||||
formatter: new CurrencyFormatter({
|
||||
currency: { symbol: 'AUTO', symbolPosition: 'prefix' },
|
||||
}),
|
||||
config: {},
|
||||
};
|
||||
|
||||
const [isHtml, formatted] = formatColumnValue(column, 0.005);
|
||||
|
||||
expect(isHtml).toBe(false);
|
||||
expect(formatted).not.toBe('');
|
||||
});
|
||||
|
||||
test('formatColumnValue renders null as N/A', () => {
|
||||
const column: DataColumnMeta = {
|
||||
...baseCol,
|
||||
formatter: undefined,
|
||||
};
|
||||
|
||||
expect(formatColumnValue(column, null)).toEqual([false, 'N/A']);
|
||||
});
|
||||
@@ -54,11 +54,12 @@ test('transformProps busts its memoization caches when sub-field inputs change (
|
||||
const first = transformProps(testData.basic);
|
||||
|
||||
// `processColumns` is wrapped with a custom equality (`isEqualColumns`) that
|
||||
// compares specific chartProps sub-fields by identity — mutating only the
|
||||
// top-level props reference is NOT enough to bust it. Here we supply a fresh
|
||||
// `datasource.columnFormats` reference, which `isEqualColumns` compares with
|
||||
// `===`, forcing `processColumns` to recompute and return a new `columns`
|
||||
// array.
|
||||
// compares specific chartProps sub-fields by value — mutating only the
|
||||
// top-level props reference is NOT enough to bust it, and neither is
|
||||
// handing it a new-but-value-equal `columnFormats` reference (e.g. another
|
||||
// `{}`). Here we supply a `datasource.columnFormats` with genuinely
|
||||
// different content, forcing `processColumns` to recompute and return a
|
||||
// new `columns` array.
|
||||
//
|
||||
// `processDataRecords` uses memoize-one's default referential equality on
|
||||
// `(data, columns)`. We also hand it a fresh `queriesData[0].data` array, so
|
||||
@@ -67,7 +68,7 @@ test('transformProps busts its memoization caches when sub-field inputs change (
|
||||
...testData.basic,
|
||||
datasource: {
|
||||
...testData.basic.datasource,
|
||||
columnFormats: {},
|
||||
columnFormats: { name: '.2f' },
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { convertFilterModel } from '../src/stateConversion';
|
||||
import {
|
||||
convertFilterModel,
|
||||
convertAgGridStateToOwnState,
|
||||
} from '../src/stateConversion';
|
||||
|
||||
const baseAgGridState = {
|
||||
columnState: [],
|
||||
sortModel: [{ colId: 'name', sort: 'asc' as const, sortIndex: 0 }],
|
||||
filterModel: {},
|
||||
};
|
||||
|
||||
describe('convertFilterModel', () => {
|
||||
test('emits a clause for a valid numeric comparison filter', () => {
|
||||
@@ -71,3 +80,38 @@ describe('convertFilterModel', () => {
|
||||
expect(result?.sqlClauses?.constructor).toBe('constructor = 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAgGridStateToOwnState', () => {
|
||||
test('suppresses client-mode state for the live query (serverPagination: false)', () => {
|
||||
const result = convertAgGridStateToOwnState({
|
||||
...baseAgGridState,
|
||||
serverPagination: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
test('converts client-mode state anyway when forExport is set, so a download reproduces the displayed sort/filter', () => {
|
||||
const result = convertAgGridStateToOwnState(
|
||||
{ ...baseAgGridState, serverPagination: false },
|
||||
{ forExport: true },
|
||||
);
|
||||
|
||||
expect(result.sortBy).toEqual([{ id: 'name', key: 'name', desc: false }]);
|
||||
});
|
||||
|
||||
test('converts state when serverPagination is undefined, preserving legacy persisted table_state/permalinks saved before this field existed', () => {
|
||||
const result = convertAgGridStateToOwnState(baseAgGridState);
|
||||
|
||||
expect(result.sortBy).toEqual([{ id: 'name', key: 'name', desc: false }]);
|
||||
});
|
||||
|
||||
test('converts state for the live query when serverPagination is true', () => {
|
||||
const result = convertAgGridStateToOwnState({
|
||||
...baseAgGridState,
|
||||
serverPagination: true,
|
||||
});
|
||||
|
||||
expect(result.sortBy).toEqual([{ id: 'name', key: 'name', desc: false }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,6 +266,69 @@ test('uses description from column even when verboseMap renames the column', ()
|
||||
expect(columnMeta!.description).toBe('Original column description');
|
||||
});
|
||||
|
||||
test('does not crash when datasource omits metrics/columns (drill-to-detail datasource)', () => {
|
||||
const props = createMockChartProps({
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ col_x: 10 }],
|
||||
colnames: ['col_x'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
datasource: {} as unknown as TableChartProps['datasource'],
|
||||
});
|
||||
|
||||
expect(() => transformProps(props)).not.toThrow();
|
||||
});
|
||||
|
||||
test('does not mistake the all_records percent-metric query for the totals query', () => {
|
||||
// buildQuery.ts appends both an "all records" percent-metric denominator
|
||||
// query and a totals query as independent extraQueries when percent
|
||||
// metrics with percent_metric_calculation "all_records" and show_totals
|
||||
// are both enabled — queriesData has 3 entries, not 2.
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: ['sum__num'],
|
||||
percent_metrics: ['sum__num'],
|
||||
percent_metric_calculation: 'all_records',
|
||||
show_totals: true,
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ name: 'a', sum__num: 1 }],
|
||||
colnames: ['name', 'sum__num'],
|
||||
coltypes: [GenericDataType.String, GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
// all_records extra query: raw percent-metric denominator, not totals.
|
||||
{
|
||||
data: [{ sum__num: 100 }],
|
||||
colnames: ['sum__num'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
},
|
||||
// totals extra query: the real one.
|
||||
{
|
||||
data: [{ sum__num: 42 }],
|
||||
colnames: ['sum__num'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
expect(result.totals).toEqual({ sum__num: 42 });
|
||||
});
|
||||
|
||||
test('excludes Green/Red color-scheme rules from columnColorFormatters', () => {
|
||||
// Green/Red rules are rendered via the increase/decrease path, so they must
|
||||
// not reach getColorFormatters, which would treat the scheme name as a hex
|
||||
@@ -316,6 +379,59 @@ test('excludes Green/Red color-scheme rules from columnColorFormatters', () => {
|
||||
expect(formattedColumns).not.toContain('metric_a');
|
||||
});
|
||||
|
||||
test('allowRearrangeColumns defaults to true when allow_rearrange_columns is unset', () => {
|
||||
// Pre-existing v2 charts saved before this control existed have no
|
||||
// allow_rearrange_columns key at all -- they must keep the always-on
|
||||
// behavior v2 originally shipped with, not v1's false default.
|
||||
const props = createMockChartProps();
|
||||
const result = transformProps(props);
|
||||
expect(result.allowRearrangeColumns).toBe(true);
|
||||
});
|
||||
|
||||
test('allowRearrangeColumns is false when allow_rearrange_columns is explicitly false', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: [],
|
||||
percent_metrics: [],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
allow_rearrange_columns: false,
|
||||
} as unknown as TableChartProps['rawFormData'],
|
||||
});
|
||||
const result = transformProps(props);
|
||||
expect(result.allowRearrangeColumns).toBe(false);
|
||||
});
|
||||
|
||||
test('allowRenderHtml defaults to true when allow_render_html is unset', () => {
|
||||
const props = createMockChartProps();
|
||||
const result = transformProps(props);
|
||||
expect(result.allowRenderHtml).toBe(true);
|
||||
});
|
||||
|
||||
test('allowRenderHtml is false when allow_render_html is explicitly false', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: [],
|
||||
percent_metrics: [],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
allow_render_html: false,
|
||||
} as unknown as TableChartProps['rawFormData'],
|
||||
});
|
||||
const result = transformProps(props);
|
||||
expect(result.allowRenderHtml).toBe(false);
|
||||
});
|
||||
|
||||
test('retains saved percentage rules with automatic bounds when server pagination is enabled', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
|
||||
@@ -94,6 +94,39 @@ test('applies the increase/decrease background when the column has one', () => {
|
||||
expect(style.backgroundColor).toBe('#00ff00');
|
||||
});
|
||||
|
||||
test('applies a cross-column formatter to its target column, keyed off the source column value', () => {
|
||||
// Rule reads metric_a (source) and paints metric_b (target, via columnFormatting).
|
||||
const crossColumnFormatter = {
|
||||
column: 'metric_a',
|
||||
columnFormatting: 'metric_b',
|
||||
getColorFromValue: (v: number) => (v === 100 ? '#ff0000' : undefined),
|
||||
objectFormatting: undefined,
|
||||
toTextColor: false,
|
||||
};
|
||||
|
||||
const targetStyle = getCellStyle(
|
||||
buildParams({
|
||||
colDef: { field: 'metric_b' },
|
||||
value: 999,
|
||||
hasColumnColorFormatters: true,
|
||||
columnColorFormatters: [crossColumnFormatter],
|
||||
node: { rowPinned: undefined, data: { metric_a: 100, metric_b: 999 } },
|
||||
}),
|
||||
);
|
||||
expect(targetStyle.backgroundColor).toBe('#ff0000');
|
||||
|
||||
const sourceStyle = getCellStyle(
|
||||
buildParams({
|
||||
colDef: { field: 'metric_a' },
|
||||
value: 100,
|
||||
hasColumnColorFormatters: true,
|
||||
columnColorFormatters: [crossColumnFormatter],
|
||||
node: { rowPinned: undefined, data: { metric_a: 100, metric_b: 999 } },
|
||||
}),
|
||||
);
|
||||
expect(sourceStyle.backgroundColor).toBe('');
|
||||
});
|
||||
|
||||
test('does not apply basic formatting to the pinned summary row', () => {
|
||||
const style = getCellStyle(
|
||||
buildParams({
|
||||
@@ -106,3 +139,37 @@ test('does not apply basic formatting to the pinned summary row', () => {
|
||||
);
|
||||
expect(style.backgroundColor).toBe('');
|
||||
});
|
||||
|
||||
test('applies a legacy v1 toAllRow formatter to every cell in the row', () => {
|
||||
// Migrated v1 charts carry `toAllRow: true` unchanged rather than being
|
||||
// rewritten to `columnFormatting: ENTIRE_ROW`; both must color every cell.
|
||||
const legacyEntireRowFormatter = {
|
||||
column: 'metric_a',
|
||||
toAllRow: true,
|
||||
getColorFromValue: (v: number) => (v === 100 ? '#ff0000' : undefined),
|
||||
objectFormatting: undefined,
|
||||
toTextColor: false,
|
||||
};
|
||||
|
||||
const otherColumnStyle = getCellStyle(
|
||||
buildParams({
|
||||
colDef: { field: 'metric_b' },
|
||||
value: 999,
|
||||
hasColumnColorFormatters: true,
|
||||
columnColorFormatters: [legacyEntireRowFormatter],
|
||||
node: { rowPinned: undefined, data: { metric_a: 100, metric_b: 999 } },
|
||||
}),
|
||||
);
|
||||
expect(otherColumnStyle.backgroundColor).toBe('#ff0000');
|
||||
|
||||
const sourceColumnStyle = getCellStyle(
|
||||
buildParams({
|
||||
colDef: { field: 'metric_a' },
|
||||
value: 100,
|
||||
hasColumnColorFormatters: true,
|
||||
columnColorFormatters: [legacyEntireRowFormatter],
|
||||
node: { rowPinned: undefined, data: { metric_a: 100, metric_b: 999 } },
|
||||
}),
|
||||
);
|
||||
expect(sourceColumnStyle.backgroundColor).toBe('#ff0000');
|
||||
});
|
||||
|
||||
@@ -811,6 +811,79 @@ test('cellStyle defaults non-numeric columns to left alignment', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('cellStyle reflects an edited conditional-formatting rule (color/threshold change, same column)', () => {
|
||||
// columnColorFormatters entries only carry a computed getColorFromValue
|
||||
// closure -- the rule's operator/threshold/color aren't mirrored onto the
|
||||
// entry itself. Memoizing on JSON.stringify(columnColorFormatters) alone
|
||||
// would see the same "shape" on both renders and keep closing over the
|
||||
// first render's (red) formatter. The memo must instead depend on the raw
|
||||
// conditionalFormatting config, which does capture the color/threshold.
|
||||
const numericCol = makeColumn({
|
||||
key: 'count',
|
||||
label: 'Count',
|
||||
dataType: GenericDataType.Numeric,
|
||||
isNumeric: true,
|
||||
isMetric: true,
|
||||
});
|
||||
// getCommonColProps also depends on `columns`/`data` by reference (as it
|
||||
// must, since transformProps.ts doesn't memoize them either), so those
|
||||
// need to stay referentially stable across rerenders here -- otherwise a
|
||||
// new array on every render would mask a broken formatter dependency by
|
||||
// invalidating the memo for an unrelated reason.
|
||||
const stableColumns = [numericCol];
|
||||
const stableData = [{ count: 42 }];
|
||||
|
||||
const cellStyleParams = {
|
||||
value: 42,
|
||||
colDef: { field: 'count' },
|
||||
rowIndex: 0,
|
||||
node: {},
|
||||
} as never;
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { color: string; targetValue: number }) =>
|
||||
useColDefs({
|
||||
...defaultProps,
|
||||
columns: stableColumns,
|
||||
data: stableData,
|
||||
columnColorFormatters: [
|
||||
{
|
||||
column: 'count',
|
||||
objectFormatting: ObjectFormattingEnum.BACKGROUND_COLOR,
|
||||
getColorFromValue: (value: unknown) =>
|
||||
value === 42 ? props.color : undefined,
|
||||
},
|
||||
],
|
||||
conditionalFormatting: [
|
||||
{
|
||||
column: 'count',
|
||||
operator: '>',
|
||||
targetValue: props.targetValue,
|
||||
colorScheme: props.color,
|
||||
} as never,
|
||||
],
|
||||
}),
|
||||
{
|
||||
wrapper: defaultThemeWrapper,
|
||||
initialProps: { color: '#ff0000', targetValue: 0 },
|
||||
},
|
||||
);
|
||||
|
||||
const firstCellStyle = getCellStyleFunction(result.current[0].cellStyle);
|
||||
expect(firstCellStyle(cellStyleParams)).toMatchObject({
|
||||
backgroundColor: '#ff0000',
|
||||
});
|
||||
|
||||
// Same column, edited threshold/color -- must produce a fresh colDef
|
||||
// whose cellStyle uses the new formatter, not the stale red one.
|
||||
rerender({ color: '#0000ff', targetValue: 10 });
|
||||
|
||||
const secondCellStyle = getCellStyleFunction(result.current[0].cellStyle);
|
||||
expect(secondCellStyle(cellStyleParams)).toMatchObject({
|
||||
backgroundColor: '#0000ff',
|
||||
});
|
||||
});
|
||||
|
||||
test('cellStyle respects explicit horizontal alignment overrides', () => {
|
||||
const numericCol = makeColumn({
|
||||
key: 'count',
|
||||
|
||||
+314
-1293
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
@@ -29,27 +29,27 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-46", "NAME_1": "Aïn Témouchent" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -1.361975709870848, 35.319898656042554 ], [ -1.267730272999927, 35.390326239000046 ], [ -1.183338995999918, 35.57648346600007 ], [ -1.106353318999936, 35.623236395000049 ], [ -1.006728888227599, 35.524912014078154 ], [ -1.06920569458714, 35.454451199263417 ], [ -1.059025438273352, 35.411533922552451 ], [ -1.017787644584132, 35.455872300766032 ], [ -0.954122279819387, 35.437320462369428 ], [ -0.715428840368702, 35.482485662882993 ], [ -0.629490933259945, 35.409621893735391 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.890456915953905, 35.159895535450573 ], [ -1.000527715581597, 35.087393499710231 ], [ -1.271622280443921, 35.195655626006328 ], [ -1.337664761119868, 35.190823879818197 ], [ -1.320663215435047, 35.283789780973564 ], [ -1.361975709870848, 35.319898656042554 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-19", "NAME_1": "Sétif" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.838444044756841, 36.304967760054069 ], [ 4.789919874602504, 36.333854885294727 ], [ 4.801081983746542, 36.379743557119468 ], [ 4.922573275835987, 36.38940705039505 ], [ 4.986807082281018, 36.462425849173599 ], [ 4.949858433320003, 36.502991848395084 ], [ 5.045304803174133, 36.524179186020604 ], [ 5.076517368381815, 36.574305324830846 ], [ 5.187621697683198, 36.53448863444288 ], [ 5.165555860914253, 36.494155178318806 ], [ 5.187776727314088, 36.408423976785059 ], [ 5.255421176645996, 36.378865057976043 ], [ 5.303221877287797, 36.389303696708225 ], [ 5.327871534643577, 36.449300035268379 ], [ 5.457889439346161, 36.499917101393066 ], [ 5.475459424912515, 36.590635076947194 ], [ 5.542793816781227, 36.523972480445593 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.776164585427239, 36.439998277198697 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.94018558060003, 35.862126573082833 ], [ 5.822880079552021, 35.914733181491101 ], [ 5.745623813787972, 35.89021271444517 ], [ 5.711620720619749, 35.849672552746085 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.423472935027917, 35.635654609072901 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.065768670387797, 35.762081000138039 ], [ 5.054916618706955, 35.820862941871269 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.034762810755126, 36.310884710960636 ], [ 4.838444044756841, 36.304967760054069 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-34", "NAME_1": "Bordj Bou Arréridj" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.801081983746542, 36.379743557119468 ], [ 4.785217318724278, 36.346644802415653 ], [ 4.821235792597747, 36.306853950449408 ], [ 5.034762810755126, 36.310884710960636 ], [ 5.125558301574358, 36.232362372425541 ], [ 5.188086785676603, 36.245514023853104 ], [ 5.212426384669925, 36.196653957813908 ], [ 5.25356082467232, 36.197377428225707 ], [ 5.184056024266113, 36.115366929739992 ], [ 5.139976026672286, 35.856623033326287 ], [ 5.066543816743717, 35.840474148363228 ], [ 5.05445153161287, 35.777325548435272 ], [ 4.854928827403455, 35.873159491917022 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.356354608178606, 36.340443631568348 ], [ 4.423378939886163, 36.22616119977954 ], [ 4.511383905442869, 36.225747789528839 ], [ 4.630084669571829, 36.253343004476051 ], [ 4.635820754224426, 36.338454088385447 ], [ 4.748010288244188, 36.420748805912638 ], [ 4.801081983746542, 36.379743557119468 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.356354608178606, 36.340443631568348 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ], [ 4.356354608178606, 36.340443631568348 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-10", "NAME_1": "Bouira" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.398729281631006, 36.373697415003676 ], [ 4.093631626298532, 36.046068834009475 ], [ 4.035133905405416, 35.870808214427541 ], [ 3.954156934794696, 35.890755316804416 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.578934359632967, 35.999353338985372 ], [ 3.551752556735096, 36.298301500313983 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.456357862825087, 36.576837470372936 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.477441846763782, 36.688561917298614 ], [ 3.635726759082672, 36.694401352940076 ], [ 3.680736931763988, 36.666806138892184 ], [ 3.641927930829354, 36.600401923010338 ], [ 3.733653599434035, 36.607610785109614 ], [ 3.888786247687165, 36.488522447353034 ], [ 4.37092736200816, 36.47265778323009 ], [ 4.398729281631006, 36.373697415003676 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-09", "NAME_1": "Blida" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 3.477441846763782, 36.688561917298614 ], [ 3.510049675951791, 36.623165391769419 ], [ 3.467364943237556, 36.583167833328844 ], [ 3.321792432774544, 36.550379136987544 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.008064812940745, 36.356437486001141 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.824458448798339, 36.365170803289914 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.64338423019808, 36.397261868540397 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.499723747652808, 36.470384020106394 ], [ 2.602456495770639, 36.467438463414339 ], [ 2.837377557128548, 36.637867336808142 ], [ 3.02992394413468, 36.662232774223128 ], [ 3.025118036368269, 36.71005931238733 ], [ 3.097413363835642, 36.649933784416589 ], [ 3.323652784748219, 36.712307237089306 ], [ 3.477441846763782, 36.688561917298614 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.712691685372988, 35.92341482193649 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ], [ 1.712691685372988, 35.92341482193649 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-38", "NAME_1": "Tissemsilt" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.638484328189179, 35.934292711139676 ], [ 1.853561639058398, 35.860498766005207 ], [ 1.964510938728836, 35.879825750757789 ], [ 2.008745965054231, 35.917911282179887 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.158194207296845, 35.698234768219947 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.249713169427196, 35.559612739236513 ], [ 1.744937777556402, 35.551938788244456 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.658069696259474, 35.547933865255629 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.346460809019106, 35.68981151019301 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.37720828623344, 35.788048408007626 ], [ 1.447953321888292, 35.914268093497697 ], [ 1.504900750069567, 35.935222887126542 ], [ 1.548360630039042, 36.003952542076149 ], [ 1.638484328189179, 35.934292711139676 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-44", "NAME_1": "Aïn Defla" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.853561639058398, 35.860498766005207 ], [ 1.712691685372988, 35.92341482193649 ], [ 1.717187533877564, 35.973205063962496 ], [ 1.588513218110506, 36.143298041471439 ], [ 1.628717482126092, 36.2114850940618 ], [ 1.594921095432198, 36.234377753130786 ], [ 1.603292676615695, 36.279077867449587 ], [ 1.553166537805453, 36.367677110410284 ], [ 1.647786086258861, 36.443150540365139 ], [ 1.716722445884159, 36.36666942005769 ], [ 1.793048536560718, 36.43198843032178 ], [ 2.476262647802969, 36.420619614703412 ], [ 2.52251305483361, 36.363982244884653 ], [ 2.652685988267706, 36.39209422556803 ], [ 2.647880079601975, 36.33251129815784 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.531659784171666, 36.189574286024481 ], [ 2.577445103208959, 36.211795152424315 ], [ 2.639043410425018, 36.181564439147508 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.311776563737396, 35.86695832017034 ], [ 2.171681756407907, 35.917601222918108 ], [ 2.008745965054231, 35.917911282179887 ], [ 1.964510938728836, 35.879825750757789 ], [ 1.853561639058398, 35.860498766005207 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-48", "NAME_1": "Relizane" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.411624789652308, 35.841559353081664 ], [ 1.361808709204581, 35.776524562758368 ], [ 1.275715773364254, 35.775491033983997 ], [ 1.245588412874952, 35.647333482154465 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.01795372888148, 35.544238999730055 ], [ 0.959456007988365, 35.54775299720302 ], [ 0.866076693884281, 35.436080227120783 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.229371372091123, 35.695935167573907 ], [ 0.423984815746621, 35.817633165238362 ], [ 0.451735060324779, 35.951966051292231 ], [ 0.529043002932212, 35.968063259411849 ], [ 0.542840610405847, 36.048962713858145 ], [ 0.704071079416792, 36.186525377444184 ], [ 0.897392611879525, 36.197635809744781 ], [ 0.948138869213437, 36.032012844117446 ], [ 1.040949740737915, 36.025449937164865 ], [ 1.159340448303055, 35.94597158512056 ], [ 1.288893263213538, 35.965867011553314 ], [ 1.356227655082193, 35.868146878575544 ], [ 1.411624789652308, 35.841559353081664 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-32", "NAME_1": "El Bayadh" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.340785759755022, 34.234138088701798 ], [ 0.729495884027813, 34.435469468940823 ], [ 0.973718702556084, 34.382087714176635 ], [ 1.031958041930068, 34.339351305518335 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.409144320953658, 33.961803290389526 ], [ 1.793978713446847, 33.816411647979123 ], [ 2.019287956573919, 33.499040839463134 ], [ 2.031380242604087, 33.392535712353208 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.054221225728952, 33.108909613687729 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.291467726154622, 32.695989080731465 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.344849480918811, 32.172791043471875 ], [ 2.315652297315637, 32.062177638786977 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.24211673280098, 31.791186428510855 ], [ 2.199432000086745, 31.735246689782912 ], [ 2.085382114093306, 31.658584703221436 ], [ 0.842460565302815, 31.096939399333792 ], [ 0.399128451916511, 30.708694363054406 ], [ -0.154016078679092, 31.123785305447484 ], [ -0.400822719592838, 31.390668239947274 ], [ -0.003585985404925, 32.443963120901287 ], [ -0.012887742575288, 32.500006212416736 ], [ -0.083942836592655, 32.524397488253442 ], [ -0.10156449990177, 32.567108059389398 ], [ -0.006686570828606, 32.804767970965088 ], [ -0.027047085254765, 33.02062042908949 ], [ 0.063490024045336, 33.308096422012227 ], [ 0.050622592558568, 33.803104966920671 ], [ 0.086020948807743, 34.048309638279136 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.340785759755022, 34.234138088701798 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.75760786291255, 34.438182480736884 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.75760786291255, 34.438182480736884 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-20", "NAME_1": "Saïda" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.856878288602104, 34.499858303218105 ], [ 0.351482781804918, 34.238117174168224 ], [ 0.235417515106235, 34.225869859406487 ], [ 0.086020948807743, 34.048309638279136 ], [ -0.044100307782969, 33.987253933422267 ], [ -0.081772427155784, 34.08153758419212 ], [ -0.220213588985928, 34.059445909001454 ], [ -0.346614141629345, 33.955214545015224 ], [ -0.356122606173358, 33.915036119421359 ], [ -0.499679735031748, 34.193158678330292 ], [ -0.060895147892779, 34.550087795715172 ], [ -0.056502651276332, 34.609050605501011 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.24687862794633, 35.098891506537768 ], [ -0.135567593069993, 35.12764944146852 ], [ -0.111693081170756, 35.160593167440709 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.014334683398943, 35.06385488549455 ], [ 0.112220899774684, 35.096100979476603 ], [ 0.189993931274898, 35.023495591848075 ], [ 0.625057813567253, 35.095920112323313 ], [ 0.754197218227034, 35.007837633300142 ], [ 0.783032668422891, 34.948151353102446 ], [ 0.669757927886053, 34.855495510309538 ], [ 0.673478630934085, 34.789375515267807 ], [ 0.856878288602104, 34.499858303218105 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-22", "NAME_1": "Sidi Bel Abbès" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -0.16517818782313, 35.122921047168575 ], [ -0.250702683781867, 35.093568833934512 ], [ -0.236284958683882, 35.023805650210591 ], [ -0.299278529880269, 34.98233531342396 ], [ -0.272716843707371, 34.904329738826391 ], [ -0.36805986167326, 34.869654852989072 ], [ -0.114380256343793, 34.682431139485459 ], [ -0.055004035407933, 34.60403799126027 ], [ -0.071902228305191, 34.529701442867292 ], [ -0.485727097927224, 34.229332180036067 ], [ -0.523140834881644, 34.290362047370593 ], [ -0.578176235145122, 34.312944648077007 ], [ -0.836196662046348, 34.215121161412469 ], [ -1.120417039015138, 34.271293443237766 ], [ -0.880018276322403, 34.467612210135371 ], [ -0.903789435434135, 34.547193914967181 ], [ -0.829582078250326, 34.578380641753199 ], [ -0.758475308288837, 34.711912543029428 ], [ -0.775631882705227, 34.737440701327273 ], [ -0.886116095281579, 34.74302175634898 ], [ -0.89056026874141, 34.843609930753701 ], [ -0.941358201120067, 34.842576401979329 ], [ -0.924149949860293, 34.881514594123189 ], [ -0.96611121306205, 34.922726549390745 ], [ -0.929834356770186, 35.047163398172245 ], [ -1.000527715581597, 35.087393499710231 ], [ -0.890456915953905, 35.159895535450573 ], [ -0.880845098622387, 35.254308377429652 ], [ -0.753669398723787, 35.318257962135249 ], [ -0.668868374076226, 35.332003892765442 ], [ -0.632281460321167, 35.298155829228108 ], [ -0.569184536337275, 35.405229397118944 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.16517818782313, 35.122921047168575 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-29", "NAME_1": "Mascara" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 0.229371372091123, 35.695935167573907 ], [ 0.319081659091296, 35.557778224785238 ], [ 0.456851027353025, 35.558139959991138 ], [ 0.532298617986839, 35.5099775250427 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.878168979914506, 35.374740302323062 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.448789503632668, 35.059074815250483 ], [ 0.209837680864268, 35.02148021114283 ], [ 0.112220899774684, 35.096100979476603 ], [ -0.014334683398943, 35.06385488549455 ], [ -0.091022508382082, 35.107909043767336 ], [ -0.105956997417479, 35.159740505819684 ], [ -0.16517818782313, 35.122921047168575 ], [ -0.216957973931358, 35.190927231706382 ], [ -0.189207729353257, 35.299318549211648 ], [ -0.361807013083194, 35.358772284513293 ], [ -0.463661261158279, 35.358048814101494 ], [ -0.464436408413519, 35.429388128958976 ], [ -0.545361701281479, 35.411973172124192 ], [ -0.302482468990775, 35.669399318923411 ], [ -0.220575324191884, 35.683558660703625 ], [ -0.114380256343793, 35.783759264178684 ], [ -0.043996954995464, 35.714461168448167 ], [ 0.104469435316162, 35.756732490012382 ], [ 0.152115106327074, 35.682628486515398 ], [ 0.229371372091123, 35.695935167573907 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-47", "NAME_1": "Ghardaïa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.085382114093306, 31.658584703221436 ], [ 2.239326205739815, 31.77353892678002 ], [ 2.215296665109008, 31.923297227385092 ], [ 2.34329918820697, 32.132664292922755 ], [ 2.297513869169734, 32.377300523500367 ], [ 2.291467726154622, 32.695989080731465 ], [ 3.100668979789589, 32.830683701991234 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.884445427914159, 32.981863104998297 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.978280469667936, 32.840760606416836 ], [ 4.738863559805452, 32.531890571192889 ], [ 4.359145135239771, 32.255990099463475 ], [ 4.110839877558305, 31.791393134085865 ], [ 3.802538283115268, 29.989849352465626 ], [ 3.721509636560484, 29.863345445236064 ], [ 3.479612257999293, 29.636847642804469 ], [ 3.397550082670193, 29.364874578798776 ], [ 3.34447838716784, 29.306557725958271 ], [ 2.9859989761718, 29.123545641018495 ], [ 2.079335971977514, 29.036367499560413 ], [ 1.943426954790141, 30.094804185964392 ], [ 2.071739536250561, 30.859692898008802 ], [ 2.085382114093306, 31.658584703221436 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-03", "NAME_1": "Laghouat" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.291467726154622, 32.695989080731465 ], [ 2.174937370563214, 32.97499013878388 ], [ 2.054221225728952, 33.108909613687729 ], [ 1.975207960778732, 33.292180081045899 ], [ 2.031380242604087, 33.392535712353208 ], [ 2.019287956573919, 33.499040839463134 ], [ 1.776925490019323, 33.831346137014577 ], [ 1.428109572298922, 33.951183782705414 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.981099074162955, 34.506989650951596 ], [ 2.220567660868824, 34.674679674127617 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.295808546826947, 34.439035143257229 ], [ 2.360972528359468, 34.352735500942572 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.949412062416741, 34.263619493145086 ], [ 3.093537632056041, 34.240029202085964 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.640997755741807, 33.564334011305561 ], [ 4.059060093248718, 33.252518419389503 ], [ 4.176210564665837, 33.029637966319058 ], [ 3.884445427914159, 32.981863104998297 ], [ 3.738614535932072, 33.000466620238285 ], [ 3.427470736685166, 32.87551300751926 ], [ 3.321327344781139, 32.780790107177666 ], [ 3.100668979789589, 32.830683701991234 ], [ 2.291467726154622, 32.695989080731465 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.249713169427196, 35.559612739236513 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.249713169427196, 35.559612739236513 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-26", "NAME_1": "Médéa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.285731642401345, 35.439077459756959 ], [ 2.261030308202123, 35.605578925427039 ], [ 2.169511346071772, 35.666815497437256 ], [ 2.16269005670074, 35.708983466213965 ], [ 2.241961704069354, 35.744123440044689 ], [ 2.459364454905653, 36.036999619936466 ], [ 2.571553988925416, 36.099088853567707 ], [ 2.643694288560539, 36.191537990785605 ], [ 2.526388786613211, 36.192778225134987 ], [ 2.486494581859461, 36.241974188857682 ], [ 2.526853874606616, 36.300006821757393 ], [ 2.614083692908082, 36.309670315032974 ], [ 2.661109246294643, 36.363103745741228 ], [ 2.845284051217959, 36.369227403122125 ], [ 2.909827915126186, 36.415245266156091 ], [ 2.996282586172413, 36.353285223734076 ], [ 3.203246698276473, 36.474905707032747 ], [ 3.283035108683293, 36.49017609285238 ], [ 3.422664828918698, 36.407855536004149 ], [ 3.500127801157078, 36.438964749323702 ], [ 3.555524936626512, 36.422945055569812 ], [ 3.583120150674404, 36.338660793960457 ], [ 3.552217644728501, 36.30827505105276 ], [ 3.56436160580347, 36.05097809546271 ], [ 3.633866408008316, 35.889540920876755 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.540900505953573, 35.803938910552233 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.411037631781255, 35.805928452835815 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.336726921809998, 35.747534084730205 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.081910434918598, 35.67187978852138 ], [ 2.960264113198264, 35.804739895329874 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.671031121089356, 35.44657054269635 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.285731642401345, 35.439077459756959 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-14", "NAME_1": "Tiaret" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 1.346460809019106, 35.68981151019301 ], [ 1.469192336357253, 35.648031114144601 ], [ 1.492343376945257, 35.594881904276463 ], [ 1.559057651189619, 35.606095689364565 ], [ 1.634608594610938, 35.5504660098984 ], [ 1.692021111684937, 35.580490016700878 ], [ 1.744937777556402, 35.551938788244456 ], [ 2.249713169427196, 35.559612739236513 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.360042352372659, 35.287975572015 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.357716912405579, 34.692869778217641 ], [ 2.220567660868824, 34.674679674127617 ], [ 1.981099074162955, 34.506989650951596 ], [ 1.806226027309322, 34.449086209261111 ], [ 1.691090935698128, 34.345810858784091 ], [ 1.64158491361286, 34.256152249526679 ], [ 1.336228874962615, 34.068928534224426 ], [ 1.231480747038859, 34.191892605559303 ], [ 1.023689812634757, 34.191246650412552 ], [ 1.031958041930068, 34.339351305518335 ], [ 0.973718702556084, 34.382087714176635 ], [ 0.75760786291255, 34.438182480736884 ], [ 0.856878288602104, 34.499858303218105 ], [ 0.693529086997728, 34.736329658187174 ], [ 0.667432488818292, 34.84221466677343 ], [ 0.781637404442677, 34.972697659469361 ], [ 0.652549675726959, 35.089796454043096 ], [ 0.492249382702823, 35.086334133413516 ], [ 0.617771437102135, 35.276038316515098 ], [ 0.684330681715551, 35.299551093208379 ], [ 0.778536818119676, 35.279371445935453 ], [ 0.819981317383906, 35.30611400016096 ], [ 0.880494418982266, 35.386367499460505 ], [ 0.827887810573998, 35.443469957272669 ], [ 0.881114535707241, 35.443702501269399 ], [ 0.959456007988365, 35.54775299720302 ], [ 1.01795372888148, 35.544238999730055 ], [ 1.010977409879558, 35.575038153787773 ], [ 1.206210972058727, 35.645679837554439 ], [ 1.292458936630624, 35.617645372136167 ], [ 1.346460809019106, 35.68981151019301 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.05445153161287, 35.777325548435272 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ], [ 5.05445153161287, 35.777325548435272 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-28", "NAME_1": "M'Sila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.854928827403455, 35.873159491917022 ], [ 5.226223993042822, 35.664024970376033 ], [ 5.379702995796606, 35.632915757955857 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.022980583986737, 35.520674547092653 ], [ 4.963242628744297, 35.4887901883165 ], [ 4.876477899335555, 35.519150091903214 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.779016146977597, 35.172840481303126 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.041119012132697, 34.918644111136814 ], [ 4.932805209892535, 34.838907375774738 ], [ 4.394646844276451, 34.746587428866746 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.036374138855422, 34.441929023105899 ], [ 3.921084019411978, 34.672664293422372 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.67112511623111, 34.790279852832896 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.571803012798796, 35.005227973392209 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.657585890276607, 35.374146023120431 ], [ 3.695929803217837, 35.472047024150811 ], [ 3.436927524385737, 35.671983141308885 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.394294467615566, 35.793836167704967 ], [ 3.432276646250216, 35.796600857243732 ], [ 3.543225945920653, 35.672396552458906 ], [ 3.536714714912137, 35.792234199049005 ], [ 3.60869998491637, 35.837373562040227 ], [ 3.628440382617555, 35.922794705211459 ], [ 3.735513949609071, 35.929977728889071 ], [ 3.850804070851154, 35.861971544351263 ], [ 3.954156934794696, 35.890755316804416 ], [ 4.022266473019329, 35.866854967382835 ], [ 4.084329868228849, 36.029093125847055 ], [ 4.136316359012767, 35.995632635937397 ], [ 4.214812860025461, 36.016613267987907 ], [ 4.379143913560767, 35.991085109690061 ], [ 4.494123976440335, 36.02844717070036 ], [ 4.540839470565118, 35.942147529285023 ], [ 4.449268833389965, 35.869697171287385 ], [ 4.466683791124069, 35.832283434333021 ], [ 4.620782912401467, 35.823601792988313 ], [ 4.854928827403455, 35.873159491917022 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-05", "NAME_1": "Batna" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.94018558060003, 35.862126573082833 ], [ 5.976669141567641, 35.915792547787817 ], [ 6.275513950108689, 35.866079820127595 ], [ 6.351064894429328, 35.884735012211024 ], [ 6.47694868313522, 35.840396633098123 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.584022251026056, 35.34332103154037 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.514930860870493, 35.119717108957389 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.58102501838988, 34.769660955988286 ], [ 6.508729689123868, 34.775500393428388 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.072218866007859, 35.058583888835358 ], [ 5.993102248270134, 34.989363308369946 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.932279087409938, 35.143565782434905 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.830631544909863, 35.288440660008405 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.610748325374914, 35.126383367798155 ], [ 5.596640658639501, 35.091760158804277 ], [ 5.306012404348962, 35.021428534299389 ], [ 5.184831169722713, 35.031040350731587 ], [ 5.004325391903308, 35.08545563427009 ], [ 4.925053745434013, 35.142196356876354 ], [ 4.782426791663113, 35.166871853553175 ], [ 4.888880242828918, 35.299861152470214 ], [ 4.880198602383587, 35.525402940493279 ], [ 4.963242628744297, 35.4887901883165 ], [ 5.022980583986737, 35.520674547092653 ], [ 5.1737724142655, 35.510339260248657 ], [ 5.438200717589041, 35.641855780819583 ], [ 5.476854688892786, 35.736087754746052 ], [ 5.575091586707345, 35.818279120385057 ], [ 5.736425409405115, 35.771046861423486 ], [ 5.761901889960257, 35.810915229554155 ], [ 5.71379113095594, 35.860421250740103 ], [ 5.80009077327054, 35.913467108720056 ], [ 5.94018558060003, 35.862126573082833 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-40", "NAME_1": "Khenchela" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.599680209573989, 35.327818101724006 ], [ 6.592703892370707, 35.436958727163528 ], [ 6.781684605060491, 35.562558295029248 ], [ 6.769127231036862, 35.683920395909524 ], [ 6.82498945539902, 35.623562323942053 ], [ 7.00327314603885, 35.629815172532176 ], [ 7.051228875412278, 35.577182725702187 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.413067254250393, 35.38662588097958 ], [ 7.411516960639233, 35.156097317137437 ], [ 7.222226189586991, 34.94629100202809 ], [ 7.26553103992552, 34.832344468822157 ], [ 7.264445835207141, 34.940632433539974 ], [ 7.30831912632658, 34.977193507973993 ], [ 7.365886672132206, 34.962465725412869 ], [ 7.402835321093164, 34.74604482740682 ], [ 7.274057650739962, 34.540734360802105 ], [ 7.135771519440084, 34.16057668756406 ], [ 6.960588413324615, 34.193933823786892 ], [ 6.714970329916753, 34.326613064341416 ], [ 6.696521844307654, 34.475828761687978 ], [ 6.748456659147507, 34.726097724130625 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.587897982805657, 34.778549302908004 ], [ 6.570483025970873, 34.839010728562243 ], [ 6.598595004855554, 34.900350654259285 ], [ 6.512140333809327, 35.052382717088676 ], [ 6.569707878715633, 35.253455714909308 ], [ 6.628360630138957, 35.316862698154978 ], [ 6.599680209573989, 35.327818101724006 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-24", "NAME_1": "Guelma" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 7.010559523403288, 36.155157783504876 ], [ 7.04564782128989, 36.275770575551576 ], [ 6.93795413577476, 36.432866930364526 ], [ 7.075723504036489, 36.502552598823343 ], [ 7.077118768016703, 36.545263169959298 ], [ 7.123989291772375, 36.585829169180784 ], [ 7.314727003648272, 36.651613268337655 ], [ 7.3672819352131, 36.721376451162257 ], [ 7.456837191683064, 36.693057765803246 ], [ 7.475595736553998, 36.638849188739073 ], [ 7.533783399983918, 36.615388088889176 ], [ 7.697029249700108, 36.657736924819176 ], [ 7.771546665246376, 36.581643378139347 ], [ 7.98786421136424, 36.484388333155039 ], [ 7.871437208560337, 36.432582709524411 ], [ 7.795266148413987, 36.440980130028947 ], [ 7.835005324436167, 36.334862576546641 ], [ 7.813869662754712, 36.304916083210628 ], [ 7.410431755920797, 36.181254380785049 ], [ 7.357515090049333, 36.129758816416199 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.133601108204573, 36.053148504899582 ], [ 7.066421745966807, 36.047283229937079 ], [ 7.010559523403288, 36.155157783504876 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-25", "NAME_1": "Constantine" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.93795413577476, 36.432866930364526 ], [ 7.048283318720166, 36.238408515440597 ], [ 7.010559523403288, 36.155157783504876 ], [ 6.898525018115095, 36.10689199486967 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.577149285710959, 36.10919159641503 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.599370151211474, 36.57399526646833 ], [ 6.652441846713828, 36.607895005949729 ], [ 6.777188754757219, 36.556270250371711 ], [ 6.920125766890635, 36.557148749515136 ], [ 6.904106073136745, 36.529036769731135 ], [ 6.845453321713421, 36.5314138865416 ], [ 6.818013136397099, 36.466818345789989 ], [ 6.93795413577476, 36.432866930364526 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.810167676796823, 36.431135768700756 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ], [ 5.810167676796823, 36.431135768700756 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-43", "NAME_1": "Mila" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 5.872386101637289, 36.401886909153518 ], [ 5.766242709733262, 36.45260732716639 ], [ 5.737200554861715, 36.555133367910571 ], [ 5.904322137256827, 36.545521552377693 ], [ 6.023229607860117, 36.605931301188605 ], [ 6.215310906872844, 36.582625230070278 ], [ 6.294582554241458, 36.624069729334508 ], [ 6.504078810089027, 36.577715969516362 ], [ 6.465579868416171, 36.490641180845785 ], [ 6.47849897584706, 36.428965359263884 ], [ 6.317733594829519, 36.36783214004123 ], [ 6.468370396376656, 36.273806870790452 ], [ 6.525007765296095, 36.204663804690824 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.170300734191528, 35.951242580880375 ], [ 6.149216750252833, 35.900392970758958 ], [ 5.976669141567641, 35.915792547787817 ], [ 5.983335402207047, 36.058471178402158 ], [ 5.89936119985947, 36.16967886049099 ], [ 5.933519320859943, 36.224972643172919 ], [ 5.866029901158981, 36.255410061125417 ], [ 5.872386101637289, 36.401886909153518 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-04", "NAME_1": "Oum el Bouaghi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.275513950108689, 35.866079820127595 ], [ 6.149216750252833, 35.900392970758958 ], [ 6.151387159689648, 35.935274563070607 ], [ 6.378556755689715, 36.013254299246512 ], [ 6.406151970636927, 36.11030263955513 ], [ 6.477103712766109, 36.158620103235137 ], [ 6.516326124850764, 36.16564809818118 ], [ 6.584797397381976, 36.108700669999905 ], [ 6.772692905353267, 36.192442328350751 ], [ 6.87992150197573, 36.146424465316784 ], [ 6.898525018115095, 36.10689199486967 ], [ 7.010559523403288, 36.155157783504876 ], [ 7.078979119990379, 36.042503159693069 ], [ 7.194889357058173, 36.080743719846794 ], [ 7.223776483198151, 36.144099026249023 ], [ 7.290800814905651, 36.143323879893103 ], [ 7.325889112792311, 36.127407538027455 ], [ 7.324958936805501, 36.080020250334258 ], [ 7.398339470789949, 35.967003893115134 ], [ 7.577243279953393, 35.833497830260626 ], [ 7.660132276683214, 35.834195462250761 ], [ 7.734598016285418, 35.934499416714687 ], [ 7.865856154437949, 35.858715929296693 ], [ 7.823378127298724, 35.739007473016443 ], [ 7.840741408189444, 35.615164903437517 ], [ 7.544842157239771, 35.447810777045675 ], [ 7.462263217973145, 35.539123032701752 ], [ 7.385161980940666, 35.571756700311425 ], [ 7.315037062010788, 35.535247300922151 ], [ 7.30335818892928, 35.600695502395524 ], [ 7.267546420630822, 35.621236883974973 ], [ 7.141197543931469, 35.617464504982877 ], [ 7.083009881400869, 35.576872667339728 ], [ 6.992576124888217, 35.631985581968991 ], [ 6.82498945539902, 35.623562323942053 ], [ 6.649496290921093, 35.796678372508836 ], [ 6.487025587560822, 35.781433824211604 ], [ 6.466820102765496, 35.846391100169114 ], [ 6.339437697291885, 35.88680206975971 ], [ 6.275513950108689, 35.866079820127595 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.83208784337927, 34.819244493338658 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ], [ 4.83208784337927, 34.819244493338658 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-07", "NAME_1": "Biskra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.434696078661091, 34.772658189523781 ], [ 4.932805209892535, 34.838907375774738 ], [ 5.028406610276818, 34.900195623729019 ], [ 5.038638543434047, 35.083311062355619 ], [ 5.272474399174143, 35.018431300763893 ], [ 5.536747673766115, 35.0663611917156 ], [ 5.613538853335399, 35.1064362663206 ], [ 5.554886101912075, 35.191392319699787 ], [ 5.830631544909863, 35.288440660008405 ], [ 6.005349562132551, 35.226273912011436 ], [ 5.932279087409938, 35.143565782434905 ], [ 5.927008090750803, 35.061684475158359 ], [ 5.993102248270134, 34.989363308369946 ], [ 6.072218866007859, 35.058583888835358 ], [ 6.166580031143496, 34.992076321065326 ], [ 6.279079624425094, 35.049256293243332 ], [ 6.429303013023627, 35.064785061481359 ], [ 6.459533726300435, 35.016984360839558 ], [ 6.439845005442635, 34.838261419728724 ], [ 6.540407342324954, 34.765681871421236 ], [ 6.714350213191778, 34.788522854546102 ], [ 6.749541863865886, 34.712222602291263 ], [ 6.695901726683303, 34.376093248004963 ], [ 6.558545769571595, 34.317027086330938 ], [ 6.211435174193923, 34.428415636472437 ], [ 5.998528272761575, 34.394955146562722 ], [ 5.477164748154564, 34.449835517195368 ], [ 5.311748488102239, 34.498256334562143 ], [ 5.29903608624636, 34.449060369940128 ], [ 5.217232293335655, 34.411956692247543 ], [ 5.185451287347007, 34.356662909565614 ], [ 5.359859246207236, 34.166209418529832 ], [ 5.37381188331176, 34.058386541805419 ], [ 5.334847852746179, 33.845996406109236 ], [ 5.121785922582262, 33.572679754966714 ], [ 4.406635775720474, 33.927645169389052 ], [ 4.335063917765581, 33.986039537494662 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.210627068984024, 34.174606839034368 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.316460401626216, 34.251785590432632 ], [ 4.184427118017084, 34.508204046879257 ], [ 4.273310580918576, 34.625845444711501 ], [ 4.434696078661091, 34.772658189523781 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "DZ-17", "NAME_1": "Djelfa" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 4.978280469667936, 32.840760606416836 ], [ 4.176210564665837, 33.029637966319058 ], [ 4.059060093248718, 33.252518419389503 ], [ 3.640997755741807, 33.564334011305561 ], [ 3.232133822617811, 33.947411403713318 ], [ 3.14924482588799, 34.074767970765208 ], [ 3.093537632056041, 34.240029202085964 ], [ 2.949412062416741, 34.263619493145086 ], [ 2.817637160326683, 34.138846747579294 ], [ 2.650205518669736, 34.09797068909603 ], [ 2.639198439156644, 33.911005357111549 ], [ 2.474867383822698, 33.92371775986669 ], [ 2.458279250187218, 34.003247788754436 ], [ 2.48339399643578, 34.113576971699842 ], [ 2.393735386279047, 34.225301419524897 ], [ 2.293483106859867, 34.474950263443873 ], [ 2.309451124669636, 34.563420315195344 ], [ 2.364383172145665, 34.639591376241015 ], [ 2.369964227167316, 34.776301378206028 ], [ 2.460294630892463, 34.870171616926541 ], [ 2.523443230820419, 34.999104316011369 ], [ 2.620284864654764, 35.031143704418412 ], [ 2.379317661181119, 35.205009060020075 ], [ 2.360042352372659, 35.287975572015 ], [ 2.286816847119781, 35.329265042547661 ], [ 2.285731642401345, 35.439077459756959 ], [ 2.527008905136825, 35.501192531809863 ], [ 2.619819776661359, 35.445692044452244 ], [ 2.687154167630695, 35.453701891329217 ], [ 2.893704867685472, 35.620823472825009 ], [ 2.88424808088422, 35.780503648224737 ], [ 2.960264113198264, 35.804739895329874 ], [ 3.081910434918598, 35.67187978852138 ], [ 3.196425408905498, 35.694462389227851 ], [ 3.255078159429502, 35.797401842021372 ], [ 3.305979444595664, 35.754587918097911 ], [ 3.379515008211058, 35.751203110934796 ], [ 3.449691603085, 35.659115708922798 ], [ 3.680426873401473, 35.495559800844092 ], [ 3.664872266741725, 35.294900214173538 ], [ 3.47356611498418, 35.157957669111113 ], [ 3.532838983132535, 35.054449775536682 ], [ 3.608079868191339, 35.0659736189873 ], [ 3.572113071161311, 35.001688137497524 ], [ 3.599243198115062, 34.886320501889656 ], [ 3.658826124625932, 34.805576077074249 ], [ 3.914417758772515, 34.758602200531129 ], [ 3.921084019411978, 34.672664293422372 ], [ 4.036374138855422, 34.441929023105899 ], [ 4.047691277630349, 34.354079088079459 ], [ 4.213727655307025, 34.232200222362337 ], [ 4.218068475080031, 34.166235256052232 ], [ 4.298477004010522, 34.135797838099734 ], [ 4.362400750294398, 33.957384955351415 ], [ 5.121785922582262, 33.572679754966714 ], [ 5.008046094951283, 33.441886704807587 ], [ 4.95311404837463, 33.24148550145469 ], [ 4.978280469667936, 32.840760606416836 ] ] ] } }
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
@@ -9,13 +9,13 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MC", "NAME_1": "Moyen-Chari" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.932091369000148, 9.063330566000076 ], [ 19.914473917000066, 9.069266663000064 ], [ 19.889462525000056, 9.046374004000128 ], [ 19.784662720000085, 9.048751120000091 ], [ 19.640072062000087, 9.013998718000053 ], [ 19.613613729000122, 9.031672059000044 ], [ 19.521319621000117, 9.008831075000046 ], [ 19.420963989000057, 9.017435202000073 ], [ 19.36939091000005, 9.00038197800005 ], [ 19.253428996000139, 9.027951356000131 ], [ 19.192037394000067, 9.020174052000115 ], [ 19.168783000000133, 9.002216492000045 ], [ 19.100570109000103, 9.015264791000092 ], [ 19.021918579000044, 8.985214946000028 ], [ 18.971895793000044, 8.938215230000054 ], [ 18.922906535000038, 8.91821645100012 ], [ 18.917738892000045, 8.898811951000141 ], [ 18.892727498000056, 8.897933452000103 ], [ 18.869783162000118, 8.849409282000124 ], [ 18.88652632700007, 8.835844218000133 ], [ 18.902959432000017, 8.844810079000069 ], [ 18.929107707000128, 8.796544292000064 ], [ 19.048686971000109, 8.745668844000136 ], [ 19.124134562000108, 8.675078837000058 ], [ 19.061296021000146, 8.625624492000028 ], [ 19.02047163900005, 8.545577698000073 ], [ 18.813042440000089, 8.276421001000045 ], [ 18.638582804000094, 8.177641500000092 ], [ 18.618635702000063, 8.138651632000105 ], [ 18.617912231000076, 8.090127462000027 ], [ 18.589283488000092, 8.047881979000053 ], [ 18.508254842000071, 8.030673727000064 ], [ 18.175430591551454, 8.021952410863994 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.014202986878331, 8.803703299867379 ], [ 17.937510613124289, 8.859727280789343 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.98389733129676, 9.016628322638951 ], [ 18.004323764230321, 8.999457098266475 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.141736600763522, 9.187974781510945 ], [ 17.995813352004575, 9.200135051791221 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.73436753243567, 9.431180195209834 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.561026646162759, 9.653622951837747 ], [ 17.543818394003665, 9.68395701700274 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.931288283027811, 10.382415676542223 ], [ 18.038826938912109, 10.344691881225344 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.600394728433969, 9.913090317763931 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.697701450261718, 9.866684882001664 ], [ 18.762710402163293, 9.884513250885789 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.998871697870527, 10.007038071749605 ], [ 19.049462924674231, 10.035666816370451 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.605914748067164, 10.359161282267337 ], [ 19.640951369110383, 10.225215968941825 ], [ 19.604519484086893, 10.10868561424968 ], [ 19.572066685429149, 10.075612697967642 ], [ 19.651028272636665, 9.92812816048621 ], [ 19.932091369000148, 9.063330566000076 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MA", "NAME_1": "Mandoul" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.175430591551454, 8.021952410863994 ], [ 18.07045210800004, 8.019201558000049 ], [ 17.858785441000123, 7.960445455000084 ], [ 17.639884074000122, 7.985043437000073 ], [ 17.580766235000084, 7.940601706000095 ], [ 17.503664998000119, 7.926235657000092 ], [ 17.466354614000096, 7.884119364000057 ], [ 17.41912235500007, 7.898227030000086 ], [ 17.385739380000075, 7.870476787000129 ], [ 17.233790205553532, 7.810801425045724 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.249469434866512, 8.290553697195378 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.197637973713483, 9.555386054023188 ], [ 17.234224888367862, 9.592024643722311 ], [ 17.455193311721928, 9.641892401912799 ], [ 17.579324080741742, 9.558863037199387 ], [ 17.624925096316133, 9.45246066932441 ], [ 17.713087058321094, 9.455500736669649 ], [ 17.795168885635576, 9.291337082040684 ], [ 17.995813352004575, 9.200135051791221 ], [ 18.141736600763522, 9.187974781510945 ], [ 18.165456576759595, 9.160101629286771 ], [ 18.091481966819288, 9.041205145700928 ], [ 18.004323764230321, 8.999457098266475 ], [ 17.98389733129676, 9.016628322638951 ], [ 17.967458529187752, 9.007391669190156 ], [ 17.961192253719503, 8.884466864054502 ], [ 17.937510613124289, 8.859727280789343 ], [ 18.014202986878331, 8.803703299867379 ], [ 18.012468759425701, 8.621609415542082 ], [ 18.031545262304007, 8.576519500874269 ], [ 18.088774768240967, 8.52102422238994 ], [ 18.090508995693597, 8.467263170458921 ], [ 18.128662000550889, 8.378817569475245 ], [ 18.126927773098259, 8.214065959676304 ], [ 18.175430591551454, 8.021952410863994 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-LR", "NAME_1": "Logone Oriental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.233790205553532, 7.810801425045724 ], [ 17.211073038000023, 7.768312480000077 ], [ 17.188645467000129, 7.764126689000051 ], [ 17.101932414000061, 7.677723694000065 ], [ 17.059351033000098, 7.696533916000078 ], [ 17.038990519000066, 7.662479147000028 ], [ 16.997546020000101, 7.66671661400008 ], [ 16.981939738000079, 7.648733215000092 ], [ 16.880653930000051, 7.632868551000087 ], [ 16.854815715000115, 7.611474508000072 ], [ 16.853885539000089, 7.567497864000117 ], [ 16.805516398000123, 7.543675029000056 ], [ 16.768619425000111, 7.55023793600013 ], [ 16.709811646000048, 7.627752584000092 ], [ 16.614210245000066, 7.679584046000073 ], [ 16.613073364000059, 7.748210347000025 ], [ 16.54930464600011, 7.794719137000058 ], [ 16.54868453000006, 7.870011699000116 ], [ 16.491737101000069, 7.84923777300007 ], [ 16.450912720000076, 7.792083638000051 ], [ 16.407401164000078, 7.796062724000123 ], [ 16.392208292000134, 7.783608703000127 ], [ 16.38714400200007, 7.69467356400007 ], [ 16.370814249000119, 7.672504374000056 ], [ 16.2827576090001, 7.660102031000079 ], [ 16.207206665000058, 7.613541565000034 ], [ 16.042978964000042, 7.583930970000083 ], [ 15.975696248000077, 7.515201315000112 ], [ 15.925260050000077, 7.488174541000063 ], [ 15.758345174000112, 7.455566711000102 ], [ 15.720001262000096, 7.468640849000067 ], [ 15.668531535000113, 7.516286519000104 ], [ 15.515569295000148, 7.512204081000093 ], [ 15.48104943900006, 7.523262838000093 ], [ 15.548952271000104, 7.630801493000121 ], [ 15.562904907000103, 7.792445374000039 ], [ 15.487870727000114, 7.804951071000048 ], [ 15.440741821000103, 7.83941925100001 ], [ 15.345347127000082, 8.135990296000088 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.519755893619561, 8.28362905503684 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.867951693715668, 8.413543606052542 ], [ 16.004119093321492, 8.511005357511237 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.513493687383743, 8.753161119390143 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.596175977639234, 8.90266103847614 ], [ 16.569820997940667, 8.945345771190375 ], [ 16.549305453883562, 8.952167060561408 ], [ 16.542484165411906, 8.980124009815199 ], [ 16.520780063848861, 8.980124009815199 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.435358920677629, 9.127608547296632 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.998993767748175, 9.037484849146438 ], [ 17.104827101289686, 9.069782619971932 ], [ 17.125911086127701, 8.995936997994022 ], [ 17.117384474413939, 8.783650214186025 ], [ 17.033461948010483, 8.632651679231572 ], [ 17.249469434866512, 8.284559231023707 ], [ 17.166735466868261, 8.158468735843485 ], [ 17.080177443034529, 8.136816311123823 ], [ 17.233790205553532, 7.810801425045724 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.560692179000085, 12.766224467000143 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ], [ 14.560692179000085, 12.766224467000143 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-HL", "NAME_1": "Hadjer-Lamis" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.575264933000142, 12.744907939000058 ], [ 14.54901330600012, 12.81821095800008 ], [ 14.49010217300011, 12.873608094000119 ], [ 14.507672159000094, 12.952440491000047 ], [ 14.481040720000067, 13.000507963000089 ], [ 15.01405032666122, 13.00334096901048 ], [ 15.043195835219592, 13.097960517463889 ], [ 15.002578159154723, 13.188239244345652 ], [ 14.996635369826436, 13.25965607356892 ], [ 15.17404056222216, 13.203742174162016 ], [ 15.185512729728657, 13.173769843303603 ], [ 15.269900343226254, 13.102042954818501 ], [ 15.286540154604438, 13.143694158758422 ], [ 15.327984652969349, 13.144159246751826 ], [ 15.383175082863772, 13.19878123586534 ], [ 15.372012973719734, 13.221828925464536 ], [ 15.305918817099723, 13.241052558328875 ], [ 15.21450320775682, 13.372414049268968 ], [ 15.356820103165205, 13.405125230345107 ], [ 15.441052687031856, 13.31867055929888 ], [ 15.743049757840026, 13.182141425386476 ], [ 15.847642857032213, 13.168757229062862 ], [ 15.883609653162921, 13.187877509139696 ], [ 15.965258416442737, 13.121990058094696 ], [ 15.986807489274213, 13.051503403958918 ], [ 15.999984979123496, 13.047575995335876 ], [ 16.246894971925371, 13.051968491952323 ], [ 16.324926384944717, 13.074447739871289 ], [ 16.447812941014377, 13.050986640021392 ], [ 16.489412469010233, 13.081630764448221 ], [ 16.719372592970785, 13.113050035230913 ], [ 16.818694696403099, 13.159507147836564 ], [ 16.880137973988269, 13.143177394820896 ], [ 16.974034051130559, 13.16291779252208 ], [ 16.997288446304765, 13.182089749442355 ], [ 17.059661898977424, 13.170565904193097 ], [ 17.131698845825042, 13.144004218020257 ], [ 17.072684360095138, 13.063957424295666 ], [ 16.969848260089123, 12.970578111090958 ], [ 16.941116163580091, 12.908411363093933 ], [ 17.038887974300565, 12.829914862980502 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.122203616603599, 11.564674405499375 ], [ 16.989452205454882, 11.687214169498304 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.060192328313292, 12.463299342090977 ], [ 15.978499152014251, 12.371394519541411 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.069662568272918, 12.259066401793007 ], [ 14.90486270493879, 12.248664494924668 ], [ 14.908267863000049, 12.326897278000075 ], [ 14.877468709000084, 12.447044983000126 ], [ 14.849563436000096, 12.457018534000042 ], [ 14.863826131000053, 12.495465800000076 ], [ 14.819177694000132, 12.638816223000035 ], [ 14.768844849000061, 12.633235168000112 ], [ 14.734118286000125, 12.680415751000041 ], [ 14.713654419000051, 12.652510478000053 ], [ 14.70207889800011, 12.668995260000131 ], [ 14.709933716000137, 12.718242900000106 ], [ 14.664458455000101, 12.715969136000098 ], [ 14.618363078000073, 12.759196472000042 ], [ 14.575264933000142, 12.744907939000058 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-CB", "NAME_1": "Chari-Baguirmi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.964801879000106, 12.092441305000079 ], [ 14.898035929000059, 12.152799377000079 ], [ 14.911575154000047, 12.1801362100001 ], [ 14.898035929000059, 12.207473043000036 ], [ 14.90486270493879, 12.248664494924668 ], [ 15.069662568272918, 12.259066401793007 ], [ 15.263683861421043, 12.299912990392158 ], [ 15.304530450020252, 12.340759578092047 ], [ 15.365800332019717, 12.330547930942259 ], [ 15.406646919719549, 12.279489696092583 ], [ 15.498551743168377, 12.269278048942795 ], [ 15.549609978018054, 12.310124637542003 ], [ 15.712996329716816, 12.350971225241835 ], [ 15.876382682314897, 12.340759578092047 ], [ 15.978499152014251, 12.371394519541411 ], [ 16.060192328313292, 12.463299342090977 ], [ 16.519716443758853, 12.289701343242371 ], [ 16.642256207757782, 12.228431461242906 ], [ 16.672891148307826, 12.116103344393821 ], [ 16.672891148307826, 12.003775226645359 ], [ 16.7750076189065, 11.881235462646487 ], [ 16.785219266056288, 11.738272405247301 ], [ 16.836277500905908, 11.697425816648092 ], [ 16.989452205454882, 11.687214169498304 ], [ 17.122203616603599, 11.564674405499375 ], [ 17.265166674902105, 11.493192876350122 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.329516228591046, 9.815835272779623 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.028966098606531, 9.938980211267733 ], [ 16.936930373437974, 9.938515123274328 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.129899530139198, 10.302627265236083 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.001225212573502, 10.789419256793281 ], [ 15.921798537372581, 10.775776678950535 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.432060988224009, 11.116272691431561 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.23129804876595, 11.184537258387763 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.021232544000043, 11.182548523000051 ], [ 15.033324829000037, 11.26026987700007 ], [ 15.063090454000104, 11.30993092900006 ], [ 15.049447876000102, 11.337267762000096 ], [ 15.060609985000042, 11.416125997000023 ], [ 15.135644165000116, 11.53082183800008 ], [ 15.069394979000037, 11.660788066000094 ], [ 15.076112915000039, 11.72145619700008 ], [ 15.097196899000068, 11.72765736900007 ], [ 15.083554321000065, 11.748172913000133 ], [ 15.110942831000102, 11.782899476000068 ], [ 15.079833618000066, 11.851215719000038 ], [ 15.044176880000066, 11.877312317000118 ], [ 15.042006469000086, 11.902375387000035 ], [ 15.063400512000101, 11.927283427000106 ], [ 15.048310995000094, 11.962785136000051 ], [ 15.081177205000103, 11.97167348300006 ], [ 15.049447876000102, 12.011929423000097 ], [ 15.040559529000092, 12.055647685000082 ], [ 15.104691864338065, 12.07118935600397 ], [ 15.125295796463831, 12.113568067412359 ], [ 15.1074616269525, 12.154100271010691 ], [ 15.050006169049425, 12.180917084955468 ], [ 15.021597762826502, 12.15508291524435 ], [ 15.011827432789801, 12.108616027907374 ], [ 14.964801879000106, 12.092441305000079 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-ND", "NAME_1": "Ville de N'Djamena" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.040559529000092, 12.055647685000082 ], [ 15.049447876000102, 12.084586487000138 ], [ 15.011827432789801, 12.108616027907374 ], [ 15.013491321567244, 12.134006169049428 ], [ 15.036189355654244, 12.17453837264776 ], [ 15.07432549192788, 12.179295797063389 ], [ 15.118810643996028, 12.134644813607224 ], [ 15.12252603384934, 12.09064481340738 ], [ 15.088478983618188, 12.061461626852577 ], [ 15.040559529000092, 12.055647685000082 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-ME", "NAME_1": "Mayo-Kebbi Est" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.382967570000119, 9.930196025000086 ], [ 15.681243937000147, 9.991277568000072 ], [ 15.476398560000092, 10.132741801000108 ], [ 15.439191528000094, 10.185245057000074 ], [ 15.301422160000072, 10.311748963000056 ], [ 15.278374471000092, 10.39427622500007 ], [ 15.241167440000083, 10.432904358000073 ], [ 15.218326457000074, 10.487216288000127 ], [ 15.138227986000118, 10.521658631000079 ], [ 15.132336873000014, 10.565402731000077 ], [ 15.149906860000101, 10.623125305000059 ], [ 15.065880981000078, 10.793114929000041 ], [ 15.079006795000026, 10.898147278000081 ], [ 15.035185181000116, 10.994627177000098 ], [ 15.028830280692887, 11.080218266259976 ], [ 15.150579460573624, 11.117926336930907 ], [ 15.236879102888281, 11.185777492737088 ], [ 15.342867466060682, 11.170377915708229 ], [ 15.421984083798407, 11.123972479945962 ], [ 15.438262159970691, 11.050281886699679 ], [ 15.513864780235394, 11.009974269896645 ], [ 15.548746371647724, 10.925948390705628 ], [ 15.591534458048784, 10.901298733349847 ], [ 15.665380080026637, 10.8888447130131 ], [ 15.745581903382117, 10.811226711143775 ], [ 15.921798537372581, 10.775776678950535 ], [ 16.001225212573502, 10.789419256793281 ], [ 16.063340284626406, 10.75123037438226 ], [ 16.07481245303228, 10.767301744080214 ], [ 16.13398196839313, 10.746476142559914 ], [ 16.108453810095227, 10.446029364463584 ], [ 16.126488884554362, 10.315029608729446 ], [ 16.177390170619844, 10.263043117945529 ], [ 16.196303745121725, 10.211935126305036 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.234647658062954, 10.023781235915294 ], [ 16.1584249201739, 9.855729478432693 ], [ 15.973940056888125, 9.683233547490204 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.29956135846993, 9.244814462817203 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.026452789764448, 9.641596272250126 ], [ 14.994766062403869, 9.671419074630592 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.77256555200006, 9.92174692800009 ], [ 14.898139282000074, 9.960478414000107 ], [ 15.033014770000136, 9.942856751000122 ], [ 15.109599244000037, 9.981536560000038 ], [ 15.214915812000072, 9.984094543000126 ], [ 15.382967570000119, 9.930196025000086 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-MO", "NAME_1": "Mayo-Kebbi Ouest" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 14.440492798000037, 9.995308329000068 ], [ 14.732464640000074, 9.923813985000052 ], [ 14.722846171713456, 9.787266672119188 ], [ 14.737544393558892, 9.665827298790816 ], [ 14.792935054808936, 9.662967116539164 ], [ 14.94071223348277, 9.695650101171225 ], [ 14.994766062403869, 9.671419074630592 ], [ 15.026452789764448, 9.641596272250126 ], [ 15.022724939804107, 9.591270293289369 ], [ 15.071186992885373, 9.527896838568211 ], [ 15.08963651271489, 9.394434942318696 ], [ 15.220104139594355, 9.292488794322367 ], [ 15.442189568593676, 9.18269562440355 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.41779829275697, 9.004308580076895 ], [ 15.37712894074798, 8.883282375980855 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.210706216000091, 8.421822448000057 ], [ 15.183703247000068, 8.479147644000122 ], [ 15.051928345000078, 8.643788758000056 ], [ 14.955086710000074, 8.676215719000069 ], [ 14.940100545000092, 8.729649149000082 ], [ 14.899379516000124, 8.774323425000119 ], [ 14.846049438000136, 8.810987854000089 ], [ 14.793856242000061, 8.813623353000096 ], [ 14.349542277000126, 9.168356221000082 ], [ 14.321326945000067, 9.243157858000103 ], [ 14.036486450000069, 9.568771057000077 ], [ 13.947602987000039, 9.637759094000032 ], [ 14.006720825000087, 9.739277446000102 ], [ 14.119788859000096, 9.85200958300004 ], [ 14.173532348000094, 9.975025329000061 ], [ 14.440492798000037, 9.995308329000068 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-X01~", "NAME_1": "Ennedi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 20.893328898000107, 21.020940247000013 ], [ 23.981305786000036, 19.496123759000014 ], [ 23.98440637200008, 15.721160381000018 ], [ 23.972624146000072, 15.691084697000051 ], [ 23.707524048000067, 15.748858948000063 ], [ 23.592699016000068, 15.749013977000104 ], [ 23.395811808000076, 15.688345846000018 ], [ 23.320570923000105, 15.681317851000088 ], [ 23.166781860000128, 15.712943827000103 ], [ 23.119249041000103, 15.707223545000105 ], [ 23.066220330603755, 15.788442288081114 ], [ 22.997645705285038, 15.816502590122411 ], [ 22.957183057951738, 15.820946764481562 ], [ 22.875534294671922, 15.797485662833083 ], [ 22.75931399924167, 15.838413398159787 ], [ 22.56082482290725, 15.80957794796393 ], [ 22.470701124757113, 15.908900051396245 ], [ 22.370913934230657, 15.948432521843415 ], [ 22.311796094813928, 15.927606920323115 ], [ 22.270971714073312, 15.931430976158595 ], [ 22.1771273128752, 15.999798895902302 ], [ 22.144209426224052, 15.978921617538617 ], [ 22.018222283831335, 15.986001288428724 ], [ 21.915954623706909, 15.944453437276309 ], [ 21.731779818783593, 15.999850571846423 ], [ 21.388803337603917, 15.923782864487578 ], [ 21.312632276558304, 15.887919420245055 ], [ 21.096624789702275, 15.985174465229363 ], [ 20.9587003918096, 15.918718573403453 ], [ 20.896326939136941, 15.839446926034782 ], [ 20.832868279947149, 15.799449368493526 ], [ 20.660268996217212, 15.737024238078106 ], [ 20.567199741374964, 15.685451158444209 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.893328898000107, 21.020940247000013 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 19.185837029855008, 21.864177058924099 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ], [ 19.185837029855008, 21.864177058924099 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BO", "NAME_1": "Borkou" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 18.917118774000073, 21.996849671000078 ], [ 20.893328898000107, 21.020940247000013 ], [ 20.004278032405239, 19.279519401099492 ], [ 20.152564183290394, 18.686374798458189 ], [ 20.4244221259803, 17.969658404012137 ], [ 20.4244221259803, 17.623657385879653 ], [ 20.399707767799214, 17.178798933224186 ], [ 20.30085033327623, 16.758654839649125 ], [ 20.637016046253507, 15.843719203078763 ], [ 20.483535597389903, 15.711599433467086 ], [ 20.291764356739634, 15.669018053540299 ], [ 20.11286054937483, 15.764102688188473 ], [ 19.863780145337444, 15.798674221238286 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.046982455975524, 15.614654445945916 ], [ 18.935413038680792, 15.521998603153008 ], [ 18.791235793097371, 15.4505300970863 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.270647413946961, 15.431461492953531 ], [ 18.084922316311861, 15.53166209642859 ], [ 17.84281823127634, 15.792524726335046 ], [ 15.468517057815973, 16.904909712513188 ], [ 15.490247843000077, 17.124537252000025 ], [ 17.359841679150179, 20.416379890019869 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.952986281791482, 20.83652398359493 ], [ 18.150701149038809, 20.935381417218593 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.917118774000073, 21.996849671000078 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-TI", "NAME_1": "Tibesti" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 15.985101359000112, 23.444719951000067 ], [ 18.917118774000073, 21.996849671000078 ], [ 18.719131393498969, 21.849812679811293 ], [ 18.768560109861141, 21.355525510793655 ], [ 18.743845751680055, 21.133096284465921 ], [ 18.150701149038809, 20.935381417218593 ], [ 17.952986281791482, 20.83652398359493 ], [ 17.829414489087412, 20.71295219089086 ], [ 17.705842697282662, 20.688237832709774 ], [ 17.409270395512351, 20.688237832709774 ], [ 17.359841679150179, 20.416379890019869 ], [ 15.490247843000077, 17.124537252000025 ], [ 15.736020955000129, 19.903540751000023 ], [ 15.970321899000112, 20.336330871000101 ], [ 15.953992147000065, 20.374571432 ], [ 15.669461711000054, 20.671865946000068 ], [ 15.570242961000105, 20.751912740000122 ], [ 15.544198039000037, 20.798989970000022 ], [ 15.544301391000147, 20.890302226000117 ], [ 15.569002727000054, 20.928904521 ], [ 15.609310343000118, 20.950660299000035 ], [ 15.301008748000044, 21.401330465000072 ], [ 15.266592245000083, 21.440656230000045 ], [ 15.184530070000108, 21.491195780000069 ], [ 15.172024373000113, 21.993387350000134 ], [ 14.979909027000133, 22.995663760000085 ], [ 15.985101359000112, 23.444719951000067 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-KA", "NAME_1": "Kanem" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 13.608052034873481, 14.518266228003995 ], [ 13.665604695000098, 14.566889751000105 ], [ 13.648344767000111, 14.649417013000033 ], [ 13.709994751000067, 14.705124207000068 ], [ 13.764358358000038, 14.719076843000053 ], [ 13.772006469000104, 14.762872620000053 ], [ 13.755159952000042, 14.847208558000062 ], [ 13.833914836000133, 15.019601136000105 ], [ 14.368972615000104, 15.749634095000076 ], [ 15.468517057815973, 16.904909712513188 ], [ 16.671386334311876, 16.341868536703601 ], [ 16.489081502309489, 16.037375794157583 ], [ 16.131673857462545, 14.811978153269138 ], [ 16.111250563162969, 14.577110272421123 ], [ 16.049980681163504, 14.474993802721826 ], [ 15.774266211716281, 14.148221098424926 ], [ 15.590456565717943, 14.076739569275674 ], [ 15.6007221766871, 13.753926464834478 ], [ 15.71617801330467, 13.754923000895758 ], [ 15.758242629293875, 13.790166327513987 ], [ 15.841751742748727, 13.765775050777961 ], [ 15.85740970219598, 13.74510447798923 ], [ 15.865006137922933, 13.700921129406595 ], [ 15.840201450036886, 13.642630113189171 ], [ 15.8581848485519, 13.581238512447385 ], [ 15.898389112567486, 13.521500556305625 ], [ 15.899939406178646, 13.250457669186062 ], [ 15.871672397663019, 13.171496080179963 ], [ 15.776226026909626, 13.171857815385863 ], [ 15.560136878913795, 13.264834071335315 ], [ 15.375818277791268, 13.751985246721745 ], [ 15.213262974306815, 13.750582180223432 ], [ 14.915813428846661, 13.678751938950825 ], [ 14.839642367800991, 13.685883287583636 ], [ 14.805174188438059, 13.663817449915371 ], [ 14.643788689796168, 13.908918769385707 ], [ 14.477442253756976, 14.104514064972079 ], [ 14.356571079291882, 14.134951483823897 ], [ 14.30737511556913, 14.198823554163653 ], [ 14.261744826162783, 14.213189602418197 ], [ 14.149400261612755, 14.308480943540701 ], [ 14.131881951990465, 14.378347480052128 ], [ 14.087336867302554, 14.416691392094037 ], [ 14.017366978003622, 14.428060207712406 ], [ 13.995817905172146, 14.452348130761607 ], [ 13.950497674128371, 14.466145738235241 ], [ 13.78492638534442, 14.476377672291733 ], [ 13.735575391990722, 14.515703437163893 ], [ 13.608052034873481, 14.518266228003995 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-OD", "NAME_1": "Ouaddaï" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 22.180762493000145, 13.920554946000053 ], [ 22.073721964000129, 13.771356913000147 ], [ 22.112892700000089, 13.729834900000071 ], [ 22.132116333000113, 13.63865183500009 ], [ 22.195885050000129, 13.580464173000081 ], [ 22.215625448000083, 13.464993184000022 ], [ 22.275983521000086, 13.376316427000106 ], [ 22.267611939000119, 13.334561869000069 ], [ 22.139971151000054, 13.193511048000047 ], [ 22.016051066000074, 13.140232646000072 ], [ 21.964064575000037, 13.098348898000083 ], [ 21.852960246000066, 12.90572499600006 ], [ 21.809448689905082, 12.793664652701523 ], [ 21.574481710962175, 12.881976869836649 ], [ 21.482576888412666, 12.851341928387285 ], [ 21.380460417813993, 12.79007204638782 ], [ 21.288555595264484, 12.698167222938991 ], [ 21.237497360414807, 12.677743929538678 ], [ 21.094534302116358, 12.698167222938991 ], [ 20.982206185267216, 12.667532282388891 ], [ 20.849454774118499, 12.718590517238567 ], [ 20.839243126968711, 12.881976869836649 ], [ 20.686068421520417, 12.91261181038675 ], [ 20.675856774370629, 13.045363221535411 ], [ 20.410353952972514, 13.045363221535411 ], [ 20.294554884700176, 13.092327786498117 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.000050896831397, 14.155983791818528 ], [ 20.288043653691659, 14.441392727192579 ], [ 20.434959751291444, 14.437878729719614 ], [ 20.492113885047729, 14.464130357529939 ], [ 20.551541781927654, 14.470228176489115 ], [ 20.7355615581194, 14.411678778752616 ], [ 20.844185417822757, 14.355041408933857 ], [ 20.89896243656716, 14.264762682052094 ], [ 20.958080275084626, 14.204146225867589 ], [ 21.000299919805457, 14.196859849402529 ], [ 21.168196648556489, 14.101465155492519 ], [ 21.232275425370574, 14.093403631772162 ], [ 21.333096143772025, 14.142651272338355 ], [ 21.421721226053705, 14.151901353564597 ], [ 21.702272576817961, 14.110870266349707 ], [ 22.042251825361461, 13.999455877786488 ], [ 22.084626498813861, 13.973462633293877 ], [ 22.124520705366251, 13.920494290579029 ], [ 22.180762493000145, 13.920554946000053 ] ] ] } },
|
||||
@@ -24,6 +24,6 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-TA", "NAME_1": "Tandjilé" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.435358920677629, 9.127608547296632 ], [ 16.420527785329, 9.180835273329194 ], [ 16.357430861345165, 9.238712877497278 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.042359652575897, 9.117118231721008 ], [ 16.001431919047832, 9.113965969453886 ], [ 15.818135614167261, 9.025082506552394 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.489008416405284, 9.007719223863091 ], [ 15.452421501750905, 9.049990546326626 ], [ 15.438262159970691, 9.186829739500865 ], [ 15.523941684660997, 9.325839342111919 ], [ 15.67416507325953, 9.495906480299823 ], [ 15.71633304203624, 9.604065252909095 ], [ 15.830692987291513, 9.741317857233298 ], [ 15.912186720041063, 9.731189276863631 ], [ 15.973940056888125, 9.683233547490204 ], [ 16.166486443894257, 9.866788234789169 ], [ 16.211496615676253, 9.955206611495896 ], [ 16.243329299407606, 10.071581936557095 ], [ 16.246274855200397, 10.149716702363889 ], [ 16.219403109765665, 10.195863755707762 ], [ 16.4402165061868, 10.192866523071586 ], [ 16.500729607785161, 10.204131985003073 ], [ 16.568115675597937, 10.25167430322648 ], [ 16.605012647715512, 10.21312368381092 ], [ 16.607803175675997, 10.145427558535005 ], [ 16.632969597868623, 10.095611477187902 ], [ 16.73988813612857, 10.026468411088274 ], [ 16.879827914726491, 10.000113430490387 ], [ 16.936930373437974, 9.938515123274328 ], [ 17.028966098606531, 9.938980211267733 ], [ 17.130148553113202, 9.916966051342172 ], [ 17.156193475348573, 9.874332994572001 ], [ 17.227558627728456, 9.831854967432776 ], [ 17.347499627106117, 9.813974920805947 ], [ 17.427288039311577, 9.774804185564676 ], [ 17.558391147833163, 9.663183092325824 ], [ 17.536532016639228, 9.587063707224274 ], [ 17.443721144215431, 9.643752752987155 ], [ 17.306881951940511, 9.600137844286053 ], [ 17.254585401894758, 9.601481432322203 ], [ 17.204459263084516, 9.568201809565835 ], [ 17.197637973713483, 9.549701646213975 ], [ 17.218256869658774, 9.507326971862256 ], [ 17.210040317206847, 9.3859907094037 ], [ 17.06322757329383, 9.263362534853115 ], [ 17.185649041370141, 9.156547349380673 ], [ 17.104827101289686, 9.069782619971932 ], [ 16.998993767748175, 9.037484849146438 ], [ 16.91910200365453, 9.036141262009608 ], [ 16.568735793222288, 9.136083482166953 ], [ 16.435358920677629, 9.127608547296632 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-LO", "NAME_1": "Logone Occidental" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 16.357430861345165, 9.238712877497278 ], [ 16.420527785329, 9.180835273329194 ], [ 16.489722528272068, 9.021465155392605 ], [ 16.592455274591259, 8.917492173824769 ], [ 16.5971061536261, 8.881111964745344 ], [ 16.490497673728669, 8.732438869758028 ], [ 16.409572380860652, 8.706548977153545 ], [ 16.365027297072061, 8.648568020197956 ], [ 16.145195754380552, 8.597511705400848 ], [ 16.004119093321492, 8.511005357511237 ], [ 15.867951693715668, 8.413543606052542 ], [ 15.85740970219598, 8.366466375822597 ], [ 15.813639763863989, 8.315823473074829 ], [ 15.728166944748693, 8.322696438389926 ], [ 15.664708285558902, 8.276394355415164 ], [ 15.614065382811134, 8.26202830716062 ], [ 15.570812209315989, 8.258152574481699 ], [ 15.558048129717406, 8.278358059277025 ], [ 15.524251743023513, 8.279908351988865 ], [ 15.520065951982076, 8.317012030580713 ], [ 15.481722039040847, 8.347914537425936 ], [ 15.431440870599658, 8.355149237047613 ], [ 15.273207635124265, 8.448631903939201 ], [ 15.264939405828898, 8.502117011490895 ], [ 15.305918817099723, 8.671770738528721 ], [ 15.37542361840525, 8.746236477231605 ], [ 15.372943148807281, 8.86426544779215 ], [ 15.428185256444408, 9.013455308515688 ], [ 15.463893670156722, 9.02673615205174 ], [ 15.518515659270236, 8.994851793275643 ], [ 15.569727003698233, 8.991389471746686 ], [ 15.7552970717025, 9.032213853386622 ], [ 15.818135614167261, 9.025082506552394 ], [ 16.022050815892442, 9.117273261351897 ], [ 16.146952751768083, 9.088541164842866 ], [ 16.204210239211136, 9.155927231756323 ], [ 16.304307488999427, 9.192410792723877 ], [ 16.357430861345165, 9.238712877497278 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-GR", "NAME_1": "Guéra" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.265166674902105, 11.493192876350122 ], [ 17.459187968050287, 11.666790876098048 ], [ 17.625570510070986, 11.715099189176215 ], [ 17.517101678199879, 11.93281199837503 ], [ 17.567382846641067, 12.129079088429194 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.986685417597926, 12.482442532396931 ], [ 18.075258823036222, 12.569672348899758 ], [ 18.435288526744102, 12.763769029517107 ], [ 19.112611524602187, 12.751728420330323 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.990284050768309, 12.970578111090958 ], [ 20.136425002012231, 12.860455633720505 ], [ 20.170738152643594, 12.854409490705393 ], [ 20.169497918294269, 12.787850246991297 ], [ 20.122472364907708, 12.767903143715102 ], [ 20.048626742929798, 12.668477688394603 ], [ 20.094050326761135, 12.594838771991704 ], [ 20.043355747169983, 12.558871974961676 ], [ 19.926308627641049, 12.423117988304512 ], [ 19.885535922844554, 12.343381252043116 ], [ 19.821457146929788, 12.163185533485546 ], [ 19.820010207005453, 12.087014472439932 ], [ 19.642036573828818, 12.097659816747125 ], [ 19.3895972019493, 12.158948066500045 ], [ 19.219220005398938, 11.999371242988445 ], [ 19.293685744101765, 11.87286733575894 ], [ 19.190797967252365, 11.673654689911984 ], [ 19.061400181073509, 11.61572540890046 ], [ 19.012204217350757, 11.703213608721057 ], [ 18.886527134219875, 11.706365871887442 ], [ 18.867613559717995, 11.692361557939535 ], [ 18.84632287020429, 11.389485988887259 ], [ 18.860430536040383, 11.345354316248688 ], [ 19.136020949407282, 11.184175523181864 ], [ 19.255496859892162, 11.142575995186007 ], [ 19.315441521608932, 11.103611965519804 ], [ 19.360089959084348, 11.102578436745432 ], [ 19.392697788272301, 11.064234523804203 ], [ 19.460807325597557, 11.062580878304857 ], [ 19.628549024717699, 10.991680813019116 ], [ 19.6929378598943, 10.917731838253701 ], [ 19.666892937658929, 10.896027736690655 ], [ 19.677124871715421, 10.705083320139124 ], [ 19.658366326844487, 10.618473619461952 ], [ 19.61428633014998, 10.595374253918692 ], [ 19.580128208250187, 10.541165675955142 ], [ 19.557803989062847, 10.552741197148521 ], [ 19.5337227715886, 10.495380356917906 ], [ 19.464372999914019, 10.482357895800249 ], [ 19.442203810357569, 10.502046617557369 ], [ 19.378590122436208, 10.469128730006844 ], [ 19.336835564809462, 10.419622707921633 ], [ 19.316371697595741, 10.357404283081166 ], [ 19.281593458970917, 10.34923940747268 ], [ 19.302677442909669, 10.299940090063103 ], [ 19.19358849521285, 10.202995103441253 ], [ 19.164132928291963, 10.137521064445536 ], [ 19.111991407877156, 10.120312812286443 ], [ 19.049462924674231, 10.035666816370451 ], [ 18.998871697870527, 10.007038071749605 ], [ 18.795266553608542, 9.994842433831252 ], [ 18.756974318410073, 9.881774399768744 ], [ 18.69020836822159, 9.867304998726695 ], [ 18.653466423936322, 9.900171210333099 ], [ 18.602410109139214, 9.9109715851705 ], [ 18.607060988174055, 10.020784003279118 ], [ 18.500762566639139, 10.10791046789376 ], [ 18.453116895628227, 10.179120592441393 ], [ 18.381596713617398, 10.204803779470808 ], [ 18.349919061315632, 10.236378078985126 ], [ 18.264756300562794, 10.247798570547559 ], [ 18.218505894431473, 10.288571275344054 ], [ 18.156752556685149, 10.311257228838031 ], [ 18.145125360447025, 10.341487942114838 ], [ 18.038826938912109, 10.344691881225344 ], [ 17.931288283027811, 10.382415676542223 ], [ 17.827160271829086, 10.47388296092987 ], [ 17.533741488678743, 10.463702703716763 ], [ 17.295801616351525, 10.584356293508108 ], [ 17.316224909751782, 10.74774264520687 ], [ 17.254955027752317, 11.237901701202532 ], [ 17.265166674902105, 11.493192876350122 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.131698845825042, 13.144004218020257 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ], [ 17.131698845825042, 13.144004218020257 ] ] ] } }
|
||||
{ "type": "Feature", "properties": { "ISO": "TD-BA", "NAME_1": "Batha" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 17.072684360095138, 13.063957424295666 ], [ 17.202133823117435, 13.250974433123588 ], [ 17.197017856988509, 13.500623277042564 ], [ 17.238152296990904, 13.720609849365019 ], [ 17.551414828831298, 13.820086982428279 ], [ 17.992886590243927, 15.616411445132087 ], [ 18.084922316311861, 15.53166209642859 ], [ 18.209049106730902, 15.457248032770508 ], [ 18.479988641062903, 15.383867498786003 ], [ 18.592178175982042, 15.390482083481345 ], [ 18.892418246704722, 15.495075181774212 ], [ 19.046982455975524, 15.614654445945916 ], [ 19.130439894385574, 15.73071971354392 ], [ 19.207437777731229, 15.940060939760542 ], [ 19.220770298110779, 16.181803290489427 ], [ 19.260044386139498, 16.161132716801433 ], [ 19.342416619831113, 16.15281281246132 ], [ 19.369391717154031, 16.1334858259101 ], [ 19.409751010800505, 15.982590643743208 ], [ 19.555013462001682, 15.886369127533214 ], [ 19.863780145337444, 15.798674221238286 ], [ 20.000050896831397, 15.783843084990338 ], [ 20.000050896831397, 14.010617987829846 ], [ 20.238537631606391, 13.886232814992411 ], [ 20.231716343134735, 13.669760240142978 ], [ 20.283237745925192, 13.563151760245546 ], [ 20.281532423582462, 13.402748114433905 ], [ 20.304786817857348, 13.22322418944475 ], [ 20.304321729863943, 13.118321030990785 ], [ 20.260396762800326, 13.043545233925386 ], [ 19.987648553338033, 12.986029364063882 ], [ 19.990284050768309, 12.970578111090958 ], [ 19.705750309529151, 13.055574868685255 ], [ 19.470882427781817, 12.953458398086582 ], [ 19.096178419698276, 12.748679510850707 ], [ 18.435288526744102, 12.763769029517107 ], [ 18.075258823036222, 12.569672348899758 ], [ 17.986685417597926, 12.482442532396931 ], [ 17.825920038379024, 12.45717275651748 ], [ 17.564747349210791, 12.236256008208215 ], [ 17.545368686715506, 12.208867498836014 ], [ 17.560096470175949, 12.155640773702714 ], [ 17.476173943772494, 12.188920396459139 ], [ 17.412405226220187, 12.187576809322252 ], [ 17.384448276067076, 12.209074205310287 ], [ 17.380572544287475, 12.256151435540289 ], [ 17.347706332681071, 12.309791570924233 ], [ 17.289466994206407, 12.354698390818044 ], [ 17.257530958586869, 12.515515447779705 ], [ 17.197172885720079, 12.538459783692076 ], [ 17.156038445717684, 12.604192206005507 ], [ 17.125290969402727, 12.623105780507331 ], [ 17.038887974300565, 12.829914862980502 ], [ 16.941116163580091, 12.908411363093933 ], [ 16.969848260089123, 12.970578111090958 ], [ 17.072684360095138, 13.063957424295666 ] ] ] } }
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-3
@@ -8,11 +8,11 @@
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X11~", "NAME_1": "Penrhyn" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -157.940744594999927, -8.980726820999905 ], [ -157.978627081999917, -8.976006768999923 ], [ -158.008290167999917, -8.952894789999959 ], [ -158.002878383999928, -8.946709893999923 ], [ -157.940744594999927, -8.980726820999905 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X04~", "NAME_1": "Rarotonga" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -159.797840949999909, -21.186130466999941 ], [ -159.742746548999918, -21.201592705999929 ], [ -159.743234829999921, -21.254327080999929 ], [ -159.846262173999918, -21.233819268999923 ], [ -159.831410285999908, -21.195570570999905 ], [ -159.797840949999909, -21.186130466999941 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X06~", "NAME_1": "Mauke" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -157.312814907999922, -20.155205987999921 ], [ -157.317982550999915, -20.171807549999926 ], [ -157.340321417999917, -20.175713799999926 ], [ -157.348988410999908, -20.143324476999908 ], [ -157.312814907999922, -20.155205987999921 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X01~", "NAME_1": "Atiu" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -158.078317837999919, -19.994235934999949 ], [ -158.081776495999918, -20.012790622999944 ], [ -158.115101691999911, -20.015720309999949 ], [ -158.135202602999925, -19.976495049999926 ], [ -158.078317837999919, -19.994235934999949 ] ] ], [ [ [ -158.268299933999913, -19.832614841999941 ], [ -158.281076626999919, -19.82195403399993 ], [ -158.276356574999909, -19.82350025799991 ], [ -158.268299933999913, -19.832614841999941 ] ] ], [ [ [ -158.287668423999918, -19.810642184999949 ], [ -158.293771938999924, -19.816582940999922 ], [ -158.297352667999917, -19.817315362999921 ], [ -158.295969204999921, -19.814548434999949 ], [ -158.287668423999918, -19.810642184999949 ] ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X02~", "NAME_1": "Aitutaki" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -158.925160285999908, -19.26921965899993 ], [ -158.950184699999909, -19.254489841999941 ], [ -158.955555792999917, -19.245293877999927 ], [ -158.918080206999917, -19.261163018999923 ], [ -158.925160285999908, -19.26921965899993 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X01~", "NAME_1": "Atiu" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -158.078317837999919, -19.994235934999949 ], [ -158.081776495999918, -20.012790622999944 ], [ -158.115101691999911, -20.015720309999949 ], [ -158.135202602999925, -19.976495049999926 ], [ -158.078317837999919, -19.994235934999949 ] ] ], [ [ [ -158.268299933999913, -19.832614841999941 ], [ -158.281076626999919, -19.82195403399993 ], [ -158.276356574999909, -19.82350025799991 ], [ -158.268299933999913, -19.832614841999941 ] ] ], [ [ [ -158.295969204999921, -19.814548434999949 ], [ -158.293771938999924, -19.816582940999922 ], [ -158.297352667999917, -19.817315362999921 ], [ -158.295969204999921, -19.814548434999949 ] ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X02~", "NAME_1": "Aitutaki" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -158.918080206999917, -19.261163018999923 ], [ -158.950184699999909, -19.254489841999941 ], [ -158.955555792999917, -19.245293877999927 ], [ -158.918080206999917, -19.261163018999923 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X08~", "NAME_1": "Pukapuka" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -165.824533657999922, -10.881768487999921 ], [ -165.811756964999915, -10.88014088299991 ], [ -165.807687954999921, -10.881280205999929 ], [ -165.818714972999913, -10.891534112999921 ], [ -165.824533657999922, -10.881768487999921 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X10~", "NAME_1": "Manihiki" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -160.986236131999902, -10.38209400799991 ], [ -160.996408657999922, -10.371514580999929 ], [ -160.93809973899991, -10.422784112999921 ], [ -160.986236131999902, -10.38209400799991 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X09~", "NAME_1": "Rakahanga" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -161.088002081999917, -10.035088799999926 ], [ -161.070057745999918, -10.027439059999949 ], [ -161.087269660999908, -10.04461028399993 ], [ -161.088002081999917, -10.035088799999926 ] ] ] } },
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X07~", "NAME_1": "Palmerston" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -159.785104946999923, -18.830824476999908 ], [ -159.772613084999904, -18.828952731999948 ], [ -159.761992967999902, -18.83725351399994 ], [ -159.761382615999906, -18.84539153399993 ], [ -159.769683397999927, -18.834323825999945 ], [ -159.776478644999912, -18.87818775799991 ], [ -159.793609178999901, -18.887465101999908 ], [ -159.802316860999923, -18.865817966999941 ], [ -159.785104946999923, -18.830824476999908 ] ] ] } }
|
||||
{ "type": "Feature", "properties": { "ISO": "CK-X07~", "NAME_1": "Palmerston" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -159.802316860999923, -18.865817966999941 ], [ -159.772613084999904, -18.828952731999948 ], [ -159.761992967999902, -18.83725351399994 ], [ -159.761382615999906, -18.84539153399993 ], [ -159.769683397999927, -18.834323825999945 ], [ -159.776478644999912, -18.87818775799991 ], [ -159.793609178999901, -18.887465101999908 ], [ -159.802316860999923, -18.865817966999941 ] ] ] } }
|
||||
]
|
||||
}
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+25
-25
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user