mirror of
https://github.com/apache/superset.git
synced 2026-06-13 19:49:18 +00:00
Compare commits
1 Commits
fix/chart-
...
showtime-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a605395147 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -41,8 +41,8 @@ body:
|
||||
label: Superset version
|
||||
options:
|
||||
- master / latest-dev
|
||||
- "6.1.0"
|
||||
- "6.0.0"
|
||||
- "5.0.0"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
28
.github/dependabot.yml
vendored
28
.github/dependabot.yml
vendored
@@ -14,6 +14,12 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
- dependency-name: "storybook"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "eslint-plugin-storybook"
|
||||
- dependency-name: "react-error-boundary"
|
||||
- dependency-name: "@rjsf/*"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
@@ -36,6 +42,14 @@ updates:
|
||||
# and confirm the issue https://github.com/apache/superset/issues/39600 is fixed
|
||||
- dependency-name: "react-checkbox-tree"
|
||||
update-types: ["version-update:semver-major"]
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
- "storybook"
|
||||
update-types:
|
||||
- "patch"
|
||||
directory: "/superset-frontend/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -76,7 +90,21 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0 in superset-frontend
|
||||
- dependency-name: "storybook"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "eslint-plugin-storybook"
|
||||
- dependency-name: "react-error-boundary"
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
- "storybook"
|
||||
update-types:
|
||||
- "patch"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -74,6 +74,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: "superset-frontend/.nvmrc"
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ pre-commit run eslint # Frontend linting
|
||||
## Platform-Specific Instructions
|
||||
|
||||
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
|
||||
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
|
||||
- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
|
||||
- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
|
||||
|
||||
@@ -29,7 +29,7 @@ ARG BUILD_TRANSLATIONS="false"
|
||||
######################################################################
|
||||
# superset-node-ci used as a base for building frontend assets and CI
|
||||
######################################################################
|
||||
FROM --platform=${BUILDPLATFORM} node:24-trixie-slim AS superset-node-ci
|
||||
FROM --platform=${BUILDPLATFORM} node:22-trixie-slim AS superset-node-ci
|
||||
ARG BUILD_TRANSLATIONS
|
||||
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
|
||||
ARG DEV_MODE="false" # Skip frontend build in dev mode
|
||||
|
||||
37
UPDATING.md
37
UPDATING.md
@@ -24,27 +24,6 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Map chart renderer and OpenStreetMap migration behavior
|
||||
|
||||
The MapLibre migration for deck.gl charts preserves saved non-Mapbox styles on
|
||||
the MapLibre-compatible path. Saved styles such as OpenStreetMap, `tile://`
|
||||
tile templates, generic HTTPS style URLs, and charts without a saved style are
|
||||
not reclassified as Mapbox during migration and do not require
|
||||
`MAPBOX_API_KEY` only because of the migration.
|
||||
|
||||
Saved true Mapbox styles whose value starts with `mapbox://` remain
|
||||
Mapbox-backed. If a Superset deployment does not configure `MAPBOX_API_KEY`,
|
||||
those saved Mapbox charts keep the existing missing-key message instead of
|
||||
silently falling back to MapLibre or another provider. In Explore, deck.gl and
|
||||
point-cluster renderer controls preserve saved Mapbox state, but the Mapbox
|
||||
choice is not available as a new working renderer without a configured key.
|
||||
|
||||
The MapLibre style choices include `Streets (OSM)`, backed by
|
||||
`https://tile.openstreetmap.org/{z}/{x}/{y}.png`. This OpenStreetMap tile
|
||||
service requires visible `© OpenStreetMap contributors` attribution and should
|
||||
be used through normal browser map tile requests and caching; it is not intended
|
||||
for bulk prefetch or offline tile downloads.
|
||||
|
||||
### Duration formatter precision
|
||||
|
||||
The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`.
|
||||
@@ -79,22 +58,6 @@ GLOBAL_ASYNC_QUERIES_JWT_SECRET = "<output of: openssl rand -base64 42>"
|
||||
|
||||
The check is only active when the relevant feature is enabled, so deployments that do not use global async queries (or embedding) are not affected.
|
||||
|
||||
### Guest token revocation (opt-in)
|
||||
|
||||
Embedded guest tokens can be coarsely revoked at runtime via a new opt-in mechanism. A new config flag `GUEST_TOKEN_REVOCATION_ENABLED` (default `False`) gates the feature. When enabled, every minted guest token carries a revocation version, and tokens whose version is below the current expected version (stored in the metadata database) are rejected at validation time.
|
||||
|
||||
Bump the expected version with the new CLI command to invalidate all outstanding guest tokens:
|
||||
|
||||
```bash
|
||||
superset revoke-guest-tokens
|
||||
```
|
||||
|
||||
This change is backward compatible. The feature is off by default, and even when enabled nothing is revoked until an admin explicitly bumps the version: the expected version starts at `0`, and tokens minted before this change (which carry no version claim) are treated as version `0`. No database migration is required.
|
||||
|
||||
### Sessions are terminated when an account is disabled
|
||||
|
||||
Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature.
|
||||
|
||||
### Dataset import validates catalog against the target connection
|
||||
|
||||
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
|
||||
|
||||
@@ -1 +1 @@
|
||||
v24.16.0
|
||||
v22.22.0
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.60.1",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
133
docs/yarn.lock
133
docs/yarn.lock
@@ -4812,21 +4812,32 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
|
||||
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
|
||||
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
|
||||
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/type-utils" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/type-utils" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1":
|
||||
"@typescript-eslint/parser@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
|
||||
integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/parser@^8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
|
||||
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
|
||||
@@ -4837,6 +4848,15 @@
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
|
||||
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.0"
|
||||
"@typescript-eslint/types" "^8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
|
||||
@@ -4846,6 +4866,14 @@
|
||||
"@typescript-eslint/types" "^8.60.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
|
||||
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
|
||||
@@ -4854,7 +4882,12 @@
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.60.1":
|
||||
"@typescript-eslint/tsconfig-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz#3af78c48956227a407dea9626b8db8ca53f130d2"
|
||||
integrity sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.60.1", "@typescript-eslint/tsconfig-utils@^8.60.0":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
|
||||
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
|
||||
@@ -4864,27 +4897,47 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
|
||||
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
|
||||
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
|
||||
"@typescript-eslint/type-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
|
||||
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.0.tgz#e77ad768e933263b1960b2fe79de75fe1cc6e7db"
|
||||
integrity sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==
|
||||
|
||||
"@typescript-eslint/types@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
|
||||
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
|
||||
|
||||
"@typescript-eslint/types@^8.60.1":
|
||||
"@typescript-eslint/types@^8.60.0", "@typescript-eslint/types@^8.60.1":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
|
||||
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
|
||||
integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.60.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
|
||||
@@ -4900,15 +4953,23 @@
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
|
||||
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
|
||||
"@typescript-eslint/utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
|
||||
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
|
||||
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.60.1":
|
||||
version "8.60.1"
|
||||
@@ -9300,9 +9361,9 @@ jiti@^1.20.0:
|
||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||
|
||||
joi@^17.9.2:
|
||||
version "17.13.4"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.4.tgz#ad6153d97ce558eb3a3b593e0d43eab51df1c474"
|
||||
integrity sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==
|
||||
version "17.13.3"
|
||||
resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz"
|
||||
integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.3.0"
|
||||
"@hapi/topo" "^5.1.0"
|
||||
@@ -14389,15 +14450,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.60.1:
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640"
|
||||
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==
|
||||
typescript-eslint@^8.60.0:
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
|
||||
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.60.1"
|
||||
"@typescript-eslint/parser" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/eslint-plugin" "8.60.0"
|
||||
"@typescript-eslint/parser" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -64,7 +64,7 @@ dependencies = [
|
||||
"holidays>=0.45, <1",
|
||||
"humanize",
|
||||
"isodate",
|
||||
"jsonpath-ng>=1.8.0, <2",
|
||||
"jsonpath-ng>=1.6.1, <2",
|
||||
"Mako>=1.2.2",
|
||||
"markdown>=3.10.2",
|
||||
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
|
||||
@@ -80,7 +80,7 @@ dependencies = [
|
||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||
# --------------------------
|
||||
"parsedatetime",
|
||||
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
|
||||
"paramiko>=3.4.0",
|
||||
"pgsanity",
|
||||
"Pillow>=11.0.0, <13",
|
||||
"polyline>=2.0.0, <3.0",
|
||||
@@ -89,12 +89,12 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"pygeohash",
|
||||
"pyarrow>=24.0.0, <25", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyarrow>=16.1.0, <21", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=5.0.0, <6.0",
|
||||
"rison>=2.0.0, <3.0",
|
||||
"selenium>=4.44.0, <5.0",
|
||||
"selenium>=4.14.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
@@ -107,7 +107,7 @@ dependencies = [
|
||||
"typing-extensions>=4, <5",
|
||||
"waitress; sys_platform == 'win32'",
|
||||
"watchdog>=6.0.0",
|
||||
"wtforms>=3.2.2, <4",
|
||||
"wtforms>=2.3.3, <4",
|
||||
"wtforms-json",
|
||||
"xlsxwriter>=3.2.9, <3.3",
|
||||
]
|
||||
@@ -118,10 +118,10 @@ athena = ["pyathena[pandas]>=2, <4"]
|
||||
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
|
||||
bigquery = [
|
||||
"pandas-gbq>=0.19.1",
|
||||
"sqlalchemy-bigquery>=1.17.0",
|
||||
"sqlalchemy-bigquery>=1.15.0",
|
||||
"google-cloud-bigquery>=3.10.0",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
|
||||
d1 = [
|
||||
@@ -143,14 +143,14 @@ duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
|
||||
dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.2.4,<4.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.
|
||||
"tiktoken>=0.13.0,<1.0",
|
||||
"tiktoken>=0.7.0,<1.0",
|
||||
]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
@@ -161,13 +161,13 @@ hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1.0.0",
|
||||
"thrift>=0.14.1, <1.0.0",
|
||||
"thrift_sasl>=0.4.3, < 1.0.0",
|
||||
]
|
||||
impala = ["impyla>0.16.2, <0.23"]
|
||||
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
|
||||
kylin = ["kylinpy>=2.8.1, <2.9"]
|
||||
mssql = ["pymssql>=2.3.13, <3"]
|
||||
mssql = ["pymssql>=2.2.8, <3"]
|
||||
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
|
||||
motherduck = ["apache-superset[duckdb]"]
|
||||
mysql = ["mysqlclient>=2.1.0, <3"]
|
||||
@@ -195,7 +195,7 @@ spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1",
|
||||
"thrift>=0.14.1, <1",
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.7.21",
|
||||
@@ -205,7 +205,7 @@ teradata = ["teradatasql>=16.20.0.23"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
|
||||
netezza = ["nzalchemy>=11.0.2"]
|
||||
starrocks = ["starrocks>=1.3.3, <2"]
|
||||
starrocks = ["starrocks>=1.0.0"]
|
||||
doris = ["pydoris>=1.0.0, <2.0.0"]
|
||||
oceanbase = ["oceanbase_py>=0.0.1.2"]
|
||||
ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"]
|
||||
@@ -447,7 +447,6 @@ requirement_txt_file = "requirements/base.txt"
|
||||
authorized_licenses = [
|
||||
"academic free license (afl)",
|
||||
"any-osi",
|
||||
"apache-2.0",
|
||||
"apache license 2.0",
|
||||
"apache software",
|
||||
"apache software, bsd",
|
||||
|
||||
@@ -30,7 +30,7 @@ cryptography>=46.0.7,<47.0.0
|
||||
# Security: Snyk - XSS vulnerability in Mako templates
|
||||
mako>=1.3.11,<2.0.0
|
||||
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
|
||||
pyarrow>=24.0.0,<25.0.0
|
||||
pyarrow>=20.0.0,<21.0.0
|
||||
# Security: CVE-2026-27459 - pyopenssl certificate validation
|
||||
pyopenssl>=26.0.0,<27.0.0
|
||||
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File
|
||||
|
||||
@@ -50,7 +50,7 @@ cattrs==25.1.1
|
||||
# via requests-cache
|
||||
celery==5.5.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2026.5.20
|
||||
certifi==2025.6.15
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
@@ -194,7 +194,7 @@ jinja2==3.1.6
|
||||
# via
|
||||
# flask
|
||||
# flask-babel
|
||||
jsonpath-ng==1.8.0
|
||||
jsonpath-ng==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.23.0
|
||||
# via
|
||||
@@ -286,13 +286,15 @@ pillow==12.2.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
polyline==2.0.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
# via click-repl
|
||||
pyarrow==24.0.0
|
||||
pyarrow==20.0.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
@@ -378,7 +380,7 @@ rpds-py==0.25.0
|
||||
# referencing
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
selenium==4.44.0
|
||||
selenium==4.32.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
setuptools==80.9.0
|
||||
# via -r requirements/base.in
|
||||
@@ -421,7 +423,7 @@ sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.10.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.33.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# selenium
|
||||
# trio-websocket
|
||||
@@ -478,7 +480,7 @@ wrapt==1.17.2
|
||||
# via deprecated
|
||||
wsproto==1.2.0
|
||||
# via trio-websocket
|
||||
wtforms==3.2.2
|
||||
wtforms==3.2.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
|
||||
@@ -112,7 +112,7 @@ celery==5.5.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
certifi==2026.5.20
|
||||
certifi==2025.6.15
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# httpcore
|
||||
@@ -471,7 +471,7 @@ jmespath==1.1.0
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
jsonpath-ng==1.8.0
|
||||
jsonpath-ng==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -674,6 +674,10 @@ platformdirs==4.3.8
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
ply==3.11
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# jsonpath-ng
|
||||
polib==1.2.0
|
||||
# via apache-superset
|
||||
polyline==2.0.2
|
||||
@@ -711,7 +715,7 @@ psycopg2-binary==2.9.12
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
pyarrow==24.0.0
|
||||
pyarrow==20.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -921,7 +925,7 @@ s3transfer==0.16.0
|
||||
# via boto3
|
||||
secretstorage==3.5.0
|
||||
# via keyring
|
||||
selenium==4.44.0
|
||||
selenium==4.32.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -976,7 +980,7 @@ sqlalchemy==1.4.54
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.0
|
||||
sqlalchemy-bigquery==1.15.0
|
||||
# via apache-superset
|
||||
sqlalchemy-utils==0.42.0
|
||||
# via
|
||||
@@ -1007,7 +1011,7 @@ tabulate==0.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
tiktoken==0.13.0
|
||||
tiktoken==0.12.0
|
||||
# via apache-superset
|
||||
tomli-w==1.2.0
|
||||
# via apache-superset-extensions-cli
|
||||
@@ -1019,7 +1023,7 @@ tqdm==4.67.1
|
||||
# prophet
|
||||
trino==0.330.0
|
||||
# via apache-superset
|
||||
trio==0.33.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# selenium
|
||||
@@ -1121,7 +1125,7 @@ wsproto==1.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# trio-websocket
|
||||
wtforms==3.2.2
|
||||
wtforms==3.2.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -1 +1 @@
|
||||
v24.16.0
|
||||
v22.22.0
|
||||
|
||||
@@ -149,7 +149,6 @@ module.exports = {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'icons/no-fa-icons-usage': 'error',
|
||||
'i18n-strings/no-template-vars': 'error',
|
||||
'i18n-strings/no-eager-t-in-config': 'off', // enabled only for controlPanel files via overrides below
|
||||
|
||||
// Core ESLint overrides for Superset
|
||||
'no-console': 'warn',
|
||||
@@ -263,19 +262,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
// Eager t()/tn() in `label`/`description` config props is captured at
|
||||
// module-load time, before i18n initializes — labels stay in the fallback
|
||||
// language even after the user switches. Surfaced as a warning (with
|
||||
// autofix to `() => t(...)`) wherever this is a real foot-gun:
|
||||
// controlPanel files. Many pre-existing call sites need conversion;
|
||||
// run `eslint --fix` on a controlPanel file to sweep it. Promote to
|
||||
// `'error'` once the codebase is clean.
|
||||
{
|
||||
files: ['**/controlPanel.{ts,tsx,js,jsx}'],
|
||||
rules: {
|
||||
'i18n-strings/no-eager-t-in-config': 'warn',
|
||||
},
|
||||
},
|
||||
// Ban JavaScript files in src/ - all new code must be TypeScript
|
||||
{
|
||||
files: ['src/**/*.js', 'src/**/*.jsx'],
|
||||
|
||||
@@ -1 +1 @@
|
||||
v24.16.0
|
||||
v22.22.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { dirname, join } from 'path';
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
@@ -16,16 +17,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// This file has been automatically migrated to valid ESM format by Storybook.
|
||||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// Superset's webpack.config.js
|
||||
import customConfig from '../webpack.config.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const customConfig = require('../webpack.config.js');
|
||||
|
||||
// Filter out plugins that shouldn't be included in Storybook's static build
|
||||
// ReactRefreshWebpackPlugin adds Fast Refresh code that requires a dev server runtime,
|
||||
@@ -83,7 +76,7 @@ const disableDevModeInRules = rules =>
|
||||
};
|
||||
});
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../src/**/*.stories.tsx',
|
||||
'../packages/superset-ui-core/src/**/*.stories.tsx',
|
||||
@@ -91,8 +84,11 @@ export default {
|
||||
],
|
||||
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-docs"
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
'@mihkeleidast/storybook-addon-source',
|
||||
getAbsolutePath('@storybook/addon-controls'),
|
||||
getAbsolutePath('@storybook/addon-mdx-gfm'),
|
||||
],
|
||||
|
||||
staticDirs: ['../src/assets/images'],
|
||||
@@ -109,13 +105,11 @@ export default {
|
||||
alias: {
|
||||
...config.resolve?.alias,
|
||||
...customConfig.resolve?.alias,
|
||||
// Fix for Storybook 8.6.x with React 17 - resolve ESM module paths
|
||||
'react-dom/test-utils': require.resolve('react-dom/test-utils'),
|
||||
// Shared storybook utilities
|
||||
'@storybook-shared': path.join(__dirname, 'shared'),
|
||||
'@storybook-shared': join(__dirname, 'shared'),
|
||||
},
|
||||
fallback: {
|
||||
tty: false,
|
||||
vm: require.resolve('vm-browserify')
|
||||
}
|
||||
},
|
||||
plugins: [...config.plugins, ...filteredPlugins],
|
||||
}),
|
||||
@@ -125,11 +119,15 @@ export default {
|
||||
},
|
||||
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/react-webpack5"),
|
||||
name: getAbsolutePath('@storybook/react-webpack5'),
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
|
||||
docs: {
|
||||
autodocs: false,
|
||||
},
|
||||
};
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return path.dirname(require.resolve(path.join(value, 'package.json')));
|
||||
return dirname(require.resolve(join(value, 'package.json')));
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { withJsx } from '@mihkeleidast/storybook-addon-source';
|
||||
import { themeObject, css, exampleThemes } from '@apache-superset/core/theme';
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
@@ -113,12 +114,9 @@ const providerDecorator = Story => (
|
||||
</Provider>
|
||||
);
|
||||
|
||||
export const decorators = [themeDecorator, providerDecorator];
|
||||
export const decorators = [withJsx, themeDecorator, providerDecorator];
|
||||
|
||||
export const parameters = {
|
||||
docs: {
|
||||
codePanel: true,
|
||||
},
|
||||
paddings: {
|
||||
values: [
|
||||
{ name: 'None', value: '0px' },
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { useState, ReactNode, SyntheticEvent } from 'react';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import type { Decorator } from '@storybook/react-webpack5';
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import { ResizeCallbackData } from 'react-resizable';
|
||||
import ResizablePanel, { Size } from './ResizablePanel';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"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.
|
||||
under the License
|
||||
-->
|
||||
|
||||
# Change Log
|
||||
|
||||
@@ -48,7 +48,6 @@ module.exports = {
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
['@babel/plugin-transform-class-properties', { loose: true }],
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
['@babel/plugin-transform-optional-chaining', { loose: true }],
|
||||
['@babel/plugin-transform-private-methods', { loose: true }],
|
||||
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
|
||||
|
||||
57
superset-frontend/cypress-base/package-lock.json
generated
57
superset-frontend/cypress-base/package-lock.json
generated
@@ -2058,24 +2058,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
|
||||
@@ -2952,8 +2934,6 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
@@ -4373,8 +4353,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
@@ -5616,9 +5594,7 @@
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
@@ -7780,9 +7756,7 @@
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
@@ -10228,23 +10202,8 @@
|
||||
"camelcase": "^5.3.1",
|
||||
"find-up": "^4.1.0",
|
||||
"get-package-type": "^0.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"resolve-from": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@istanbuljs/schema": {
|
||||
@@ -11047,8 +11006,6 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
@@ -12094,9 +12051,7 @@
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"esquery": {
|
||||
"version": "1.4.0",
|
||||
@@ -12998,8 +12953,6 @@
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
@@ -14510,9 +14463,7 @@
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.18.0",
|
||||
|
||||
@@ -65,86 +65,6 @@ const plugin: { rules: Record<string, Rule.RuleModule> } = {
|
||||
};
|
||||
},
|
||||
},
|
||||
'no-eager-t-in-config': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow eager t()/tn() calls for `label` and `description` in config objects evaluated at module load (e.g., controlPanel files). The translation is captured at module-evaluation time, before i18n has loaded, and never updates when the user switches language. Wrap the call in an arrow function so it is evaluated at render time.',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
properties: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
eager:
|
||||
'Eager `{{property}}: {{fn}}(...)` is evaluated at module load, before i18n is initialized. Wrap in an arrow function: `{{property}}: () => {{fn}}(...)`.',
|
||||
},
|
||||
},
|
||||
create(context: Rule.RuleContext): Rule.RuleListener {
|
||||
const watchedProps: string[] = context.options[0]?.properties ?? [
|
||||
'label',
|
||||
'description',
|
||||
];
|
||||
const TRANSLATE_FNS = new Set(['t', 'tn']);
|
||||
|
||||
function handler(node: Node): void {
|
||||
const prop = node as Node & {
|
||||
key: { type: string; name?: string; value?: string };
|
||||
value: Node & {
|
||||
type: string;
|
||||
callee?: { type: string; name?: string };
|
||||
};
|
||||
shorthand?: boolean;
|
||||
computed?: boolean;
|
||||
};
|
||||
if (prop.shorthand || prop.computed) return;
|
||||
|
||||
const keyName =
|
||||
prop.key.type === 'Identifier'
|
||||
? prop.key.name
|
||||
: prop.key.type === 'Literal'
|
||||
? prop.key.value
|
||||
: undefined;
|
||||
if (typeof keyName !== 'string' || !watchedProps.includes(keyName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callee = prop.value;
|
||||
if (
|
||||
callee.type !== 'CallExpression' ||
|
||||
callee.callee?.type !== 'Identifier' ||
|
||||
!callee.callee.name ||
|
||||
!TRANSLATE_FNS.has(callee.callee.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: prop.value,
|
||||
messageId: 'eager',
|
||||
data: { property: keyName, fn: callee.callee.name },
|
||||
fix(fixer) {
|
||||
const source = context.getSourceCode().getText(prop.value);
|
||||
return fixer.replaceText(prop.value, `() => ${source}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Property: handler,
|
||||
};
|
||||
},
|
||||
},
|
||||
'sentence-case-buttons': {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
||||
});
|
||||
const rule: Rule.RuleModule = plugin.rules['no-eager-t-in-config'];
|
||||
|
||||
ruleTester.run('no-eager-t-in-config', rule, {
|
||||
valid: [
|
||||
// Lazy form — the recommended pattern
|
||||
"const c = { label: () => t('Foo') };",
|
||||
"const c = { description: () => t('Foo') };",
|
||||
"const c = { label: () => tn('one', 'many', n) };",
|
||||
// Static strings — no translation, no issue
|
||||
"const c = { label: 'Foo' };",
|
||||
// Other property names — unaffected
|
||||
"const c = { name: t('Foo') };",
|
||||
"const c = { title: t('Foo') };",
|
||||
// Computed keys are too dynamic to lint usefully
|
||||
"const c = { [labelKey]: t('Foo') };",
|
||||
// Shorthand: `{ label }` — no value to inspect
|
||||
'const label = t("Foo"); const c = { label };',
|
||||
// t() called inside a function body — already lazy
|
||||
"const c = { label: state => t('Foo') };",
|
||||
// Non-t() call expressions are fine
|
||||
"const c = { label: someOtherFn('Foo') };",
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const c = { label: t('Foo') };",
|
||||
output: "const c = { label: () => t('Foo') };",
|
||||
errors: [{ messageId: 'eager' }],
|
||||
},
|
||||
{
|
||||
code: "const c = { description: t('Foo bar') };",
|
||||
output: "const c = { description: () => t('Foo bar') };",
|
||||
errors: [{ messageId: 'eager' }],
|
||||
},
|
||||
{
|
||||
code: "const c = { label: tn('one', 'many', 2) };",
|
||||
output: "const c = { label: () => tn('one', 'many', 2) };",
|
||||
errors: [{ messageId: 'eager' }],
|
||||
},
|
||||
// String-literal keys are equivalent to identifier keys
|
||||
{
|
||||
code: "const c = { 'label': t('Foo') };",
|
||||
output: "const c = { 'label': () => t('Foo') };",
|
||||
errors: [{ messageId: 'eager' }],
|
||||
},
|
||||
// Custom watched-property list via rule option
|
||||
{
|
||||
code: "const c = { headerTitle: t('Foo') };",
|
||||
output: "const c = { headerTitle: () => t('Foo') };",
|
||||
options: [{ properties: ['headerTitle'] }],
|
||||
errors: [{ messageId: 'eager' }],
|
||||
},
|
||||
// Nested config — fires per occurrence
|
||||
{
|
||||
code: "const c = { foo: { label: t('A'), description: t('B') } };",
|
||||
output:
|
||||
"const c = { foo: { label: () => t('A'), description: () => t('B') } };",
|
||||
errors: [{ messageId: 'eager' }, { messageId: 'eager' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -69,7 +69,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.)',
|
||||
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
9827
superset-frontend/package-lock.json
generated
9827
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -164,8 +164,8 @@
|
||||
"@visx/scale": "^3.5.0",
|
||||
"@visx/tooltip": "^3.0.0",
|
||||
"@visx/xychart": "^3.5.1",
|
||||
"ag-grid-community": "35.3.1",
|
||||
"ag-grid-react": "35.3.1",
|
||||
"ag-grid-community": "35.3.0",
|
||||
"ag-grid-react": "35.3.0",
|
||||
"antd": "^5.26.0",
|
||||
"chrono-node": "^2.9.1",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -178,7 +178,7 @@
|
||||
"echarts": "^5.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fuse.js": "^7.4.1",
|
||||
"fuse.js": "^7.3.0",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -261,14 +261,22 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.13",
|
||||
"@formatjs/intl-durationformat": "^0.10.3",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.2",
|
||||
"@storybook/addon-links": "10.4.2",
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@storybook/addon-actions": "^8.6.18",
|
||||
"@storybook/addon-controls": "^8.6.18",
|
||||
"@storybook/addon-essentials": "^8.6.18",
|
||||
"@storybook/addon-links": "^8.6.18",
|
||||
"@storybook/addon-mdx-gfm": "^8.6.18",
|
||||
"@storybook/components": "^8.6.18",
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/react": "^8.6.18",
|
||||
"@storybook/react-webpack5": "^8.6.18",
|
||||
"@storybook/test": "^8.6.18",
|
||||
"@storybook/test-runner": "^0.17.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
@@ -289,6 +297,7 @@
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-resizable": "^4.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -325,7 +334,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
|
||||
"eslint-plugin-storybook": "10.4.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -355,7 +364,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.2",
|
||||
"storybook": "8.6.18",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
@@ -382,8 +391,8 @@
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.16.0",
|
||||
"npm": "^11.13.0"
|
||||
"node": "^22.22.0",
|
||||
"npm": "^10.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
"uuid": "$uuid",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Disposable, Event } from '../common';
|
||||
import { Disposable } from '../common';
|
||||
|
||||
/**
|
||||
* Represents a menu item that links a view to a command.
|
||||
@@ -102,37 +102,3 @@ export declare function registerMenuItem(
|
||||
* ```
|
||||
*/
|
||||
export declare function getMenu(location: string): Menu | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is registered.
|
||||
*/
|
||||
export interface MenuItemRegisteredEvent {
|
||||
/** The menu item that was registered. */
|
||||
item: MenuItem;
|
||||
/** The location where the item was registered. */
|
||||
location: string;
|
||||
/** The group the item was placed in. */
|
||||
group: 'primary' | 'secondary' | 'context';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is unregistered.
|
||||
*/
|
||||
export interface MenuItemUnregisteredEvent {
|
||||
/** The menu item that was unregistered. */
|
||||
item: MenuItem;
|
||||
/** The location where the item was registered. */
|
||||
location: string;
|
||||
/** The group the item was placed in. */
|
||||
group: 'primary' | 'secondary' | 'context';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is registered.
|
||||
*/
|
||||
export declare const onDidRegisterMenuItem: Event<MenuItemRegisteredEvent>;
|
||||
|
||||
/**
|
||||
* Event fired when a menu item is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterMenuItem: Event<MenuItemUnregisteredEvent>;
|
||||
|
||||
@@ -26,7 +26,7 @@ test('t() warns and creates a default translator when called before configure',
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
const result = t('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/was called before configure\(\)/),
|
||||
'You should call configure(...) before calling other methods',
|
||||
);
|
||||
expect(result).toBe('hello');
|
||||
consoleSpy.mockRestore();
|
||||
@@ -54,7 +54,7 @@ test('resetTranslation resets the configured singleton', () => {
|
||||
// After reset, calling t() should warn again
|
||||
t('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/was called before configure\(\)/),
|
||||
'You should call configure(...) before calling other methods',
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
@@ -96,69 +96,6 @@ test('tn() calls translateWithNumber on the singleton', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('pre-configure warning fires once per unique key', () => {
|
||||
jest.isolateModules(() => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
t('apple');
|
||||
t('apple');
|
||||
t('apple');
|
||||
t('banana');
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(2);
|
||||
expect(consoleSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringContaining('"apple"'),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining('"banana"'),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('pre-configure warning suggests the lazy-function fix', () => {
|
||||
jest.isolateModules(() => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
t('Sort ascending');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('() => t("Sort ascending")'),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('pre-configure warning is suppressed in production', () => {
|
||||
jest.isolateModules(() => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = 'production';
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
t('hello');
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
} else {
|
||||
delete process.env.NODE_ENV;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('resetTranslation clears the warned-keys dedupe set', () => {
|
||||
jest.isolateModules(() => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t, resetTranslation } = require('./TranslatorSingleton');
|
||||
t('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
resetTranslation();
|
||||
t('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(2);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('resetTranslation does nothing when not yet configured', () => {
|
||||
jest.isolateModules(() => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
@@ -168,7 +105,7 @@ test('resetTranslation does nothing when not yet configured', () => {
|
||||
// The singleton is still unconfigured, so t() warns
|
||||
t('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/was called before configure\(\)/),
|
||||
'You should call configure(...) before calling other methods',
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -25,10 +25,6 @@ import { TranslatorConfig, Translations, LocaleData } from './types';
|
||||
let singleton: Translator | undefined;
|
||||
let isConfigured = false;
|
||||
|
||||
// Tracks which keys have already triggered a pre-configure warning so the
|
||||
// logs don't drown in repeated calls from large module-load fan-outs.
|
||||
const warnedPreConfigureKeys = new Set<string>();
|
||||
|
||||
function configure(config?: TranslatorConfig) {
|
||||
singleton = new Translator(config);
|
||||
isConfigured = true;
|
||||
@@ -37,6 +33,10 @@ function configure(config?: TranslatorConfig) {
|
||||
}
|
||||
|
||||
function getInstance() {
|
||||
if (!isConfigured) {
|
||||
console.warn('You should call configure(...) before calling other methods');
|
||||
}
|
||||
|
||||
if (typeof singleton === 'undefined') {
|
||||
singleton = new Translator();
|
||||
}
|
||||
@@ -44,32 +44,11 @@ function getInstance() {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
function warnPreConfigure(fn: 't' | 'tn', key: string) {
|
||||
// Only warn in non-production builds — production callers may legitimately
|
||||
// tolerate the fallback, and the noise isn't useful at runtime.
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.env?.NODE_ENV === 'production'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (warnedPreConfigureKeys.has(key)) return;
|
||||
warnedPreConfigureKeys.add(key);
|
||||
console.warn(
|
||||
`[i18n] ${fn}(${JSON.stringify(key)}) was called before configure() — ` +
|
||||
`the result is the fallback language and will not update when the ` +
|
||||
`user switches language. If this call is at module load (e.g., a ` +
|
||||
`controlPanel \`label\`/\`description\`), wrap it in an arrow ` +
|
||||
`function: \`() => ${fn}(${JSON.stringify(key)})\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
function resetTranslation() {
|
||||
if (isConfigured) {
|
||||
isConfigured = false;
|
||||
singleton = undefined;
|
||||
}
|
||||
warnedPreConfigureKeys.clear();
|
||||
}
|
||||
|
||||
function addTranslation(key: string, translations: string[]) {
|
||||
@@ -85,12 +64,10 @@ function addLocaleData(data: LocaleData) {
|
||||
}
|
||||
|
||||
function t(input: string, ...args: unknown[]) {
|
||||
if (!isConfigured) warnPreConfigure('t', input);
|
||||
return getInstance().translate(input, ...args);
|
||||
}
|
||||
|
||||
function tn(key: string, ...args: unknown[]) {
|
||||
if (!isConfigured) warnPreConfigure('tn', key);
|
||||
return getInstance().translateWithNumber(key, ...args);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { Disposable, Event } from '../common';
|
||||
import { Disposable } from '../common';
|
||||
|
||||
/**
|
||||
* Represents a contributed view in the application.
|
||||
@@ -88,33 +88,3 @@ export declare function registerView(
|
||||
* ```
|
||||
*/
|
||||
export declare function getViews(location: string): View[] | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when a view is registered.
|
||||
*/
|
||||
export interface ViewRegisteredEvent {
|
||||
/** The descriptor of the view that was registered. */
|
||||
view: View;
|
||||
/** The location where the view was registered. */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a view is unregistered.
|
||||
*/
|
||||
export interface ViewUnregisteredEvent {
|
||||
/** The descriptor of the view that was unregistered. */
|
||||
view: View;
|
||||
/** The location where the view was registered. */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when a view is registered.
|
||||
*/
|
||||
export declare const onDidRegisterView: Event<ViewRegisteredEvent>;
|
||||
|
||||
/**
|
||||
* Event fired when a view is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterView: Event<ViewUnregisteredEvent>;
|
||||
|
||||
@@ -204,14 +204,8 @@ export type TabOverride = 'data' | 'customize' | 'matrixify' | boolean;
|
||||
* these configs will be passed to the UI component for control as props.
|
||||
*
|
||||
* - type: the control type, referencing a React component of the same name
|
||||
* - label: the label as shown in the control's header. When the value involves
|
||||
* `t()`/`tn()`, prefer the arrow-function form (`label: () => t('Foo')`) so
|
||||
* the lookup runs at render time rather than at module load — eager
|
||||
* `label: t('Foo')` captures the fallback language before i18n initializes
|
||||
* and does not update on runtime language change. The
|
||||
* `i18n-strings/no-eager-t-in-config` lint rule autofixes this.
|
||||
* - description: shown in the info tooltip of the control's header. Same
|
||||
* lazy-form guidance as `label`.
|
||||
* - label: the label as shown in the control's header
|
||||
* - description: shown in the info tooltip of the control's header
|
||||
* - default: the default value when opening a new chart, or changing visualization type
|
||||
* - renderTrigger: a bool that defines whether the visualization should be re-rendered
|
||||
* when changed. This should `true` for controls that only affect the rendering (client side)
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
"ag-grid-community": "35.3.1",
|
||||
"ag-grid-react": "35.3.1",
|
||||
"ag-grid-community": "35.3.0",
|
||||
"ag-grid-react": "35.3.0",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.5.1",
|
||||
"core-js": "^3.49.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dompurify": "^3.4.8",
|
||||
"dompurify": "^3.4.7",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
|
||||
@@ -72,7 +72,6 @@ test('should generate a 2x2 grid for metrics mode', () => {
|
||||
createAdhocMetric('Revenue'),
|
||||
createSqlMetric('Q1', 'SUM(CASE WHEN quarter = 1 THEN value END)'),
|
||||
]);
|
||||
expect(firstCell!.formData.metric).toEqual(createAdhocMetric('Revenue'));
|
||||
});
|
||||
|
||||
test('should generate grid for dimensions mode', () => {
|
||||
@@ -214,9 +213,6 @@ test('should skip missing column metrics when generating cell form data', () =>
|
||||
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
|
||||
createAdhocMetric('Revenue'),
|
||||
]);
|
||||
expect(grid!.cells[0][0]!.formData.metric).toEqual(
|
||||
createAdhocMetric('Revenue'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not escape HTML entities in cell titles', () => {
|
||||
@@ -475,51 +471,6 @@ test('should handle metrics without labels', () => {
|
||||
expect(grid!.colHeaders).toEqual(['count']);
|
||||
});
|
||||
|
||||
test('should set singular metric for singular-metric chart types like Pie', () => {
|
||||
const rowMetricFormData: TestFormData = {
|
||||
viz_type: 'pie',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
|
||||
};
|
||||
|
||||
const grid = generateMatrixifyGrid(rowMetricFormData);
|
||||
|
||||
expect(grid).not.toBeNull();
|
||||
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
|
||||
createAdhocMetric('Revenue'),
|
||||
]);
|
||||
expect(grid!.cells[0][0]!.formData.metric).toEqual(
|
||||
createAdhocMetric('Revenue'),
|
||||
);
|
||||
expect(grid!.cells[1][0]!.formData.metrics).toEqual([
|
||||
createAdhocMetric('Profit'),
|
||||
]);
|
||||
expect(grid!.cells[1][0]!.formData.metric).toEqual(
|
||||
createAdhocMetric('Profit'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not overwrite singular metric in dimension-only mode', () => {
|
||||
const dimensionFormData: TestFormData = {
|
||||
viz_type: 'pie',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_dimension_rows: {
|
||||
dimension: 'country',
|
||||
values: ['USA', 'Canada'],
|
||||
},
|
||||
metric: 'existing_metric',
|
||||
};
|
||||
|
||||
const grid = generateMatrixifyGrid(dimensionFormData);
|
||||
|
||||
expect(grid).not.toBeNull();
|
||||
expect(grid!.cells[0][0]!.formData.metric).toBe('existing_metric');
|
||||
});
|
||||
|
||||
test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => {
|
||||
const formDataWithDashboardContext: TestFormData = {
|
||||
...baseFormData,
|
||||
|
||||
@@ -197,7 +197,6 @@ function generateCellFormData(
|
||||
// If we have metrics from the matrix, use them; otherwise keep original
|
||||
if (metrics.length > 0) {
|
||||
cellFormData.metrics = metrics;
|
||||
cellFormData.metric = metrics[0];
|
||||
}
|
||||
|
||||
return cellFormData;
|
||||
|
||||
@@ -17,20 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type {
|
||||
ComponentType,
|
||||
WeakValidationMap,
|
||||
ForwardRefExoticComponent,
|
||||
PropsWithoutRef,
|
||||
RefAttributes,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-syntax -- whole React import is required for `reactify.test.tsx` Jest test passing.
|
||||
import { Component, ComponentClass, WeakValidationMap } from 'react';
|
||||
|
||||
// TODO: Note that id and className can collide between Props and ReactifyProps
|
||||
// leading to (likely) unexpected behaviors. We should either require Props to not
|
||||
@@ -61,103 +49,66 @@ export interface RenderFuncType<Props> {
|
||||
propTypes?: WeakValidationMap<Props & ReactifyProps>;
|
||||
}
|
||||
|
||||
export interface ReactifiedComponentRef {
|
||||
container?: HTMLDivElement;
|
||||
}
|
||||
|
||||
export type ReactifiedComponent<Props> = ForwardRefExoticComponent<
|
||||
PropsWithoutRef<Props & ReactifyProps> & RefAttributes<ReactifiedComponentRef>
|
||||
>;
|
||||
|
||||
// Return the widest public type that covers "use it as a React component" so
|
||||
// TypeScript JSX callers and `ComponentType<...>`-typed variables still compile;
|
||||
// callers with explicit `ComponentClass<...>` annotations must widen to
|
||||
// `ComponentType`. Those wanting the forwardRef surface can narrow to
|
||||
// `ReactifiedComponent<Props>` explicitly.
|
||||
export default function reactify<Props extends object>(
|
||||
renderFn: RenderFuncType<Props>,
|
||||
callbacks?: LifeCycleCallbacks,
|
||||
): ComponentType<Props & ReactifyProps> {
|
||||
const ReactifiedComponent = forwardRef<
|
||||
ReactifiedComponentRef,
|
||||
Props & ReactifyProps
|
||||
>(function ReactifiedComponent(props, ref) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Keep the latest props available to the unmount callback — legacy
|
||||
// consumers read values off `this.props` (e.g. ReactNVD3 uses id).
|
||||
// Update the ref in a layout effect rather than during render so the
|
||||
// assignment only happens for committed renders (safe under Concurrent
|
||||
// Mode) and is in place before the passive unmount effect reads it.
|
||||
const propsRef = useRef(props);
|
||||
useLayoutEffect(() => {
|
||||
propsRef.current = props;
|
||||
});
|
||||
): ComponentClass<Props & ReactifyProps> {
|
||||
class ReactifiedComponent extends Component<Props & ReactifyProps> {
|
||||
container?: HTMLDivElement;
|
||||
|
||||
// Expose container via ref for external access
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
get container() {
|
||||
return containerRef.current ?? undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
constructor(props: Props & ReactifyProps) {
|
||||
super(props);
|
||||
this.setContainerRef = this.setContainerRef.bind(this);
|
||||
}
|
||||
|
||||
// Execute renderFn on mount and every update (mimics componentDidMount + componentDidUpdate)
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
// `forwardRef` widens the props parameter to `PropsWithoutRef<...>`,
|
||||
// which TypeScript can't narrow back to `Props & ReactifyProps` when
|
||||
// `Props` is a generic `object`. The values are identical at runtime,
|
||||
// so assert the original prop shape for `renderFn`.
|
||||
renderFn(
|
||||
containerRef.current,
|
||||
props as Readonly<Props & ReactifyProps>,
|
||||
);
|
||||
componentDidMount() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.container = undefined;
|
||||
if (callbacks?.componentWillUnmount) {
|
||||
callbacks.componentWillUnmount.bind(this)();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (callbacks?.componentWillUnmount) {
|
||||
// Preserve legacy behavior where `this` was a component instance
|
||||
// exposing `props`. The class version cleared `this.container`
|
||||
// before invoking componentWillUnmount, so mirror that here to
|
||||
// prevent callbacks from touching a DOM node that's being torn
|
||||
// down.
|
||||
callbacks.componentWillUnmount.call({
|
||||
container: undefined,
|
||||
props: propsRef.current,
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
setContainerRef(ref: HTMLDivElement) {
|
||||
this.container = ref;
|
||||
}
|
||||
|
||||
const { id, className } = props;
|
||||
execute() {
|
||||
if (this.container) {
|
||||
renderFn(this.container, this.props);
|
||||
}
|
||||
}
|
||||
|
||||
return <div ref={containerRef} id={id} className={className} />;
|
||||
});
|
||||
render() {
|
||||
const { id, className } = this.props;
|
||||
|
||||
if (renderFn.displayName) {
|
||||
ReactifiedComponent.displayName = renderFn.displayName;
|
||||
return <div ref={this.setContainerRef} id={id} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- forwardRef static field types don't line up with renderFn's validator types
|
||||
const result = ReactifiedComponent as any;
|
||||
const ReactifiedClass: ComponentClass<Props & ReactifyProps> =
|
||||
ReactifiedComponent;
|
||||
|
||||
if (renderFn.displayName) {
|
||||
ReactifiedClass.displayName = renderFn.displayName;
|
||||
}
|
||||
// eslint-disable-next-line react/forbid-foreign-prop-types
|
||||
if (renderFn.propTypes) {
|
||||
result.propTypes = {
|
||||
...result.propTypes,
|
||||
ReactifiedClass.propTypes = {
|
||||
...ReactifiedClass.propTypes,
|
||||
...renderFn.propTypes,
|
||||
};
|
||||
}
|
||||
|
||||
if (renderFn.defaultProps) {
|
||||
result.defaultProps = renderFn.defaultProps;
|
||||
ReactifiedClass.defaultProps = renderFn.defaultProps;
|
||||
}
|
||||
|
||||
return result as unknown as ComponentType<Props & ReactifyProps>;
|
||||
return ReactifiedComponent;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AutoComplete } from '.';
|
||||
import type { AutoCompleteProps } from './types';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Breadcrumb } from '.';
|
||||
import type { BreadcrumbProps } from './types';
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { action } from 'storybook/actions';
|
||||
import { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { CachedLabel } from '.';
|
||||
import type { CacheLabelProps } from './types';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useArgs } from 'storybook/preview-api';
|
||||
import { useArgs } from '@storybook/preview-api';
|
||||
import { useState } from 'react';
|
||||
import { Checkbox } from '.';
|
||||
import type { CheckboxProps, CheckboxChangeEvent } from './types';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
import { EmptyState, imageMap } from '.';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { FaveStar } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import Slider from '@superset-ui/core/components/Slider/index';
|
||||
import { useState } from 'react';
|
||||
import { Row, Col } from '.';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { IconButton } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { StoryObj } from '@storybook/react-webpack5';
|
||||
import type { StoryObj } from '@storybook/react';
|
||||
import { Input, InputNumber } from '.';
|
||||
import type { InputProps, InputNumberProps, TextAreaProps } from './types';
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { action } from 'storybook/actions';
|
||||
import { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import type { LabelType } from './types';
|
||||
import { Label, DatasetTypeLabel, PublishedLabel } from '.';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Menu } from '../Menu';
|
||||
import type { LayoutProps, SiderProps } from './types';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ListViewCard } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { StoryObj } from '@storybook/react-webpack5';
|
||||
import type { StoryObj } from '@storybook/react';
|
||||
import { css } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Space } from '../Space';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { SafeMarkdown } from './SafeMarkdown';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Space } from '../Space';
|
||||
import { Skeleton, type SkeletonProps } from '.';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useArgs } from 'storybook/preview-api';
|
||||
import { useArgs } from '@storybook/preview-api';
|
||||
import { Switch, type SwitchProps } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
*/
|
||||
import { useState, DragEvent } from 'react';
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { action } from 'storybook/actions';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import {
|
||||
ColumnsType,
|
||||
ETableAction,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import ActionCell from './index';
|
||||
import { exampleMenuOptions, exampleRow } from './fixtures';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { action } from 'storybook/actions';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ActionMenuItem } from './index';
|
||||
|
||||
export const exampleMenuOptions: ActionMenuItem[] = [
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import BooleanCell from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { action } from 'storybook/actions';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ButtonCell } from './index';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import NullCell from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import { CurrencyCode, NumericCell, LocaleCode, Style } from './index';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryFn, Meta } from '@storybook/react-webpack5';
|
||||
import { StoryFn, Meta } from '@storybook/react';
|
||||
import { TimeFormats } from '@superset-ui/core';
|
||||
import TimeCell from '.';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { Meta, StoryFn } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import {
|
||||
useTable,
|
||||
useSortBy,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useArgs } from 'storybook/preview-api';
|
||||
import { useArgs } from '@storybook/preview-api';
|
||||
import TimezoneSelector, { TimezoneSelectorProps } from './index';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryObj } from '@storybook/react-webpack5';
|
||||
import { StoryObj } from '@storybook/react';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import Tree, { TreeProps, type TreeDataNode } from './index';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { TreeSelect, type TreeSelectProps } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Typography } from '.';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { StoryObj } from '@storybook/react-webpack5';
|
||||
import type { StoryObj } from '@storybook/react';
|
||||
import { Icons } from '../Icons';
|
||||
import { Button } from '../Button';
|
||||
import { Upload } from '.';
|
||||
|
||||
@@ -35,4 +35,3 @@ export * from './typedMemo';
|
||||
export * from './html';
|
||||
export * from './tooltip';
|
||||
export * from './merge';
|
||||
export * from './mapStyles';
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getBootstrapDataFromDocument,
|
||||
getDefaultMapRenderer,
|
||||
getMapProviderMapStyle,
|
||||
getMapboxApiKeyFromBootstrap,
|
||||
getMapRendererOptions,
|
||||
hasMapboxApiKey,
|
||||
isRasterTileTemplate,
|
||||
OSM_TILE_ATTRIBUTION,
|
||||
OSM_TILE_STYLE_URL,
|
||||
resolveMapStyle,
|
||||
} from './mapStyles';
|
||||
|
||||
test('OSM style metadata uses the approved URL and attribution', () => {
|
||||
expect(OSM_TILE_STYLE_URL).toBe(
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
);
|
||||
expect(OSM_TILE_ATTRIBUTION).toBe('© OpenStreetMap contributors');
|
||||
});
|
||||
|
||||
test('Mapbox key helpers report absence and presence from bootstrap data', () => {
|
||||
expect(getMapboxApiKeyFromBootstrap({ common: { conf: {} } })).toBe('');
|
||||
expect(hasMapboxApiKey({ common: { conf: {} } })).toBe(false);
|
||||
expect(
|
||||
getMapboxApiKeyFromBootstrap({
|
||||
common: { conf: { MAPBOX_API_KEY: 'pk.test' } },
|
||||
}),
|
||||
).toBe('pk.test');
|
||||
expect(
|
||||
getMapboxApiKeyFromBootstrap({
|
||||
common: { conf: { MAPBOX_API_KEY: ' pk.test ' } },
|
||||
}),
|
||||
).toBe('pk.test');
|
||||
expect(hasMapboxApiKey({ common: { conf: { MAPBOX_API_KEY: ' ' } } })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
hasMapboxApiKey({ common: { conf: { MAPBOX_API_KEY: 'pk.test' } } }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('bootstrap data helper parses document data safely', () => {
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
|
||||
common: { conf: { MAPBOX_API_KEY: 'pk.document' } },
|
||||
})}'></div>`;
|
||||
|
||||
expect(getBootstrapDataFromDocument()).toEqual({
|
||||
common: { conf: { MAPBOX_API_KEY: 'pk.document' } },
|
||||
});
|
||||
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='not-json'></div>`;
|
||||
expect(getBootstrapDataFromDocument()).toBeUndefined();
|
||||
|
||||
document.body.innerHTML = '';
|
||||
expect(getBootstrapDataFromDocument()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('bootstrap data helper returns undefined without a document', () => {
|
||||
// jsdom defines `document` as a non-configurable global, so the SSR guard
|
||||
// cannot be exercised by deleting it. Instead, re-evaluate the function's
|
||||
// own source in a scope where `document` is shadowed with undefined. When
|
||||
// running under coverage, the source is istanbul-instrumented and references
|
||||
// its module-scoped counter, so the counter is injected to keep the guard's
|
||||
// execution attributed to mapStyles.ts.
|
||||
const source = getBootstrapDataFromDocument.toString();
|
||||
const counterName = source.match(/cov_\w+/)?.[0] ?? 'unusedCoverageCounter';
|
||||
const coverage = (globalThis as { __coverage__?: Record<string, unknown> })
|
||||
.__coverage__;
|
||||
const coverageEntry =
|
||||
coverage?.[
|
||||
Object.keys(coverage).find(file => file.endsWith('mapStyles.ts')) ?? ''
|
||||
];
|
||||
// eslint-disable-next-line no-new-func
|
||||
const callWithoutDocument = new Function(
|
||||
counterName,
|
||||
'document',
|
||||
`return (${source})();`,
|
||||
);
|
||||
expect(callWithoutDocument(() => coverageEntry, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('renderer options enable Mapbox only when a key is available', () => {
|
||||
expect(getMapRendererOptions({ hasMapboxKey: true })).toEqual([
|
||||
{ value: 'maplibre' },
|
||||
{ value: 'mapbox' },
|
||||
]);
|
||||
expect(getMapRendererOptions({ hasMapboxKey: false })).toEqual([
|
||||
{ value: 'maplibre' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('renderer options preserve saved Mapbox without API-key labels', () => {
|
||||
expect(
|
||||
getMapRendererOptions({ hasMapboxKey: false, currentValue: 'mapbox' }),
|
||||
).toEqual([{ value: 'maplibre' }, { value: 'mapbox', disabled: true }]);
|
||||
});
|
||||
|
||||
test('map provider style helper preserves legacy non-Mapbox styles for MapLibre', () => {
|
||||
expect(
|
||||
getMapProviderMapStyle({
|
||||
mapProvider: 'maplibre',
|
||||
maplibreStyle: undefined,
|
||||
mapboxStyle: OSM_TILE_STYLE_URL,
|
||||
legacyMapStyle: 'https://example.com/fallback-style.json',
|
||||
}),
|
||||
).toEqual({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: OSM_TILE_STYLE_URL,
|
||||
});
|
||||
});
|
||||
|
||||
test('map provider style helper does not send Mapbox URLs to MapLibre', () => {
|
||||
expect(
|
||||
getMapProviderMapStyle({
|
||||
mapProvider: 'maplibre',
|
||||
mapboxStyle: 'mapbox://styles/mapbox/dark-v11',
|
||||
legacyMapStyle: 'https://example.com/fallback-style.json',
|
||||
}),
|
||||
).toEqual({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: 'https://example.com/fallback-style.json',
|
||||
});
|
||||
});
|
||||
|
||||
test('map provider style helper uses Mapbox style when Mapbox is selected', () => {
|
||||
expect(
|
||||
getMapProviderMapStyle({
|
||||
mapProvider: 'mapbox',
|
||||
mapboxStyle: 'mapbox://styles/mapbox/dark-v11',
|
||||
legacyMapStyle: 'https://example.com/fallback-style.json',
|
||||
}),
|
||||
).toEqual({
|
||||
mapProvider: 'mapbox',
|
||||
mapStyle: 'mapbox://styles/mapbox/dark-v11',
|
||||
});
|
||||
});
|
||||
|
||||
test('default renderer uses configured Mapbox only when a key is available', () => {
|
||||
expect(
|
||||
getDefaultMapRenderer({
|
||||
common: {
|
||||
conf: {
|
||||
DEFAULT_MAP_RENDERER: 'mapbox',
|
||||
MAPBOX_API_KEY: 'pk.test',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe('mapbox');
|
||||
expect(
|
||||
getDefaultMapRenderer({
|
||||
common: { conf: { DEFAULT_MAP_RENDERER: 'mapbox' } },
|
||||
}),
|
||||
).toBe('maplibre');
|
||||
expect(
|
||||
getDefaultMapRenderer({
|
||||
common: {
|
||||
conf: {
|
||||
DEFAULT_MAP_RENDERER: 'invalid',
|
||||
MAPBOX_API_KEY: 'pk.test',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe('maplibre');
|
||||
});
|
||||
|
||||
test('raster tile templates resolve to MapLibre raster style objects with attribution', () => {
|
||||
const style = resolveMapStyle(OSM_TILE_STYLE_URL, 'default-style.json');
|
||||
|
||||
expect(style).toEqual({
|
||||
version: 8,
|
||||
sources: {
|
||||
'osm-raster-tiles': {
|
||||
type: 'raster',
|
||||
tiles: [OSM_TILE_STYLE_URL],
|
||||
tileSize: 256,
|
||||
attribution: OSM_TILE_ATTRIBUTION,
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'osm-raster-layer',
|
||||
type: 'raster',
|
||||
source: 'osm-raster-tiles',
|
||||
minzoom: 0,
|
||||
maxzoom: 22,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('tile protocol raster templates are unwrapped before style resolution', () => {
|
||||
const style = resolveMapStyle(
|
||||
`tile://${OSM_TILE_STYLE_URL}`,
|
||||
'default-style.json',
|
||||
);
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([
|
||||
OSM_TILE_STYLE_URL,
|
||||
]);
|
||||
expect(style.sources['osm-raster-tiles'].attribution).toBe(
|
||||
OSM_TILE_ATTRIBUTION,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenStreetMap subdomain raster templates receive OSM attribution', () => {
|
||||
const osmSubdomainTileUrl =
|
||||
'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
const style = resolveMapStyle(
|
||||
`tile://${osmSubdomainTileUrl}`,
|
||||
'default-style.json',
|
||||
);
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([
|
||||
osmSubdomainTileUrl,
|
||||
]);
|
||||
expect(style.sources['osm-raster-tiles'].attribution).toBe(
|
||||
OSM_TILE_ATTRIBUTION,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('custom raster tile templates do not receive OSM attribution', () => {
|
||||
const customTileUrl = 'https://tiles.example.com/{z}/{x}/{y}.png';
|
||||
const style = resolveMapStyle(
|
||||
`tile://${customTileUrl}`,
|
||||
'default-style.json',
|
||||
);
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([customTileUrl]);
|
||||
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
|
||||
}
|
||||
});
|
||||
|
||||
test('relative raster tile templates do not receive OSM attribution', () => {
|
||||
const relativeTileUrl = '/tiles/{z}/{x}/{y}.png';
|
||||
const style = resolveMapStyle(relativeTileUrl, 'default-style.json');
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([relativeTileUrl]);
|
||||
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
|
||||
}
|
||||
});
|
||||
|
||||
test('lookalike OpenStreetMap hostnames do not receive OSM attribution', () => {
|
||||
const lookalikeTileUrl =
|
||||
'https://openstreetmap.org.example.com/{z}/{x}/{y}.png';
|
||||
const style = resolveMapStyle(
|
||||
`tile://${lookalikeTileUrl}`,
|
||||
'default-style.json',
|
||||
);
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([lookalikeTileUrl]);
|
||||
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
|
||||
}
|
||||
});
|
||||
|
||||
test('style JSON URLs pass through without raster wrapping', () => {
|
||||
const styleUrl = 'https://example.com/styles/custom-style.json';
|
||||
|
||||
expect(isRasterTileTemplate(undefined)).toBe(false);
|
||||
expect(isRasterTileTemplate(styleUrl)).toBe(false);
|
||||
expect(resolveMapStyle(styleUrl, 'default-style.json')).toBe(styleUrl);
|
||||
expect(resolveMapStyle(undefined, 'default-style.json')).toBe(
|
||||
'default-style.json',
|
||||
);
|
||||
});
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export type MapProvider = 'maplibre' | 'mapbox';
|
||||
|
||||
export type MapRendererOption = {
|
||||
value: MapProvider;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type MapProviderMapStyle = {
|
||||
mapProvider?: unknown;
|
||||
maplibreStyle?: unknown;
|
||||
mapboxStyle?: unknown;
|
||||
legacyMapStyle?: unknown;
|
||||
};
|
||||
|
||||
export type SelectedMapProviderMapStyle = {
|
||||
mapProvider: MapProvider;
|
||||
mapStyle?: string;
|
||||
};
|
||||
|
||||
export type RasterTileMapStyle = {
|
||||
version: 8;
|
||||
sources: {
|
||||
[sourceId: string]: {
|
||||
type: 'raster';
|
||||
tiles: string[];
|
||||
tileSize: 256;
|
||||
attribution?: string;
|
||||
};
|
||||
};
|
||||
layers: [
|
||||
{
|
||||
id: string;
|
||||
type: 'raster';
|
||||
source: string;
|
||||
minzoom: 0;
|
||||
maxzoom: 22;
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export type ResolvedMapStyle = string | RasterTileMapStyle;
|
||||
|
||||
export const OSM_TILE_STYLE_URL =
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
export const OSM_TILE_ATTRIBUTION = '© OpenStreetMap contributors';
|
||||
|
||||
export const MAPLIBRE_RENDERER_OPTION: MapRendererOption = {
|
||||
value: 'maplibre',
|
||||
};
|
||||
export const MAPBOX_RENDERER_OPTION: MapRendererOption = {
|
||||
value: 'mapbox',
|
||||
};
|
||||
export const DISABLED_MAPBOX_RENDERER_OPTION: MapRendererOption = {
|
||||
...MAPBOX_RENDERER_OPTION,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
const TILE_PROTOCOL = 'tile://';
|
||||
const RASTER_SOURCE_ID = 'osm-raster-tiles';
|
||||
const RASTER_LAYER_ID = 'osm-raster-layer';
|
||||
|
||||
type BootstrapData = {
|
||||
common?: {
|
||||
conf?: {
|
||||
DEFAULT_MAP_RENDERER?: unknown;
|
||||
MAPBOX_API_KEY?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function getBootstrapDataFromDocument(): unknown {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const appContainer = document.getElementById('app');
|
||||
const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
|
||||
return dataBootstrap ? JSON.parse(dataBootstrap) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMapboxApiKeyFromBootstrap(
|
||||
bootstrapData: unknown = getBootstrapDataFromDocument(),
|
||||
): string {
|
||||
const mapboxApiKey = (bootstrapData as BootstrapData | undefined)?.common
|
||||
?.conf?.MAPBOX_API_KEY;
|
||||
return typeof mapboxApiKey === 'string' ? mapboxApiKey.trim() : '';
|
||||
}
|
||||
|
||||
export function hasMapboxApiKey(
|
||||
bootstrapData: unknown = getBootstrapDataFromDocument(),
|
||||
): boolean {
|
||||
return getMapboxApiKeyFromBootstrap(bootstrapData).trim().length > 0;
|
||||
}
|
||||
|
||||
export function getDefaultMapRenderer(
|
||||
bootstrapData: unknown = getBootstrapDataFromDocument(),
|
||||
): MapProvider {
|
||||
const conf = (bootstrapData as BootstrapData | undefined)?.common?.conf;
|
||||
const defaultRenderer = conf?.DEFAULT_MAP_RENDERER;
|
||||
|
||||
if (defaultRenderer === 'mapbox' && hasMapboxApiKey(bootstrapData)) {
|
||||
return 'mapbox';
|
||||
}
|
||||
|
||||
return 'maplibre';
|
||||
}
|
||||
|
||||
export function getMapRendererOptions({
|
||||
hasMapboxKey,
|
||||
currentValue,
|
||||
}: {
|
||||
hasMapboxKey: boolean;
|
||||
currentValue?: MapProvider;
|
||||
}): MapRendererOption[] {
|
||||
if (!hasMapboxKey && currentValue !== 'mapbox') {
|
||||
return [MAPLIBRE_RENDERER_OPTION];
|
||||
}
|
||||
|
||||
return [
|
||||
MAPLIBRE_RENDERER_OPTION,
|
||||
hasMapboxKey ? MAPBOX_RENDERER_OPTION : DISABLED_MAPBOX_RENDERER_OPTION,
|
||||
];
|
||||
}
|
||||
|
||||
function getNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isMapboxStyle(value: unknown): boolean {
|
||||
return getNonEmptyString(value)?.startsWith('mapbox://') ?? false;
|
||||
}
|
||||
|
||||
export function getMapProviderMapStyle({
|
||||
mapProvider,
|
||||
maplibreStyle,
|
||||
mapboxStyle,
|
||||
legacyMapStyle,
|
||||
}: MapProviderMapStyle): SelectedMapProviderMapStyle {
|
||||
const selectedMapProvider: MapProvider =
|
||||
mapProvider === 'mapbox' ? 'mapbox' : 'maplibre';
|
||||
const maplibreStyleValue = getNonEmptyString(maplibreStyle);
|
||||
const mapboxStyleValue = getNonEmptyString(mapboxStyle);
|
||||
const legacyMapStyleValue = getNonEmptyString(legacyMapStyle);
|
||||
|
||||
if (selectedMapProvider === 'mapbox') {
|
||||
return {
|
||||
mapProvider: selectedMapProvider,
|
||||
mapStyle: mapboxStyleValue ?? legacyMapStyleValue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mapProvider: selectedMapProvider,
|
||||
mapStyle:
|
||||
maplibreStyleValue ??
|
||||
(isMapboxStyle(mapboxStyleValue) ? undefined : mapboxStyleValue) ??
|
||||
legacyMapStyleValue,
|
||||
};
|
||||
}
|
||||
|
||||
function unwrapTileProtocol(value: string): string {
|
||||
return value.startsWith(TILE_PROTOCOL)
|
||||
? value.slice(TILE_PROTOCOL.length)
|
||||
: value;
|
||||
}
|
||||
|
||||
export function isRasterTileTemplate(value: unknown): value is string {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const tileUrl = unwrapTileProtocol(value);
|
||||
return ['{z}', '{x}', '{y}'].every(templateParam =>
|
||||
tileUrl.includes(templateParam),
|
||||
);
|
||||
}
|
||||
|
||||
function isOpenStreetMapTileUrl(value: string): boolean {
|
||||
try {
|
||||
const hostname = new URL(value).hostname.toLowerCase();
|
||||
return (
|
||||
hostname === 'openstreetmap.org' ||
|
||||
hostname.endsWith('.openstreetmap.org')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRasterTileMapStyle(value: string): RasterTileMapStyle {
|
||||
const tileUrl = unwrapTileProtocol(value);
|
||||
const attribution = isOpenStreetMapTileUrl(tileUrl)
|
||||
? { attribution: OSM_TILE_ATTRIBUTION }
|
||||
: {};
|
||||
|
||||
return {
|
||||
version: 8,
|
||||
sources: {
|
||||
[RASTER_SOURCE_ID]: {
|
||||
type: 'raster',
|
||||
tiles: [tileUrl],
|
||||
tileSize: 256,
|
||||
...attribution,
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: RASTER_LAYER_ID,
|
||||
type: 'raster',
|
||||
source: RASTER_SOURCE_ID,
|
||||
minzoom: 0,
|
||||
maxzoom: 22,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMapStyle(
|
||||
value: string | undefined,
|
||||
defaultStyle: string,
|
||||
): ResolvedMapStyle {
|
||||
if (!value) {
|
||||
return defaultStyle;
|
||||
}
|
||||
|
||||
return isRasterTileTemplate(value) ? buildRasterTileMapStyle(value) : value;
|
||||
}
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PureComponent } from 'react';
|
||||
import { reactify } from '@superset-ui/core';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RenderFuncType } from '../../../src/chart/components/reactify';
|
||||
|
||||
describe('reactify(renderFn)', () => {
|
||||
@@ -52,41 +52,48 @@ describe('reactify(renderFn)', () => {
|
||||
componentWillUnmount: willUnmountCb,
|
||||
});
|
||||
|
||||
function TestComponent() {
|
||||
const [content, setContent] = useState('abc');
|
||||
class TestComponent extends PureComponent<{}, { content: string }> {
|
||||
constructor(props = {}) {
|
||||
super(props);
|
||||
this.state = { content: 'abc' };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setContent('def');
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.setState({ content: 'def' });
|
||||
}, 10);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
}
|
||||
|
||||
return <TheChart id="test" content={content} />;
|
||||
render() {
|
||||
const { content } = this.state;
|
||||
|
||||
return <TheChart id="test" content={content} />;
|
||||
}
|
||||
}
|
||||
|
||||
function AnotherTestComponent() {
|
||||
return <TheChartWithWillUnmountHook id="another_test" />;
|
||||
class AnotherTestComponent extends PureComponent<{}, {}> {
|
||||
render() {
|
||||
return <TheChartWithWillUnmountHook id="another_test" />;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(renderFn as jest.Mock).mockClear();
|
||||
willUnmountCb.mockClear();
|
||||
});
|
||||
test('returns a React component class', () =>
|
||||
new Promise(done => {
|
||||
render(<TestComponent />);
|
||||
|
||||
test('returns a React component and re-renders on prop changes', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute('id', 'test');
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(renderFn).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('abc')).toBeInTheDocument();
|
||||
expect(screen.getByText('abc').parentNode).toHaveAttribute('id', 'test');
|
||||
setTimeout(() => {
|
||||
expect(renderFn).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByText('def')).toBeInTheDocument();
|
||||
expect(screen.getByText('def').parentNode).toHaveAttribute(
|
||||
'id',
|
||||
'test',
|
||||
);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
describe('displayName', () => {
|
||||
test('has displayName if renderFn.displayName is defined', () => {
|
||||
expect(TheChart.displayName).toEqual('BoldText');
|
||||
@@ -119,16 +126,20 @@ describe('reactify(renderFn)', () => {
|
||||
expect(AnotherChart.defaultProps).toBeUndefined();
|
||||
});
|
||||
});
|
||||
test('calls renderFn when container is set', () => {
|
||||
test('does not try to render if not mounted', () => {
|
||||
const anotherRenderFn = jest.fn();
|
||||
const AnotherChart = reactify(anotherRenderFn);
|
||||
const { unmount } = render(<AnotherChart id="test" />);
|
||||
expect(anotherRenderFn).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
test('calls willUnmount hook when it is provided', () => {
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
const AnotherChart = reactify(anotherRenderFn); // enables valid new AnotherChart() call
|
||||
// @ts-expect-error
|
||||
new AnotherChart({ id: 'test' }).execute();
|
||||
expect(anotherRenderFn).not.toHaveBeenCalled();
|
||||
});
|
||||
test('calls willUnmount hook when it is provided', () =>
|
||||
new Promise(done => {
|
||||
const { unmount } = render(<AnotherTestComponent />);
|
||||
setTimeout(() => {
|
||||
unmount();
|
||||
expect(willUnmountCb).toHaveBeenCalledTimes(1);
|
||||
done(undefined);
|
||||
}, 20);
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ test('format milliseconds in human readable format with default options', () =>
|
||||
});
|
||||
test('format seconds in human readable format with default options', () => {
|
||||
const formatter = createDurationFormatter({ multiplier: 1000 });
|
||||
expect(formatter(-0.5)).toBe('0s');
|
||||
expect(formatter(-0.5)).toBe('-0s');
|
||||
expect(formatter(0.5)).toBe('0s');
|
||||
expect(formatter(1)).toBe('1s');
|
||||
expect(formatter(30)).toBe('30s');
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.4.8",
|
||||
"dompurify": "^3.4.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function transformProps(
|
||||
...DEFAULT_RADAR_FORM_DATA,
|
||||
...formData,
|
||||
};
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks ?? {};
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const denormalizedSeriesValues: SeriesNormalizedMap = {};
|
||||
@@ -140,7 +140,7 @@ export default function transformProps(
|
||||
|
||||
const metricLabels = metrics.map(getMetricLabel);
|
||||
|
||||
const metricsWithCustomBounds = new Set<string>(
|
||||
const metricsWithCustomBounds = new Set(
|
||||
metricLabels.filter(metricLabel => {
|
||||
const config = columnConfig?.[metricLabel];
|
||||
const hasMax = !!isDefined(config?.radarMetricMaxValue);
|
||||
@@ -358,7 +358,6 @@ export default function transformProps(
|
||||
metricLabels,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
numberFormatter,
|
||||
);
|
||||
|
||||
const echartOptions: EChartsCoreOption = {
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { NumberFormatter } from '@superset-ui/core';
|
||||
|
||||
/*
|
||||
function for finding the max metric values among all series data for Radar Chart
|
||||
*/
|
||||
@@ -49,7 +47,7 @@ interface TooltipParams {
|
||||
|
||||
interface TooltipMetricValue {
|
||||
metric: string;
|
||||
value: number | string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const renderNormalizedTooltip = (
|
||||
@@ -57,7 +55,6 @@ export const renderNormalizedTooltip = (
|
||||
metrics: string[],
|
||||
getDenormalizedValue: (seriesName: string, value: string) => number,
|
||||
metricsWithCustomBounds: Set<string>,
|
||||
formatter?: NumberFormatter,
|
||||
): string => {
|
||||
const { color, name = '', value: values } = params;
|
||||
const seriesName = name || 'series0';
|
||||
@@ -73,7 +70,7 @@ export const renderNormalizedTooltip = (
|
||||
|
||||
return {
|
||||
metric,
|
||||
value: formatter ? formatter(originalValue) : originalValue,
|
||||
value: originalValue,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getNumberFormatter } from '@superset-ui/core';
|
||||
import { renderNormalizedTooltip } from '../../src/Radar/utils';
|
||||
|
||||
describe('renderNormalizedTooltip', () => {
|
||||
const mockGetDenormalizedValue = jest.fn((_, value) => Number(value));
|
||||
const metrics = ['metric1', 'metric2'];
|
||||
const params = {
|
||||
color: 'red',
|
||||
name: 'series1',
|
||||
value: [100, 200],
|
||||
};
|
||||
const metricsWithCustomBounds = new Set<string>();
|
||||
|
||||
test('should render tooltip with formatted values when formatter is provided', () => {
|
||||
const formatter = getNumberFormatter(',.2f');
|
||||
const tooltip = renderNormalizedTooltip(
|
||||
params,
|
||||
metrics,
|
||||
mockGetDenormalizedValue,
|
||||
metricsWithCustomBounds,
|
||||
formatter,
|
||||
);
|
||||
expect(tooltip).toContain(formatter(100));
|
||||
expect(tooltip).toContain(formatter(200));
|
||||
});
|
||||
|
||||
test('should render tooltip with raw values when formatter is not provided', () => {
|
||||
const tooltip = renderNormalizedTooltip(
|
||||
params,
|
||||
metrics,
|
||||
mockGetDenormalizedValue,
|
||||
metricsWithCustomBounds,
|
||||
);
|
||||
expect(tooltip).toContain('100');
|
||||
expect(tooltip).toContain('200');
|
||||
});
|
||||
});
|
||||
@@ -20,10 +20,6 @@ import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { Map as MapLibreMap } from 'react-map-gl/maplibre';
|
||||
import { Map as MapboxMap } from 'react-map-gl/mapbox';
|
||||
import { WebMercatorViewport } from '@math.gl/web-mercator';
|
||||
import {
|
||||
resolveMapStyle,
|
||||
type ResolvedMapStyle,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import ScatterPlotOverlay from './components/ScatterPlotOverlay';
|
||||
@@ -164,10 +160,7 @@ function MapLibre({
|
||||
const clusters = clusterer.getClusters(bbox, Math.round(viewport.zoom));
|
||||
|
||||
const theme = useTheme();
|
||||
const resolvedMapStyle: ResolvedMapStyle =
|
||||
mapProvider === 'mapbox'
|
||||
? mapStyle || DEFAULT_MAP_STYLE
|
||||
: resolveMapStyle(mapStyle, DEFAULT_MAP_STYLE);
|
||||
const resolvedMapStyle = mapStyle || DEFAULT_MAP_STYLE;
|
||||
const mapboxApiKey = mapProvider === 'mapbox' ? getMapboxApiKey() : '';
|
||||
|
||||
if (mapProvider === 'mapbox' && !mapboxApiKey) {
|
||||
|
||||
@@ -19,19 +19,11 @@
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
columnChoices,
|
||||
ControlPanelState,
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
sharedControls,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { QueryFormData } from '@superset-ui/core';
|
||||
import type { MapProvider } from '@superset-ui/core/utils/mapStyles';
|
||||
import { getDefaultMapRenderer } from '@superset-ui/core/utils/mapStyles';
|
||||
import {
|
||||
getPointClusterMapRendererProps,
|
||||
POINT_CLUSTER_MAPLIBRE_STYLE_CHOICES,
|
||||
} from './utils/mapControls';
|
||||
|
||||
const columnsConfig = sharedControls.entity;
|
||||
|
||||
@@ -43,11 +35,6 @@ const colorChoices = [
|
||||
['#dc143c', t('Crimson')],
|
||||
['#228b22', t('Forest Green')],
|
||||
];
|
||||
type MapStyleVisibilityProps = {
|
||||
controls?: {
|
||||
map_renderer?: { value?: unknown };
|
||||
};
|
||||
};
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -122,7 +109,7 @@ const config: ControlPanelConfig = {
|
||||
'Either a numerical column or `Auto`, which scales the point based ' +
|
||||
'on the largest cluster',
|
||||
),
|
||||
mapStateToProps: (state: ControlPanelState) => {
|
||||
mapStateToProps: (state: any) => {
|
||||
const datasourceChoices = columnChoices(state.datasource);
|
||||
const choices: [string, string][] = [['Auto', t('Auto')]];
|
||||
return {
|
||||
@@ -169,7 +156,7 @@ const config: ControlPanelConfig = {
|
||||
'Non-numerical columns will be used to label points. ' +
|
||||
'Leave empty to get a count of points in each cluster.',
|
||||
),
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
mapStateToProps: (state: any) => ({
|
||||
choices: columnChoices(state.datasource),
|
||||
}),
|
||||
},
|
||||
@@ -213,17 +200,14 @@ const config: ControlPanelConfig = {
|
||||
label: t('Map Renderer'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
options: getPointClusterMapRendererProps().options,
|
||||
choices: [
|
||||
['maplibre', t('MapLibre (open-source)')],
|
||||
['mapbox', t('Mapbox (API key required)')],
|
||||
],
|
||||
default: 'maplibre',
|
||||
description: t(
|
||||
'MapLibre is open-source and requires no API key. Mapbox requires MAPBOX_API_KEY to be configured on the server.',
|
||||
),
|
||||
mapStateToProps: (state: ControlPanelState) => ({
|
||||
...getPointClusterMapRendererProps(
|
||||
state.form_data?.map_renderer as MapProvider | undefined,
|
||||
),
|
||||
default: getDefaultMapRenderer(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -236,13 +220,30 @@ const config: ControlPanelConfig = {
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
choices: POINT_CLUSTER_MAPLIBRE_STYLE_CHOICES,
|
||||
choices: [
|
||||
[
|
||||
'https://tiles.openfreemap.org/styles/liberty',
|
||||
t('Liberty (OpenFreeMap)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
t('Light (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
||||
t('Dark (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
|
||||
t('Streets (Carto)'),
|
||||
],
|
||||
],
|
||||
default: 'https://tiles.openfreemap.org/styles/liberty',
|
||||
description: t(
|
||||
'Base layer map style. See MapLibre documentation: %s',
|
||||
'https://maplibre.org/maplibre-style-spec/',
|
||||
),
|
||||
visibility: ({ controls }: MapStyleVisibilityProps) =>
|
||||
visibility: ({ controls }: any) =>
|
||||
controls?.map_renderer?.value !== 'mapbox',
|
||||
},
|
||||
},
|
||||
@@ -271,7 +272,7 @@ const config: ControlPanelConfig = {
|
||||
description: t(
|
||||
'Base layer map style. Accepts a Mapbox style URL (mapbox://styles/...).',
|
||||
),
|
||||
visibility: ({ controls }: MapStyleVisibilityProps) =>
|
||||
visibility: ({ controls }: any) =>
|
||||
controls?.map_renderer?.value === 'mapbox',
|
||||
},
|
||||
},
|
||||
@@ -386,7 +387,7 @@ const config: ControlPanelConfig = {
|
||||
),
|
||||
},
|
||||
},
|
||||
formDataOverrides: (formData: QueryFormData) => ({
|
||||
formDataOverrides: (formData: any) => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
}),
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Supercluster, {
|
||||
type Options as SuperclusterOptions,
|
||||
} from 'supercluster';
|
||||
import { ChartProps, getMapProviderMapStyle } from '@superset-ui/core';
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapLibre';
|
||||
import roundDecimal from './utils/roundDecimal';
|
||||
@@ -152,7 +152,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
map_renderer: mapProvider,
|
||||
maplibre_style: maplibreStyle,
|
||||
mapbox_style: mapboxStyle = '',
|
||||
map_style: legacyMapStyle,
|
||||
pandas_aggfunc: pandasAggfunc,
|
||||
point_radius: pointRadius,
|
||||
point_radius_unit: pointRadiusUnit,
|
||||
@@ -243,12 +242,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
const clusterer = new Supercluster<PointProperties, ClusterProperties>(opts);
|
||||
// Disable strict typecheck on load since Supercluster typings have namespace issues with esModuleInterop
|
||||
clusterer.load(geoJSON.features as any);
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider,
|
||||
maplibreStyle,
|
||||
mapboxStyle,
|
||||
legacyMapStyle,
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
@@ -258,8 +251,11 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
clusterer,
|
||||
globalOpacity: Math.min(1, Math.max(0, toFiniteNumber(globalOpacity) ?? 1)),
|
||||
hasCustomMetric,
|
||||
mapProvider: selectedMap.mapProvider,
|
||||
mapStyle: selectedMap.mapStyle,
|
||||
mapProvider,
|
||||
mapStyle:
|
||||
mapProvider === 'mapbox'
|
||||
? (mapboxStyle as string)
|
||||
: (maplibreStyle as string),
|
||||
onViewportChange({
|
||||
latitude,
|
||||
longitude,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
getMapRendererOptions,
|
||||
OSM_TILE_STYLE_URL,
|
||||
type MapRendererOption,
|
||||
type MapProvider,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import { hasMapboxApiKey } from './mapbox';
|
||||
|
||||
export const POINT_CLUSTER_MAPLIBRE_STYLE_CHOICES = [
|
||||
['https://tiles.openfreemap.org/styles/liberty', t('Liberty (OpenFreeMap)')],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
t('Light (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
||||
t('Dark (Carto)'),
|
||||
],
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
|
||||
t('Streets (Carto)'),
|
||||
],
|
||||
[OSM_TILE_STYLE_URL, t('Streets (OSM)')],
|
||||
];
|
||||
|
||||
export function getPointClusterMapRendererProps(currentValue?: MapProvider) {
|
||||
const hasKey = hasMapboxApiKey();
|
||||
return {
|
||||
options: getMapRendererOptions({
|
||||
hasMapboxKey: hasKey,
|
||||
currentValue,
|
||||
}).map((option: MapRendererOption) => ({
|
||||
...option,
|
||||
label:
|
||||
option.value === 'maplibre'
|
||||
? t('MapLibre (open-source)')
|
||||
: t('Mapbox (API key required)'),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -17,15 +17,19 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getMapboxApiKeyFromBootstrap,
|
||||
hasMapboxApiKey as hasBootstrapMapboxApiKey,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
|
||||
export function getMapboxApiKey(): string {
|
||||
return getMapboxApiKeyFromBootstrap();
|
||||
}
|
||||
|
||||
export function hasMapboxApiKey(): boolean {
|
||||
return hasBootstrapMapboxApiKey();
|
||||
if (typeof document === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const appContainer = document.getElementById('app');
|
||||
const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
|
||||
if (dataBootstrap) {
|
||||
const bootstrapData = JSON.parse(dataBootstrap);
|
||||
return bootstrapData?.common?.conf?.MAPBOX_API_KEY || '';
|
||||
}
|
||||
} catch {
|
||||
// If bootstrap data is unavailable or malformed, return empty string
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -18,12 +18,7 @@
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
OSM_TILE_ATTRIBUTION,
|
||||
OSM_TILE_STYLE_URL,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
// Capture the most recent viewport props passed to the Map component
|
||||
let lastMapProps: Record<string, unknown> = {};
|
||||
@@ -96,7 +91,6 @@ const defaultProps = {
|
||||
|
||||
beforeEach(() => {
|
||||
lastMapProps = {};
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
mockFitBounds.mockImplementation(
|
||||
(
|
||||
@@ -189,65 +183,6 @@ test('passes globalOpacity to ScatterPlotOverlay', () => {
|
||||
expect(overlay!.getAttribute('data-opacity')).toBe('0.5');
|
||||
});
|
||||
|
||||
test('converts OSM raster tile templates into MapLibre style objects', () => {
|
||||
render(<MapLibre {...defaultProps} mapStyle={OSM_TILE_STYLE_URL} />);
|
||||
|
||||
expect(lastMapProps.mapStyle).toEqual({
|
||||
version: 8,
|
||||
sources: {
|
||||
'osm-raster-tiles': {
|
||||
type: 'raster',
|
||||
tiles: [OSM_TILE_STYLE_URL],
|
||||
tileSize: 256,
|
||||
attribution: OSM_TILE_ATTRIBUTION,
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'osm-raster-layer',
|
||||
type: 'raster',
|
||||
source: 'osm-raster-tiles',
|
||||
minzoom: 0,
|
||||
maxzoom: 22,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the missing Mapbox key signal for saved Mapbox charts', () => {
|
||||
render(
|
||||
<MapLibre
|
||||
{...defaultProps}
|
||||
mapProvider="mapbox"
|
||||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Mapbox requires a MAPBOX_API_KEY to be configured on the server.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(lastMapProps.mapStyle).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passes Mapbox styles through when a key exists', () => {
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
|
||||
common: { conf: { MAPBOX_API_KEY: 'pk.test' } },
|
||||
})}'></div>`;
|
||||
|
||||
render(
|
||||
<MapLibre
|
||||
{...defaultProps}
|
||||
mapProvider="mapbox"
|
||||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastMapProps.mapStyle).toBe('mapbox://styles/mapbox/dark-v11');
|
||||
expect(lastMapProps.mapboxAccessToken).toBe('pk.test');
|
||||
});
|
||||
|
||||
test('handles undefined bounds gracefully', () => {
|
||||
render(<MapLibre {...defaultProps} bounds={undefined} />);
|
||||
expect(lastMapProps.longitude).toBe(0);
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
* under the License.
|
||||
*/
|
||||
import type {
|
||||
ControlPanelState,
|
||||
ControlPanelConfig,
|
||||
CustomControlItem,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { OSM_TILE_STYLE_URL } from '@superset-ui/core/utils/mapStyles';
|
||||
import controlPanel from '../src/controlPanel';
|
||||
|
||||
type ControlConfig = Required<CustomControlItem['config']>;
|
||||
@@ -56,27 +54,6 @@ function getControl(
|
||||
return item;
|
||||
}
|
||||
|
||||
type RendererControlConfig = ControlConfig & {
|
||||
mapStateToProps: (state: ControlPanelState) => {
|
||||
options?: unknown;
|
||||
warning?: string;
|
||||
default?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const setBootstrap = (conf: Record<string, unknown>) => {
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
|
||||
common: { conf },
|
||||
})}'></div>`;
|
||||
};
|
||||
|
||||
const getMapRendererProps = (value?: string) =>
|
||||
(
|
||||
getControl(controlPanel, 'map_renderer').config as RendererControlConfig
|
||||
).mapStateToProps({
|
||||
form_data: { map_renderer: value },
|
||||
} as unknown as ControlPanelState);
|
||||
|
||||
test('viewport controls default to empty values and rerender without query refresh', () => {
|
||||
const longitudeControl = getControl(controlPanel, 'viewport_longitude');
|
||||
const latitudeControl = getControl(controlPanel, 'viewport_latitude');
|
||||
@@ -102,63 +79,3 @@ test('opacity control rerenders immediately when changed', () => {
|
||||
expect(opacityControl.config.renderTrigger).toBe(true);
|
||||
expect(opacityControl.config.isFloat).toBe(true);
|
||||
});
|
||||
|
||||
test('MapLibre style choices expose Streets (OSM)', () => {
|
||||
expect(
|
||||
getControl(controlPanel, 'maplibre_style').config.choices,
|
||||
).toContainEqual([OSM_TILE_STYLE_URL, 'Streets (OSM)']);
|
||||
});
|
||||
|
||||
test('map renderer hides Mapbox when no key exists for new selections', () => {
|
||||
setBootstrap({});
|
||||
|
||||
const props = getMapRendererProps('maplibre');
|
||||
|
||||
expect(props.options).toEqual([
|
||||
{ value: 'maplibre', label: 'MapLibre (open-source)' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('map renderer keeps saved Mapbox visible while disabled without a key', () => {
|
||||
setBootstrap({});
|
||||
|
||||
const props = getMapRendererProps('mapbox');
|
||||
|
||||
expect(props.options).toContainEqual({
|
||||
value: 'mapbox',
|
||||
label: 'Mapbox (API key required)',
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('map renderer enables Mapbox when a key exists', () => {
|
||||
setBootstrap({ MAPBOX_API_KEY: 'pk.test' });
|
||||
|
||||
const props = getMapRendererProps('maplibre');
|
||||
|
||||
expect(props.options).toEqual([
|
||||
{ value: 'maplibre', label: 'MapLibre (open-source)' },
|
||||
{ value: 'mapbox', label: 'Mapbox (API key required)' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('map renderer keeps the original explanatory description', () => {
|
||||
expect(getControl(controlPanel, 'map_renderer').config.description).toBe(
|
||||
'MapLibre is open-source and requires no API key. Mapbox requires MAPBOX_API_KEY to be configured on the server.',
|
||||
);
|
||||
});
|
||||
|
||||
test('map renderer defaults to configured Mapbox when a key exists', () => {
|
||||
setBootstrap({
|
||||
DEFAULT_MAP_RENDERER: 'mapbox',
|
||||
MAPBOX_API_KEY: 'pk.test',
|
||||
});
|
||||
|
||||
expect(getMapRendererProps('maplibre').default).toBe('mapbox');
|
||||
});
|
||||
|
||||
test('map renderer falls back from configured Mapbox default without a key', () => {
|
||||
setBootstrap({ DEFAULT_MAP_RENDERER: 'mapbox' });
|
||||
|
||||
expect(getMapRendererProps('maplibre').default).toBe('maplibre');
|
||||
});
|
||||
|
||||
@@ -34,8 +34,6 @@ import transformProps from '../src/transformProps';
|
||||
|
||||
type TransformPropsResult = {
|
||||
globalOpacity?: number;
|
||||
mapProvider?: string;
|
||||
mapStyle?: string;
|
||||
onViewportChange?: (viewport: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
@@ -217,41 +215,6 @@ test('passes through numeric values unchanged', () => {
|
||||
expect(result.globalOpacity).toBe(0.8);
|
||||
});
|
||||
|
||||
test('uses the MapLibre style when maplibre renderer is selected', () => {
|
||||
const result = getTransformPropsResult({
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: 'https://example.com/maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/dark-v11',
|
||||
});
|
||||
|
||||
expect(result.mapProvider).toBe('maplibre');
|
||||
expect(result.mapStyle).toBe('https://example.com/maplibre-style.json');
|
||||
});
|
||||
|
||||
test('uses legacy non-Mapbox style for MapLibre when provider style is absent', () => {
|
||||
const result = getTransformPropsResult({
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: undefined,
|
||||
mapbox_style: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
});
|
||||
|
||||
expect(result.mapProvider).toBe('maplibre');
|
||||
expect(result.mapStyle).toBe(
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses the Mapbox style when mapbox renderer is selected', () => {
|
||||
const result = getTransformPropsResult({
|
||||
map_renderer: 'mapbox',
|
||||
maplibre_style: 'https://example.com/maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/dark-v11',
|
||||
});
|
||||
|
||||
expect(result.mapProvider).toBe('mapbox');
|
||||
expect(result.mapStyle).toBe('mapbox://styles/mapbox/dark-v11');
|
||||
});
|
||||
|
||||
test('calls onError and falls back to black for invalid color', () => {
|
||||
const onError = jest.fn();
|
||||
const chartProps = new ChartProps({
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@deck.gl/extensions": "~9.2.9",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mapbox": "~9.3.3",
|
||||
"@deck.gl/mapbox": "~9.3.2",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
getMapProviderMapStyle,
|
||||
} from '@superset-ui/core';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
@@ -319,12 +318,6 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
},
|
||||
[categories],
|
||||
);
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider: props.formData.map_renderer,
|
||||
maplibreStyle: props.formData.maplibre_style,
|
||||
mapboxStyle: props.formData.mapbox_style,
|
||||
legacyMapStyle: props.formData.map_style,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -333,8 +326,14 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
viewport={viewport}
|
||||
layers={getLayers()}
|
||||
setControlValue={props.setControlValue}
|
||||
mapStyle={selectedMap.mapStyle}
|
||||
mapProvider={selectedMap.mapProvider}
|
||||
mapStyle={
|
||||
props.formData.map_renderer === 'mapbox'
|
||||
? props.formData.mapbox_style
|
||||
: props.formData.maplibre_style
|
||||
}
|
||||
mapProvider={
|
||||
props.formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'
|
||||
}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ComponentProps, createRef, ReactNode } from 'react';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
|
||||
import {
|
||||
OSM_TILE_ATTRIBUTION,
|
||||
OSM_TILE_STYLE_URL,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { DeckGLContainer, DeckGLContainerHandle } from './DeckGLContainer';
|
||||
|
||||
jest.mock('react-map-gl/maplibre', () => ({
|
||||
Map: ({
|
||||
children,
|
||||
mapStyle,
|
||||
onMove,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
mapStyle: unknown;
|
||||
onMove: (evt: { viewState: Record<string, number> }) => void;
|
||||
}) => (
|
||||
<div data-test="maplibre-map" data-map-style={JSON.stringify(mapStyle)}>
|
||||
<button
|
||||
type="button"
|
||||
data-test="maplibre-move"
|
||||
onClick={() =>
|
||||
onMove({ viewState: { longitude: 1, latitude: 2, zoom: 3 } })
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('react-map-gl/mapbox', () => ({
|
||||
Map: ({ children, mapStyle }: { children: ReactNode; mapStyle: unknown }) => (
|
||||
<div data-test="mapbox-map" data-map-style={JSON.stringify(mapStyle)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('mapbox-gl', () => ({ accessToken: '' }));
|
||||
|
||||
jest.mock(
|
||||
'./components/DeckGLOverlayMapLibre',
|
||||
() =>
|
||||
({ layers }: { layers: unknown[] }) => (
|
||||
<div data-test="maplibre-overlay" data-layers-count={layers.length} />
|
||||
),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'./components/DeckGLOverlayMapbox',
|
||||
() =>
|
||||
({ layers }: { layers: unknown[] }) => (
|
||||
<div data-test="mapbox-overlay" data-layers-count={layers.length} />
|
||||
),
|
||||
);
|
||||
|
||||
jest.mock('./components/Tooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({ variant = 'default' }: { variant?: 'default' | 'custom' }) => (
|
||||
<div data-test={`tooltip-${variant}`} />
|
||||
),
|
||||
}));
|
||||
|
||||
const baseProps = {
|
||||
viewport: { longitude: 0, latitude: 0, zoom: 1, bearing: 0, pitch: 0 },
|
||||
width: 800,
|
||||
height: 600,
|
||||
layers: [],
|
||||
};
|
||||
|
||||
const renderContainer = (
|
||||
props: Partial<ComponentProps<typeof DeckGLContainer>>,
|
||||
) =>
|
||||
render(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<DeckGLContainer {...baseProps} {...props} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('DeckGLContainer converts OSM raster tile templates into MapLibre style objects', () => {
|
||||
renderContainer({ mapProvider: 'maplibre', mapStyle: OSM_TILE_STYLE_URL });
|
||||
|
||||
const style = JSON.parse(
|
||||
screen.getByTestId('maplibre-map').getAttribute('data-map-style') || '{}',
|
||||
);
|
||||
|
||||
expect(style.sources['osm-raster-tiles']).toEqual({
|
||||
type: 'raster',
|
||||
tiles: [OSM_TILE_STYLE_URL],
|
||||
tileSize: 256,
|
||||
attribution: OSM_TILE_ATTRIBUTION,
|
||||
});
|
||||
expect(style.layers[0]).toMatchObject({
|
||||
id: 'osm-raster-layer',
|
||||
type: 'raster',
|
||||
source: 'osm-raster-tiles',
|
||||
});
|
||||
});
|
||||
|
||||
test('DeckGLContainer passes style JSON URLs through to MapLibre', () => {
|
||||
const styleUrl = 'https://example.com/styles/custom-style.json';
|
||||
|
||||
renderContainer({ mapProvider: 'maplibre', mapStyle: styleUrl });
|
||||
|
||||
expect(screen.getByTestId('maplibre-map')).toHaveAttribute(
|
||||
'data-map-style',
|
||||
JSON.stringify(styleUrl),
|
||||
);
|
||||
});
|
||||
|
||||
test('DeckGLContainer keeps the missing Mapbox key signal for saved Mapbox charts', () => {
|
||||
renderContainer({
|
||||
mapProvider: 'mapbox',
|
||||
mapStyle: 'mapbox://styles/mapbox/dark-v9',
|
||||
mapboxApiKey: '',
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Mapbox requires a MAPBOX_API_KEY to be configured on the server.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('maplibre-map')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('mapbox-map')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('DeckGLContainer passes Mapbox styles through when a key exists', () => {
|
||||
renderContainer({
|
||||
mapProvider: 'mapbox',
|
||||
mapStyle: 'mapbox://styles/mapbox/dark-v9',
|
||||
mapboxApiKey: 'pk.test',
|
||||
});
|
||||
|
||||
expect(mapboxgl.accessToken).toBe('pk.test');
|
||||
expect(screen.getByTestId('mapbox-map')).toHaveAttribute(
|
||||
'data-map-style',
|
||||
JSON.stringify('mapbox://styles/mapbox/dark-v9'),
|
||||
);
|
||||
});
|
||||
|
||||
test('DeckGLContainer supports layer factories for MapLibre overlays', () => {
|
||||
const layer = { id: 'layer-1' } as unknown as Layer;
|
||||
const layerFactory = () => layer;
|
||||
|
||||
renderContainer({ mapProvider: 'maplibre', layers: [layerFactory] });
|
||||
|
||||
expect(screen.getByTestId('maplibre-overlay')).toHaveAttribute(
|
||||
'data-layers-count',
|
||||
'1',
|
||||
);
|
||||
});
|
||||
|
||||
test('DeckGLContainer updates viewport controls after map movement is throttled', () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(1000);
|
||||
const setControlValue = jest.fn();
|
||||
|
||||
renderContainer({ mapProvider: 'maplibre', setControlValue });
|
||||
fireEvent.click(screen.getByTestId('maplibre-move'));
|
||||
|
||||
jest.setSystemTime(1301);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250);
|
||||
});
|
||||
|
||||
expect(setControlValue).toHaveBeenCalledWith('viewport', {
|
||||
longitude: 1,
|
||||
latitude: 2,
|
||||
zoom: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('DeckGLContainer suppresses the native context menu', () => {
|
||||
renderContainer({ mapProvider: 'maplibre' });
|
||||
|
||||
const event = new MouseEvent('contextmenu', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
|
||||
const stopPropagationSpy = jest.spyOn(event, 'stopPropagation');
|
||||
|
||||
screen.getByTestId('maplibre-map').parentElement?.dispatchEvent(event);
|
||||
|
||||
expect(preventDefaultSpy).toHaveBeenCalled();
|
||||
expect(stopPropagationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('DeckGLContainer renders default and custom tooltip variants through its ref', () => {
|
||||
const ref = createRef<DeckGLContainerHandle>();
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<DeckGLContainer {...baseProps} mapProvider="maplibre" ref={ref} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
ref.current?.setTooltip({ x: 0, y: 0, content: 'Default tooltip' });
|
||||
});
|
||||
expect(screen.getByTestId('tooltip-default')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
ref.current?.setTooltip({
|
||||
x: 0,
|
||||
y: 0,
|
||||
content: <span data-tooltip-type="custom">Custom tooltip</span>,
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('tooltip-custom')).toBeInTheDocument();
|
||||
});
|
||||
@@ -33,11 +33,6 @@ import { Map as MapboxMap } from 'react-map-gl/mapbox';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import { JsonObject, JsonValue, usePrevious } from '@superset-ui/core';
|
||||
import {
|
||||
resolveMapStyle,
|
||||
type MapProvider,
|
||||
type ResolvedMapStyle,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import { styled, useTheme } from '@apache-superset/core/theme';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import DeckGLOverlayMapLibre from './components/DeckGLOverlayMapLibre';
|
||||
@@ -55,7 +50,7 @@ export type DeckGLContainerProps = {
|
||||
viewport: Viewport;
|
||||
setControlValue?: (control: string, value: JsonValue) => void;
|
||||
mapStyle?: string;
|
||||
mapProvider?: MapProvider;
|
||||
mapProvider?: 'maplibre' | 'mapbox';
|
||||
mapboxApiKey?: string;
|
||||
children?: ReactNode;
|
||||
width: number;
|
||||
@@ -128,9 +123,7 @@ export const DeckGLContainer = memo(
|
||||
const theme = useTheme();
|
||||
const { children = null, height, width } = props;
|
||||
const isMapbox = props.mapProvider === 'mapbox';
|
||||
const mapStyle: ResolvedMapStyle = isMapbox
|
||||
? props.mapStyle || DEFAULT_MAP_STYLE
|
||||
: resolveMapStyle(props.mapStyle, DEFAULT_MAP_STYLE);
|
||||
const mapStyle = props.mapStyle || DEFAULT_MAP_STYLE;
|
||||
|
||||
if (isMapbox && !props.mapboxApiKey) {
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
QueryFormData,
|
||||
QueryObjectFilterClause,
|
||||
SupersetClient,
|
||||
getMapProviderMapStyle,
|
||||
usePrevious,
|
||||
} from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
@@ -398,12 +397,6 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
.filter(layer => layer !== undefined),
|
||||
[layerOrder, subSlicesLayers],
|
||||
);
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider: formData.map_renderer,
|
||||
maplibreStyle: formData.maplibre_style,
|
||||
mapboxStyle: formData.mapbox_style,
|
||||
legacyMapStyle: formData.map_style,
|
||||
});
|
||||
|
||||
return (
|
||||
<MultiWrapper height={height} width={width}>
|
||||
@@ -411,8 +404,12 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
ref={containerRef}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={selectedMap.mapStyle}
|
||||
mapProvider={selectedMap.mapProvider}
|
||||
mapStyle={
|
||||
formData.map_renderer === 'mapbox'
|
||||
? formData.mapbox_style
|
||||
: formData.maplibre_style
|
||||
}
|
||||
mapProvider={formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
setControlValue={setControlValue}
|
||||
onViewportChange={setViewport}
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
FilterState,
|
||||
JsonValue,
|
||||
ContextMenuFilters,
|
||||
getMapProviderMapStyle,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -185,12 +184,6 @@ export function createDeckGLComponent(
|
||||
}, [computeLayers, prevFormData, prevFilterState, prevPayload, props]);
|
||||
|
||||
const { formData, setControlValue, height, width } = props;
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider: formData.map_renderer,
|
||||
maplibreStyle: formData.maplibre_style,
|
||||
mapboxStyle: formData.mapbox_style,
|
||||
legacyMapStyle: formData.map_style,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -198,8 +191,14 @@ export function createDeckGLComponent(
|
||||
ref={containerRef}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={selectedMap.mapStyle}
|
||||
mapProvider={selectedMap.mapProvider}
|
||||
mapStyle={
|
||||
formData.map_renderer === 'mapbox'
|
||||
? formData.mapbox_style
|
||||
: formData.maplibre_style
|
||||
}
|
||||
mapProvider={
|
||||
formData.map_renderer === 'mapbox' ? 'mapbox' : 'maplibre'
|
||||
}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
setControlValue={setControlValue}
|
||||
width={width}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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 { SqlaFormData } from '@superset-ui/core';
|
||||
import {
|
||||
computeGeoJsonTextOptionsFromJsOutput,
|
||||
computeGeoJsonTextOptionsFromFormData,
|
||||
computeGeoJsonIconOptionsFromJsOutput,
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
} from './Geojson';
|
||||
|
||||
jest.mock('react-map-gl/maplibre', () => ({
|
||||
__esModule: true,
|
||||
Map: () => null,
|
||||
useControl: () => null,
|
||||
}));
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => {
|
||||
const input = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
invalidOption: true,
|
||||
};
|
||||
const expectedOutput = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
};
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
label_property_name: 'name',
|
||||
label_color: { r: 1, g: 2, b: 3, a: 1 },
|
||||
label_size: 123,
|
||||
label_size_unit: 'pixels',
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getText: expect.any(Function),
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
getTextSize: 123,
|
||||
textSizeUnits: 'pixels',
|
||||
};
|
||||
|
||||
const actualOutput = computeGeoJsonTextOptionsFromFormData(formData);
|
||||
expect(actualOutput).toEqual(expectedOutput);
|
||||
|
||||
const sampleFeature = { properties: { name: 'Test' } };
|
||||
expect(actualOutput.getText(sampleFeature)).toBe('Test');
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => {
|
||||
const input = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
invalidOption: false,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
};
|
||||
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
icon_size: 123,
|
||||
icon_size_unit: 'pixels',
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: expect.any(Function),
|
||||
getIconSize: 123,
|
||||
iconSizeUnits: 'pixels',
|
||||
};
|
||||
|
||||
const actualOutput = computeGeoJsonIconOptionsFromFormData(formData);
|
||||
expect(actualOutput).toEqual(expectedOutput);
|
||||
|
||||
expect(actualOutput.getIcon()).toEqual({
|
||||
url: 'https://example.com/icon.png',
|
||||
height: 128,
|
||||
width: 128,
|
||||
});
|
||||
});
|
||||
@@ -1,297 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactElement } from 'react';
|
||||
import type { ControlPanelSectionConfig } from '@superset-ui/chart-controls';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render } from '@testing-library/react';
|
||||
import { SqlaFormData } from '@superset-ui/core';
|
||||
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
|
||||
import DeckGLGeoJson, {
|
||||
computeGeoJsonTextOptionsFromJsOutput,
|
||||
computeGeoJsonTextOptionsFromFormData,
|
||||
computeGeoJsonIconOptionsFromJsOutput,
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
getPoints,
|
||||
} from './Geojson';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const mockDeckGLContainerProps: Array<Record<string, unknown>> = [];
|
||||
|
||||
jest.mock('../../DeckGLContainer', () => ({
|
||||
DeckGLContainerStyledWrapper: (props: Record<string, unknown>) => {
|
||||
mockDeckGLContainerProps.push(props);
|
||||
const React = jest.requireActual('react');
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ 'data-testid': 'deckgl-container' },
|
||||
props.children,
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/mapbox', () => ({
|
||||
getMapboxApiKey: () => 'bootstrap-mapbox-key',
|
||||
hasMapboxApiKey: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('react-map-gl/maplibre', () => ({
|
||||
__esModule: true,
|
||||
Map: () => null,
|
||||
useControl: () => null,
|
||||
}));
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => {
|
||||
const input = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
invalidOption: true,
|
||||
};
|
||||
const expectedOutput = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
};
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
label_property_name: 'name',
|
||||
label_color: { r: 1, g: 2, b: 3, a: 1 },
|
||||
label_size: 123,
|
||||
label_size_unit: 'pixels',
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getText: expect.any(Function),
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
getTextSize: 123,
|
||||
textSizeUnits: 'pixels',
|
||||
};
|
||||
|
||||
const actualOutput = computeGeoJsonTextOptionsFromFormData(formData);
|
||||
expect(actualOutput).toEqual(expectedOutput);
|
||||
|
||||
const sampleFeature = { properties: { name: 'Test' } };
|
||||
expect(actualOutput.getText(sampleFeature)).toBe('Test');
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => {
|
||||
const input = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
invalidOption: false,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
};
|
||||
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
icon_size: 123,
|
||||
icon_size_unit: 'pixels',
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: expect.any(Function),
|
||||
getIconSize: 123,
|
||||
iconSizeUnits: 'pixels',
|
||||
};
|
||||
|
||||
const actualOutput = computeGeoJsonIconOptionsFromFormData(formData);
|
||||
expect(actualOutput).toEqual(expectedOutput);
|
||||
|
||||
expect(actualOutput.getIcon()).toEqual({
|
||||
url: 'https://example.com/icon.png',
|
||||
height: 128,
|
||||
width: 128,
|
||||
});
|
||||
});
|
||||
|
||||
test('controlPanel expands Map section so renderer controls are visible', () => {
|
||||
const mapSection = controlPanel.controlPanelSections.find(
|
||||
(
|
||||
section: ControlPanelSectionConfig | null,
|
||||
): section is ControlPanelSectionConfig =>
|
||||
section !== null && section.label === 'Map',
|
||||
);
|
||||
|
||||
expect(mapSection).toBeDefined();
|
||||
expect(mapSection?.expanded).toBe(true);
|
||||
});
|
||||
|
||||
test('getPoints skips malformed GeoJSON entries instead of throwing', () => {
|
||||
const features = [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [1, 2] },
|
||||
properties: {},
|
||||
},
|
||||
[[0, 0]],
|
||||
null,
|
||||
] as unknown as Parameters<typeof getPoints>[0];
|
||||
|
||||
expect(getPoints(features)).toEqual([
|
||||
[1, 2],
|
||||
[1, 2],
|
||||
]);
|
||||
expect(getPoints()).toEqual([]);
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
const geoJsonProps = {
|
||||
formData: {
|
||||
datasource: 'test_datasource',
|
||||
viz_type: 'deck_geojson',
|
||||
slice_id: 1,
|
||||
autozoom: false,
|
||||
map_style: 'legacy-map-style',
|
||||
extruded: false,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
line_width: 1,
|
||||
line_width_unit: 'pixels',
|
||||
point_radius_scale: 1,
|
||||
enable_labels: false,
|
||||
enable_icons: false,
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [0, 0] },
|
||||
properties: { name: 'Test point' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
setControlValue: jest.fn(),
|
||||
viewport: { longitude: 0, latitude: 0, zoom: 1 },
|
||||
onAddFilter: jest.fn(),
|
||||
height: 600,
|
||||
width: 800,
|
||||
filterState: {},
|
||||
onContextMenu: jest.fn(),
|
||||
setDataMask: jest.fn(),
|
||||
emitCrossFilters: false,
|
||||
};
|
||||
|
||||
const lastDeckGLContainerProps = () =>
|
||||
mockDeckGLContainerProps
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(props => props?.viewport !== undefined);
|
||||
|
||||
test('DeckGLGeoJson passes selected MapLibre renderer props to the container', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLGeoJson
|
||||
{...geoJsonProps}
|
||||
formData={{
|
||||
...geoJsonProps.formData,
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: 'https://example.com/maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/dark-v9',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: 'https://example.com/maplibre-style.json',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('DeckGLGeoJson passes selected Mapbox renderer props to the container', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLGeoJson
|
||||
{...geoJsonProps}
|
||||
formData={{
|
||||
...geoJsonProps.formData,
|
||||
map_renderer: 'mapbox',
|
||||
maplibre_style: 'https://example.com/maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/satellite-v9',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'mapbox',
|
||||
mapStyle: 'mapbox://styles/mapbox/satellite-v9',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('DeckGLGeoJson falls back to legacy map_style when provider-specific style is absent', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLGeoJson
|
||||
{...geoJsonProps}
|
||||
formData={{
|
||||
...geoJsonProps.formData,
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: undefined,
|
||||
map_style: 'legacy-map-style',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: 'legacy-map-style',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
SqlaFormData,
|
||||
getMapProviderMapStyle,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -47,7 +46,6 @@ import { Point } from '../../types';
|
||||
import { GetLayerType } from '../../factory';
|
||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||
import { BLACK_COLOR, PRIMARY_COLOR } from '../../utilities/controls';
|
||||
import { getMapboxApiKey } from '../../utils/mapbox';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -359,19 +357,9 @@ export type DeckGLGeoJsonProps = {
|
||||
emitCrossFilters?: boolean;
|
||||
};
|
||||
|
||||
export function getPoints(data?: Point[]) {
|
||||
if (!Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getPoints(data: Point[]) {
|
||||
return data.reduce((acc: Array<any>, feature: any) => {
|
||||
let bounds;
|
||||
try {
|
||||
bounds = geojsonExtent(feature);
|
||||
} catch {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const bounds = geojsonExtent(feature);
|
||||
if (bounds) {
|
||||
return [...acc, [bounds[0], bounds[1]], [bounds[2], bounds[3]]];
|
||||
}
|
||||
@@ -394,13 +382,13 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
|
||||
const viewport: Viewport = useMemo(() => {
|
||||
if (formData.autozoom) {
|
||||
const points = getPoints(payload?.data?.features);
|
||||
const points = getPoints(payload.data.features) || [];
|
||||
|
||||
if (points.length) {
|
||||
return fitViewport(props.viewport, {
|
||||
width,
|
||||
height,
|
||||
points,
|
||||
points: getPoints(payload.data.features) || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -424,21 +412,12 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
emitCrossFilters: props.emitCrossFilters,
|
||||
});
|
||||
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider: formData.map_renderer,
|
||||
maplibreStyle: formData.maplibre_style,
|
||||
mapboxStyle: formData.mapbox_style,
|
||||
legacyMapStyle: formData.map_style,
|
||||
});
|
||||
|
||||
return (
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapProvider={selectedMap.mapProvider}
|
||||
mapStyle={selectedMap.mapStyle}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
mapStyle={formData.map_style}
|
||||
setControlValue={setControlValue}
|
||||
height={height}
|
||||
width={width}
|
||||
|
||||
@@ -82,7 +82,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ReactElement } from 'react';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render, screen } from '@testing-library/react';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
@@ -34,23 +33,10 @@ const mockGetColorBreakpointsBuckets = jest.spyOn(
|
||||
);
|
||||
|
||||
// Mock DeckGL container and Legend
|
||||
const mockDeckGLContainerProps: Array<Record<string, unknown>> = [];
|
||||
|
||||
jest.mock('../../DeckGLContainer', () => ({
|
||||
DeckGLContainerStyledWrapper: (props: Record<string, unknown>) => {
|
||||
mockDeckGLContainerProps.push(props);
|
||||
const React = jest.requireActual('react');
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ 'data-testid': 'deckgl-container' },
|
||||
props.children,
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/mapbox', () => ({
|
||||
getMapboxApiKey: () => 'bootstrap-mapbox-key',
|
||||
hasMapboxApiKey: () => true,
|
||||
DeckGLContainerStyledWrapper: ({ children }: any) => (
|
||||
<div data-testid="deckgl-container">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/Legend', () => ({ categories, position }: any) => (
|
||||
@@ -123,95 +109,6 @@ const mockProps = {
|
||||
emitCrossFilters: false,
|
||||
};
|
||||
|
||||
describe('DeckGLPolygon renderer propagation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetBuckets.mockReturnValue({});
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
const lastDeckGLContainerProps = () =>
|
||||
mockDeckGLContainerProps
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(props => props?.viewport !== undefined);
|
||||
|
||||
test('passes selected MapLibre renderer props to the container', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLPolygon
|
||||
{...mockProps}
|
||||
formData={{
|
||||
...mockProps.formData,
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: 'https://example.com/polygon-maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/dark-v9',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: 'https://example.com/polygon-maplibre-style.json',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('passes selected Mapbox renderer props to the container', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLPolygon
|
||||
{...mockProps}
|
||||
formData={{
|
||||
...mockProps.formData,
|
||||
map_renderer: 'mapbox',
|
||||
maplibre_style: 'https://example.com/polygon-maplibre-style.json',
|
||||
mapbox_style: 'mapbox://styles/mapbox/satellite-v9',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'mapbox',
|
||||
mapStyle: 'mapbox://styles/mapbox/satellite-v9',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to legacy map_style when provider-specific style is absent', () => {
|
||||
mockDeckGLContainerProps.length = 0;
|
||||
|
||||
renderWithTheme(
|
||||
<DeckGLPolygon
|
||||
{...mockProps}
|
||||
formData={{
|
||||
...mockProps.formData,
|
||||
map_renderer: 'maplibre',
|
||||
maplibre_style: undefined,
|
||||
map_style: 'legacy-map-style',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastDeckGLContainerProps()).toEqual(
|
||||
expect.objectContaining({
|
||||
mapProvider: 'maplibre',
|
||||
mapStyle: 'legacy-map-style',
|
||||
mapboxApiKey: 'bootstrap-mapbox-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeckGLPolygon bucket generation logic', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -222,7 +119,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('should use getBuckets for linear_palette color scheme', () => {
|
||||
@@ -330,7 +227,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
|
||||
mockGetColorBreakpointsBuckets.mockReturnValue({});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('handles empty features data gracefully', () => {
|
||||
@@ -394,7 +291,7 @@ describe('DeckGLPolygon Legend Integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const renderWithTheme = (component: ReactElement) =>
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
test('renders legend with non-empty categories when metric and linear_palette are defined', () => {
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
getMapProviderMapStyle,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import { PolygonLayer } from '@deck.gl/layers';
|
||||
@@ -58,7 +57,6 @@ import { TooltipProps } from '../../components/Tooltip';
|
||||
import { GetLayerType } from '../../factory';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
import { DEFAULT_DECKGL_COLOR } from '../../utilities/Shared_DeckGL';
|
||||
import { getMapboxApiKey } from '../../utils/mapbox';
|
||||
import {
|
||||
createTooltipContent,
|
||||
CommonTooltipRows,
|
||||
@@ -341,12 +339,6 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints
|
||||
? getColorBreakpointsBuckets(formData.color_breakpoints)
|
||||
: getBuckets(formData, payload.data.features, accessor);
|
||||
const selectedMap = getMapProviderMapStyle({
|
||||
mapProvider: formData.map_renderer,
|
||||
maplibreStyle: formData.maplibre_style,
|
||||
mapboxStyle: formData.mapbox_style,
|
||||
legacyMapStyle: formData.map_style,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -355,9 +347,7 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
viewport={viewport}
|
||||
layers={getLayers()}
|
||||
setControlValue={setControlValue}
|
||||
mapProvider={selectedMap.mapProvider}
|
||||
mapStyle={selectedMap.mapStyle}
|
||||
mapboxApiKey={getMapboxApiKey()}
|
||||
mapStyle={formData.map_style}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
/>
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ControlPanelState } from '@superset-ui/chart-controls';
|
||||
import { OSM_TILE_STYLE_URL } from '@superset-ui/core/utils/mapStyles';
|
||||
import { mapProvider, maplibreStyle } from './Shared_DeckGL';
|
||||
|
||||
const setBootstrap = ({
|
||||
conf = {},
|
||||
deckglTiles,
|
||||
}: {
|
||||
conf?: Record<string, unknown>;
|
||||
deckglTiles?: unknown;
|
||||
}) => {
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
|
||||
common: {
|
||||
conf,
|
||||
...(deckglTiles === undefined ? {} : { deckgl_tiles: deckglTiles }),
|
||||
},
|
||||
})}'></div>`;
|
||||
};
|
||||
|
||||
type MapProviderControlConfig = typeof mapProvider.config & {
|
||||
mapStateToProps: (state: ControlPanelState) => {
|
||||
options?: unknown;
|
||||
warning?: string;
|
||||
default?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const getMapProviderProps = (value?: string) =>
|
||||
(mapProvider.config as MapProviderControlConfig).mapStateToProps({
|
||||
form_data: { map_renderer: value },
|
||||
} as unknown as ControlPanelState);
|
||||
|
||||
type MapLibreStyleControlConfig = typeof maplibreStyle.config & {
|
||||
mapStateToProps: () => {
|
||||
choices: unknown;
|
||||
default: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const getMapLibreStyleProps = () =>
|
||||
(maplibreStyle.config as MapLibreStyleControlConfig).mapStateToProps();
|
||||
|
||||
test('deck.gl MapLibre style choices expose Streets (OSM)', () => {
|
||||
expect(maplibreStyle.config.choices).toContainEqual([
|
||||
OSM_TILE_STYLE_URL,
|
||||
'Streets (OSM)',
|
||||
]);
|
||||
});
|
||||
|
||||
test('deck.gl map renderer hides Mapbox when no key exists for new selections', () => {
|
||||
setBootstrap({ conf: {} });
|
||||
|
||||
const props = getMapProviderProps('maplibre');
|
||||
|
||||
expect(props.options).toEqual([
|
||||
{ value: 'maplibre', label: 'MapLibre (open-source)' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('deck.gl map renderer keeps saved Mapbox visible while disabled without a key', () => {
|
||||
setBootstrap({ conf: {} });
|
||||
|
||||
const props = getMapProviderProps('mapbox');
|
||||
|
||||
expect(props.options).toContainEqual({
|
||||
value: 'mapbox',
|
||||
label: 'Mapbox (API key required)',
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('deck.gl map renderer enables Mapbox when a key exists', () => {
|
||||
setBootstrap({ conf: { MAPBOX_API_KEY: 'pk.test' } });
|
||||
|
||||
const props = getMapProviderProps('maplibre');
|
||||
|
||||
expect(props.options).toEqual([
|
||||
{ value: 'maplibre', label: 'MapLibre (open-source)' },
|
||||
{ value: 'mapbox', label: 'Mapbox (API key required)' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('deck.gl map renderer keeps the original explanatory description', () => {
|
||||
expect(mapProvider.config.description).toBe(
|
||||
'Select the map tile provider. MapLibre is open-source and requires no API key. Mapbox requires MAPBOX_API_KEY to be configured in Superset.',
|
||||
);
|
||||
});
|
||||
|
||||
test('deck.gl map renderer defaults to configured Mapbox when a key exists', () => {
|
||||
setBootstrap({
|
||||
conf: { DEFAULT_MAP_RENDERER: 'mapbox', MAPBOX_API_KEY: 'pk.test' },
|
||||
});
|
||||
|
||||
expect(getMapProviderProps('maplibre').default).toBe('mapbox');
|
||||
});
|
||||
|
||||
test('deck.gl map renderer falls back from configured Mapbox default without a key', () => {
|
||||
setBootstrap({ conf: { DEFAULT_MAP_RENDERER: 'mapbox' } });
|
||||
|
||||
expect(getMapProviderProps('maplibre').default).toBe('maplibre');
|
||||
});
|
||||
|
||||
test('deck.gl map style falls back to default tiles for empty overrides', () => {
|
||||
setBootstrap({ deckglTiles: [] });
|
||||
|
||||
const props = getMapLibreStyleProps();
|
||||
|
||||
expect(props.choices).toContainEqual([OSM_TILE_STYLE_URL, 'Streets (OSM)']);
|
||||
expect(props.default).toBe(
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
);
|
||||
});
|
||||
|
||||
test('deck.gl map style falls back to default tiles for malformed overrides', () => {
|
||||
setBootstrap({
|
||||
deckglTiles: [
|
||||
['https://tiles.example.com/{z}/{x}/{y}.png'],
|
||||
['https://tiles.example.com/{z}/{x}/{y}.png', 'Custom', 'Extra'],
|
||||
['', 'Empty URL'],
|
||||
],
|
||||
});
|
||||
|
||||
const props = getMapLibreStyleProps();
|
||||
|
||||
expect(props.choices).toContainEqual([OSM_TILE_STYLE_URL, 'Streets (OSM)']);
|
||||
expect(props.default).toBe(
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
);
|
||||
});
|
||||
|
||||
test('deck.gl map style accepts well-formed tile overrides', () => {
|
||||
setBootstrap({
|
||||
deckglTiles: [['https://tiles.example.com/style.json', 'Custom']],
|
||||
});
|
||||
|
||||
const props = getMapLibreStyleProps();
|
||||
|
||||
expect(props.choices).toEqual([
|
||||
['https://tiles.example.com/style.json', 'Custom'],
|
||||
]);
|
||||
expect(props.default).toBe('https://tiles.example.com/style.json');
|
||||
});
|
||||
@@ -25,20 +25,9 @@ import {
|
||||
getCategoricalSchemeRegistry,
|
||||
getSequentialSchemeRegistry,
|
||||
SequentialScheme,
|
||||
type QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getDefaultMapRenderer,
|
||||
getBootstrapDataFromDocument,
|
||||
getMapRendererOptions,
|
||||
OSM_TILE_STYLE_URL,
|
||||
type MapRendererOption,
|
||||
type MapProvider,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
import {
|
||||
ControlPanelState,
|
||||
ControlStateMapping,
|
||||
ControlState,
|
||||
CustomControlItem,
|
||||
D3_FORMAT_OPTIONS,
|
||||
getColorControlsProps,
|
||||
@@ -51,23 +40,15 @@ import {
|
||||
isColorSchemeTypeVisible,
|
||||
} from './utils';
|
||||
import { TooltipTemplateControl } from './TooltipTemplateControl';
|
||||
import { hasMapboxApiKey } from '../utils/mapbox';
|
||||
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
const sequentialSchemeRegistry = getSequentialSchemeRegistry();
|
||||
|
||||
export const DEFAULT_DECKGL_COLOR = { r: 158, g: 158, b: 158, a: 1 };
|
||||
|
||||
type DeckGLTileChoice = [string, string];
|
||||
type MapStyleVisibilityProps = {
|
||||
controls?: ControlStateMapping;
|
||||
};
|
||||
type MetricControlValue = {
|
||||
type?: unknown;
|
||||
value?: unknown;
|
||||
};
|
||||
let deckglTiles: string[][];
|
||||
|
||||
export const DEFAULT_DECKGL_TILES: DeckGLTileChoice[] = [
|
||||
export const DEFAULT_DECKGL_TILES = [
|
||||
[
|
||||
'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
||||
'Light (Carto)',
|
||||
@@ -81,10 +62,9 @@ export const DEFAULT_DECKGL_TILES: DeckGLTileChoice[] = [
|
||||
'Streets (Carto)',
|
||||
],
|
||||
['https://tiles.openfreemap.org/styles/liberty', 'Liberty (OpenFreeMap)'],
|
||||
[OSM_TILE_STYLE_URL, 'Streets (OSM)'],
|
||||
];
|
||||
|
||||
export const DEFAULT_MAPBOX_TILES: DeckGLTileChoice[] = [
|
||||
export const DEFAULT_MAPBOX_TILES = [
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets (Mapbox)'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark (Mapbox)'],
|
||||
['mapbox://styles/mapbox/light-v9', 'Light (Mapbox)'],
|
||||
@@ -93,56 +73,17 @@ export const DEFAULT_MAPBOX_TILES: DeckGLTileChoice[] = [
|
||||
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors (Mapbox)'],
|
||||
];
|
||||
|
||||
const isDeckGLTileChoices = (value: unknown): value is DeckGLTileChoice[] =>
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value.every(
|
||||
choice =>
|
||||
Array.isArray(choice) &&
|
||||
choice.length === 2 &&
|
||||
typeof choice[0] === 'string' &&
|
||||
choice[0].trim().length > 0 &&
|
||||
typeof choice[1] === 'string' &&
|
||||
choice[1].trim().length > 0,
|
||||
);
|
||||
|
||||
const getDeckGLTiles = () => {
|
||||
const bootstrapData = getBootstrapDataFromDocument();
|
||||
const deckglTilesOverride = (
|
||||
bootstrapData as {
|
||||
common?: { deckgl_tiles?: unknown };
|
||||
} | null
|
||||
)?.common?.deckgl_tiles;
|
||||
return isDeckGLTileChoices(deckglTilesOverride)
|
||||
? deckglTilesOverride
|
||||
: DEFAULT_DECKGL_TILES;
|
||||
if (!deckglTiles) {
|
||||
const appContainer = document.getElementById('app');
|
||||
const { common } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
deckglTiles = common?.deckgl_tiles ?? DEFAULT_DECKGL_TILES;
|
||||
}
|
||||
return deckglTiles;
|
||||
};
|
||||
|
||||
const getMapLibreStyleProps = () => {
|
||||
const choices = getDeckGLTiles();
|
||||
return {
|
||||
choices,
|
||||
default: choices[0][0],
|
||||
};
|
||||
};
|
||||
|
||||
const getLabeledMapRendererOptions = ({
|
||||
hasMapboxKey,
|
||||
currentValue,
|
||||
}: {
|
||||
hasMapboxKey: boolean;
|
||||
currentValue?: MapProvider;
|
||||
}) =>
|
||||
getMapRendererOptions({ hasMapboxKey, currentValue }).map(
|
||||
(option: MapRendererOption) => ({
|
||||
...option,
|
||||
label:
|
||||
option.value === 'maplibre'
|
||||
? t('MapLibre (open-source)')
|
||||
: t('Mapbox (API key required)'),
|
||||
}),
|
||||
);
|
||||
|
||||
const DEFAULT_VIEWPORT = {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
@@ -515,26 +456,15 @@ export const mapProvider = {
|
||||
label: t('Map Renderer'),
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
options: getLabeledMapRendererOptions({
|
||||
hasMapboxKey: hasMapboxApiKey(),
|
||||
}),
|
||||
choices: [
|
||||
['maplibre', t('MapLibre (open-source)')],
|
||||
['mapbox', t('Mapbox (API key required)')],
|
||||
],
|
||||
default: 'maplibre',
|
||||
description: t(
|
||||
'Select the map tile provider. MapLibre is open-source and requires no API key. ' +
|
||||
'Mapbox requires MAPBOX_API_KEY to be configured in Superset.',
|
||||
),
|
||||
mapStateToProps: (state: ControlPanelState) => {
|
||||
const hasKey = hasMapboxApiKey();
|
||||
return {
|
||||
options: getLabeledMapRendererOptions({
|
||||
hasMapboxKey: hasKey,
|
||||
currentValue: state.form_data?.map_renderer as
|
||||
| MapProvider
|
||||
| undefined,
|
||||
}),
|
||||
default: getDefaultMapRenderer(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -546,14 +476,13 @@ export const maplibreStyle = {
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
choices: DEFAULT_DECKGL_TILES,
|
||||
default: DEFAULT_DECKGL_TILES[0][0],
|
||||
choices: getDeckGLTiles(),
|
||||
default: getDeckGLTiles()[0][0],
|
||||
description: t(
|
||||
'Base layer map style. Accepts a MapLibre-compatible style URL.',
|
||||
),
|
||||
visibility: ({ controls }: MapStyleVisibilityProps) =>
|
||||
visibility: ({ controls }: ControlPanelState) =>
|
||||
controls?.map_renderer?.value !== 'mapbox',
|
||||
mapStateToProps: getMapLibreStyleProps,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -570,7 +499,7 @@ export const mapboxStyle = {
|
||||
description: t(
|
||||
'Base layer map style. Accepts a Mapbox style URL (mapbox://styles/...).',
|
||||
),
|
||||
visibility: ({ controls }: MapStyleVisibilityProps) =>
|
||||
visibility: ({ controls }: ControlPanelState) =>
|
||||
controls?.map_renderer?.value === 'mapbox',
|
||||
},
|
||||
};
|
||||
@@ -588,14 +517,14 @@ export const geojsonColumn = {
|
||||
},
|
||||
};
|
||||
|
||||
const extractMetricsFromFormData = (formData: QueryFormData) => {
|
||||
const metrics = new Set<unknown>();
|
||||
const extractMetricsFromFormData = (formData: any) => {
|
||||
const metrics = new Set<string>();
|
||||
|
||||
if (formData.metrics) {
|
||||
(Array.isArray(formData.metrics)
|
||||
? formData.metrics
|
||||
: [formData.metrics]
|
||||
).forEach((metric: unknown) => metrics.add(metric));
|
||||
).forEach((metric: any) => metrics.add(metric));
|
||||
}
|
||||
|
||||
if (formData.point_radius_fixed?.value) {
|
||||
@@ -604,9 +533,8 @@ const extractMetricsFromFormData = (formData: QueryFormData) => {
|
||||
|
||||
Object.entries(formData).forEach(([, value]) => {
|
||||
if (!value || typeof value !== 'object') return;
|
||||
const controlValue = value as MetricControlValue;
|
||||
if (controlValue.type === 'metric' && controlValue.value) {
|
||||
metrics.add(controlValue.value);
|
||||
if ((value as any).type === 'metric' && (value as any).value) {
|
||||
metrics.add((value as any).value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +555,7 @@ export const tooltipContents = {
|
||||
),
|
||||
ghostButtonText: t('Drop columns/metrics here or click'),
|
||||
disabledTabs: new Set(['saved', 'sqlExpression']),
|
||||
mapStateToProps: (state: ControlPanelState) => {
|
||||
mapStateToProps: (state: any) => {
|
||||
const { datasource, form_data: formData } = state;
|
||||
|
||||
const selectedMetrics = formData
|
||||
@@ -636,8 +564,7 @@ export const tooltipContents = {
|
||||
|
||||
return {
|
||||
columns: datasource?.columns || [],
|
||||
savedMetrics:
|
||||
datasource && 'metrics' in datasource ? datasource.metrics || [] : [],
|
||||
savedMetrics: datasource?.metrics || [],
|
||||
datasource,
|
||||
selectedMetrics,
|
||||
disabledTabs: new Set(['saved', 'sqlExpression']),
|
||||
@@ -657,7 +584,7 @@ export const tooltipTemplate = {
|
||||
default: '',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
mapStateToProps: (_state: ControlPanelState, control: ControlState) => ({
|
||||
mapStateToProps: (_state: any, control: any) => ({
|
||||
value: control.value,
|
||||
}),
|
||||
},
|
||||
@@ -775,13 +702,8 @@ export const deckGLBreakpointMetric: CustomControlItem = {
|
||||
// mapStateToProps: (state: ControlPanelState) => ({
|
||||
// datasource: state.datasource,
|
||||
// }),
|
||||
visibility: ({ controls }: MapStyleVisibilityProps) =>
|
||||
controls
|
||||
? isColorSchemeTypeVisible(
|
||||
controls,
|
||||
COLOR_SCHEME_TYPES.color_breakpoints,
|
||||
)
|
||||
: false,
|
||||
visibility: ({ controls }: { controls: any }) =>
|
||||
isColorSchemeTypeVisible(controls, COLOR_SCHEME_TYPES.color_breakpoints),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getMapboxApiKey, hasMapboxApiKey } from './mapbox';
|
||||
|
||||
const setBootstrap = (conf: Record<string, unknown>) => {
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify({
|
||||
common: { conf },
|
||||
})}'></div>`;
|
||||
};
|
||||
|
||||
test('deck.gl Mapbox helpers read key presence from bootstrap data', () => {
|
||||
setBootstrap({ MAPBOX_API_KEY: 'pk.test' });
|
||||
|
||||
expect(getMapboxApiKey()).toBe('pk.test');
|
||||
expect(hasMapboxApiKey()).toBe(true);
|
||||
|
||||
setBootstrap({});
|
||||
|
||||
expect(getMapboxApiKey()).toBe('');
|
||||
expect(hasMapboxApiKey()).toBe(false);
|
||||
});
|
||||
@@ -17,15 +17,19 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getMapboxApiKeyFromBootstrap,
|
||||
hasMapboxApiKey as hasBootstrapMapboxApiKey,
|
||||
} from '@superset-ui/core/utils/mapStyles';
|
||||
|
||||
export function getMapboxApiKey(): string {
|
||||
return getMapboxApiKeyFromBootstrap();
|
||||
}
|
||||
|
||||
export function hasMapboxApiKey(): boolean {
|
||||
return hasBootstrapMapboxApiKey();
|
||||
if (typeof document === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const appContainer = document.getElementById('app');
|
||||
const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
|
||||
if (dataBootstrap) {
|
||||
const bootstrapData = JSON.parse(dataBootstrap);
|
||||
return bootstrapData?.common?.conf?.MAPBOX_API_KEY || '';
|
||||
}
|
||||
} catch {
|
||||
// If bootstrap data is unavailable or malformed, return empty string
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user