mirror of
https://github.com/apache/superset.git
synced 2026-07-01 12:25:32 +00:00
Compare commits
48 Commits
fix-report
...
feat/glyph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb36d7b043 | ||
|
|
7c2f5142ce | ||
|
|
874ac3dc01 | ||
|
|
f56e34d6e6 | ||
|
|
742a21f6f7 | ||
|
|
a7c49ac9f2 | ||
|
|
99d927eac7 | ||
|
|
994594e4a8 | ||
|
|
e92599fb50 | ||
|
|
eebe1a1a5b | ||
|
|
664e777a84 | ||
|
|
750518cf6f | ||
|
|
59d1b5f300 | ||
|
|
a27ec1923e | ||
|
|
3e2174b50f | ||
|
|
5b66443d48 | ||
|
|
2ea7585490 | ||
|
|
eeac76146c | ||
|
|
6a1091d576 | ||
|
|
8e82b6b2c3 | ||
|
|
b0c5f99007 | ||
|
|
f1ae683923 | ||
|
|
d51d98891e | ||
|
|
1f95a6c486 | ||
|
|
e93cbd6c38 | ||
|
|
dca8af770c | ||
|
|
81c1181519 | ||
|
|
387c62919e | ||
|
|
77d7483f27 | ||
|
|
1a8d08152d | ||
|
|
257dafeec5 | ||
|
|
6d08e79259 | ||
|
|
01ed81785e | ||
|
|
7b4efacbc2 | ||
|
|
7cb4990403 | ||
|
|
c90b2571d7 | ||
|
|
1a4941eee5 | ||
|
|
d839cca995 | ||
|
|
0ec7e7df99 | ||
|
|
9d8287e1bd | ||
|
|
0c696cea7e | ||
|
|
fe625a917e | ||
|
|
a69f9eb00d | ||
|
|
1311d040ba | ||
|
|
6e2db42d98 | ||
|
|
28aedc82c3 | ||
|
|
f56524bb71 | ||
|
|
4ae9980e4c |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -3,10 +3,6 @@ enable-beta-ecosystems: true
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
ignore:
|
||||
# Ignore temporarily as release schedule is too mentally taxing for dep-handling maintainers
|
||||
# Additionally, very few PRs are reviewed by this action.
|
||||
- dependency-name: anthropics/claude-code-action
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
|
||||
88
.github/workflows/claude.yml
vendored
88
.github/workflows/claude.yml
vendored
@@ -1,88 +0,0 @@
|
||||
name: Claude PR Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
allowed: ${{ steps.check.outputs.allowed }}
|
||||
steps:
|
||||
- name: Check if user is allowed
|
||||
id: check
|
||||
env:
|
||||
COMMENTER: ${{ github.event.comment.user.login }}
|
||||
run: |
|
||||
# List of allowed users
|
||||
ALLOWED_USERS="mistercrunch,rusackas"
|
||||
|
||||
echo "Checking permissions for user: $COMMENTER"
|
||||
|
||||
# Check if user is in allowed list
|
||||
if [[ ",$ALLOWED_USERS," == *",$COMMENTER,"* ]]; then
|
||||
echo "allowed=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ User $COMMENTER is allowed to use Claude"
|
||||
else
|
||||
echo "allowed=false" >> $GITHUB_OUTPUT
|
||||
echo "❌ User $COMMENTER is not allowed to use Claude"
|
||||
fi
|
||||
|
||||
deny-access:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
COMMENTER_LOGIN: ${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}
|
||||
with:
|
||||
script: |
|
||||
const commenter = process.env.COMMENTER_LOGIN;
|
||||
const message = `👋 Hi @${commenter}!
|
||||
|
||||
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
|
||||
|
||||
If you believe you should have access, please contact a project maintainer.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: message
|
||||
});
|
||||
|
||||
claude-code-action:
|
||||
needs: check-permissions
|
||||
if: needs.check-permissions.outputs.allowed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
@@ -14,6 +14,10 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Install pre-push hooks too, not just pre-commit:
|
||||
# pre-commit install --hook-type pre-push
|
||||
# (or this default applies automatically on `pre-commit install`)
|
||||
default_install_hook_types: [pre-commit, pre-push]
|
||||
repos:
|
||||
- repo: https://github.com/MarcoGorelli/auto-walrus
|
||||
rev: 0.3.4
|
||||
@@ -94,6 +98,19 @@ repos:
|
||||
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
|
||||
exclude: ^superset-frontend/cypress-base\/
|
||||
require_serial: true
|
||||
- id: full-type-check-frontend
|
||||
name: Full Type-Check (Frontend, pre-push)
|
||||
# The pre-commit (per-file) variant catches most issues but `tsc <file>`
|
||||
# bypasses some project-wide invariants. This runs the same check CI
|
||||
# runs (`npm run type` -> `tsc --noEmit -p tsconfig.json`) so cross-file
|
||||
# type errors are surfaced before they reach origin.
|
||||
entry: bash -c 'cd superset-frontend && npm run type'
|
||||
language: system
|
||||
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
|
||||
exclude: ^superset-frontend/cypress-base\/
|
||||
pass_filenames: false
|
||||
stages: [pre-push]
|
||||
require_serial: true
|
||||
# blacklist unsafe functions like make_url (see #19526)
|
||||
- repo: https://github.com/skorokithakis/blacklist-pre-commit-hook
|
||||
rev: e2f070289d8eddcaec0b580d3bde29437e7c8221
|
||||
|
||||
10
UPDATING.md
10
UPDATING.md
@@ -24,6 +24,16 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### `thumbnail_url` removed from dashboard list API response
|
||||
|
||||
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
|
||||
|
||||
```
|
||||
/api/v1/dashboard/{id}/thumbnail/{changed_on_utc}/
|
||||
```
|
||||
|
||||
The thumbnail endpoint redirects to the current digest URL regardless of whether the supplied digest is exact. If the image is not yet cached, that digest URL may return `202` and trigger async generation. Using `changed_on_utc` as the digest is sufficient for cache-busting purposes.
|
||||
|
||||
### Webhook alerts/reports block private/internal hosts by default
|
||||
|
||||
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
|
||||
|
||||
@@ -111,6 +111,8 @@ services:
|
||||
superset-init-light:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
ports:
|
||||
- "${SUPERSET_PORT:-8088}:8088"
|
||||
environment:
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
@@ -162,7 +164,7 @@ services:
|
||||
environment:
|
||||
# set this to false if you have perf issues running the npm i; npm run dev in-docker
|
||||
# if you do so, you have to run this manually on the host, which should perform better!
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: false
|
||||
NPM_RUN_PRUNE: false
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
|
||||
|
||||
@@ -34,6 +34,14 @@ x-superset-volumes: &superset-volumes
|
||||
- superset_home:/app/superset_home
|
||||
- ./tests:/app/tests
|
||||
- superset_data:/app/data
|
||||
# Python package metadata for the editable `uv pip install -e .` that
|
||||
# docker-bootstrap.sh runs at container start. Without these bind mounts
|
||||
# the editable install reads stale metadata baked into the image at
|
||||
# build time and may conflict with apache-superset-core's current pins.
|
||||
- ./pyproject.toml:/app/pyproject.toml
|
||||
- ./setup.py:/app/setup.py
|
||||
- ./MANIFEST.in:/app/MANIFEST.in
|
||||
- ./README.md:/app/README.md
|
||||
x-common-build: &common-build
|
||||
context: .
|
||||
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@swc/core": "^1.15.41",
|
||||
"antd": "^6.4.3",
|
||||
"baseline-browser-mapping": "^2.10.34",
|
||||
"baseline-browser-mapping": "^2.10.35",
|
||||
"caniuse-lite": "^1.0.30001797",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
@@ -101,7 +101,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.60.1",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
@@ -109,7 +109,7 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.60.1",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
@@ -7235,10 +7235,10 @@
|
||||
"pypi_packages": [
|
||||
"oracledb"
|
||||
],
|
||||
"connection_string": "oracle://{username}:{password}@{hostname}:{port}",
|
||||
"connection_string": "oracle+oracledb://{username}:{password}@{hostname}:{port}",
|
||||
"default_port": 1521,
|
||||
"notes": "Previously used cx_Oracle, now uses oracledb.",
|
||||
"docs_url": "https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html",
|
||||
"docs_url": "https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html",
|
||||
"category": "Other Databases"
|
||||
},
|
||||
"engine": "oracle",
|
||||
|
||||
286
docs/yarn.lock
286
docs/yarn.lock
@@ -4143,86 +4143,86 @@
|
||||
dependencies:
|
||||
apg-lite "^1.0.4"
|
||||
|
||||
"@swc/core-darwin-arm64@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz#b05d715b04c4fd47baf59288233da85a683cc0bc"
|
||||
integrity sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==
|
||||
"@swc/core-darwin-arm64@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz#4fcbc9cbb9dfc9027d66e2b23b8d1d0315d164bd"
|
||||
integrity sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==
|
||||
|
||||
"@swc/core-darwin-x64@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz#3180daef5c1e47b435f8edd084509e0a5c0d883b"
|
||||
integrity sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==
|
||||
"@swc/core-darwin-x64@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz#726c60a893e2f1a07bee28f79b519b8e6489415b"
|
||||
integrity sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz#18fcd3c70e48fdfae07c9f18751b1409ce1e5e84"
|
||||
integrity sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==
|
||||
"@swc/core-linux-arm-gnueabihf@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz#08930e8015ca2fadc729546d5bd4b758a3999dda"
|
||||
integrity sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==
|
||||
|
||||
"@swc/core-linux-arm64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz#26304933922f2a8e3194770e404403fc25a19c89"
|
||||
integrity sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==
|
||||
"@swc/core-linux-arm64-gnu@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz#6c27490a4013647a09ff64cea1d6b1169394602f"
|
||||
integrity sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==
|
||||
|
||||
"@swc/core-linux-arm64-musl@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz#3402dfba04ba7b8ea81f243e2f8fa2c336b54d03"
|
||||
integrity sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==
|
||||
"@swc/core-linux-arm64-musl@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz#4cce52fbbbe78b1f99c2a4e3f9ad2629f6eae494"
|
||||
integrity sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==
|
||||
|
||||
"@swc/core-linux-ppc64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz#b3df9065cad352328c1eeef08a28fc9fe98785aa"
|
||||
integrity sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==
|
||||
"@swc/core-linux-ppc64-gnu@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz#3d1fadd8d320e7250a6b2a2d9c0b0d4dac162f97"
|
||||
integrity sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==
|
||||
|
||||
"@swc/core-linux-s390x-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz#58e5b601f641dde81b30626ef66a668701ec918f"
|
||||
integrity sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==
|
||||
"@swc/core-linux-s390x-gnu@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz#6e4c54168d4a8d7852ef797437bd25e6fb5d7a50"
|
||||
integrity sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==
|
||||
|
||||
"@swc/core-linux-x64-gnu@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz#cf057dce0c148c53f2d30152baaf60ea29e5d59c"
|
||||
integrity sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==
|
||||
"@swc/core-linux-x64-gnu@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz#5f947698786e15e2f696e0c6b3afd25138bae86b"
|
||||
integrity sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==
|
||||
|
||||
"@swc/core-linux-x64-musl@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz#21fb1a4d0193e9bbcd1469ecd36166d2e96e4006"
|
||||
integrity sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==
|
||||
"@swc/core-linux-x64-musl@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz#f4a0910cb273e39bcc09d572a08f62a355a93628"
|
||||
integrity sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz#1dba23b2b0db86b3d6d65da2abd627cc607a1fbc"
|
||||
integrity sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==
|
||||
"@swc/core-win32-arm64-msvc@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz#a55334b1b7c23a962d4219f332b6422f3c3374e4"
|
||||
integrity sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==
|
||||
|
||||
"@swc/core-win32-ia32-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz#b2da1e33165d469467b1046a2189db468da488eb"
|
||||
integrity sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==
|
||||
"@swc/core-win32-ia32-msvc@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz#e1135f8d6857f6c48e4bfb6105568b37b3f88dc5"
|
||||
integrity sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.15.40":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz#3563f7e8ce8708f5fda43eb8e0956ef11e0da320"
|
||||
integrity sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==
|
||||
"@swc/core-win32-x64-msvc@1.15.41":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz#52d241e2bf4c6154675c0ad447b29cbdb0ccb547"
|
||||
integrity sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==
|
||||
|
||||
"@swc/core@^1.15.40", "@swc/core@^1.7.39":
|
||||
version "1.15.40"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.40.tgz#941c949aa88c0d8d291f102f519f3c2c77701b90"
|
||||
integrity sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==
|
||||
"@swc/core@^1.15.41", "@swc/core@^1.7.39":
|
||||
version "1.15.41"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.41.tgz#a212c5040abd1ffd2ad6caf140f0d586ffcfaa6e"
|
||||
integrity sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
"@swc/types" "^0.1.26"
|
||||
optionalDependencies:
|
||||
"@swc/core-darwin-arm64" "1.15.40"
|
||||
"@swc/core-darwin-x64" "1.15.40"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.15.40"
|
||||
"@swc/core-linux-arm64-gnu" "1.15.40"
|
||||
"@swc/core-linux-arm64-musl" "1.15.40"
|
||||
"@swc/core-linux-ppc64-gnu" "1.15.40"
|
||||
"@swc/core-linux-s390x-gnu" "1.15.40"
|
||||
"@swc/core-linux-x64-gnu" "1.15.40"
|
||||
"@swc/core-linux-x64-musl" "1.15.40"
|
||||
"@swc/core-win32-arm64-msvc" "1.15.40"
|
||||
"@swc/core-win32-ia32-msvc" "1.15.40"
|
||||
"@swc/core-win32-x64-msvc" "1.15.40"
|
||||
"@swc/core-darwin-arm64" "1.15.41"
|
||||
"@swc/core-darwin-x64" "1.15.41"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.15.41"
|
||||
"@swc/core-linux-arm64-gnu" "1.15.41"
|
||||
"@swc/core-linux-arm64-musl" "1.15.41"
|
||||
"@swc/core-linux-ppc64-gnu" "1.15.41"
|
||||
"@swc/core-linux-s390x-gnu" "1.15.41"
|
||||
"@swc/core-linux-x64-gnu" "1.15.41"
|
||||
"@swc/core-linux-x64-musl" "1.15.41"
|
||||
"@swc/core-win32-arm64-msvc" "1.15.41"
|
||||
"@swc/core-win32-ia32-msvc" "1.15.41"
|
||||
"@swc/core-win32-x64-msvc" "1.15.41"
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
@@ -4922,110 +4922,110 @@
|
||||
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.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
|
||||
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
|
||||
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.61.0"
|
||||
"@typescript-eslint/type-utils" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.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":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
|
||||
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
|
||||
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
|
||||
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.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"
|
||||
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
|
||||
"@typescript-eslint/project-service@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
|
||||
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.1"
|
||||
"@typescript-eslint/types" "^8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.61.0"
|
||||
"@typescript-eslint/types" "^8.61.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@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"
|
||||
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
|
||||
"@typescript-eslint/scope-manager@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
|
||||
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.60.1":
|
||||
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==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@^8.60.1":
|
||||
"@typescript-eslint/tsconfig-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
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/tsconfig-utils@^8.61.0":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
|
||||
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
|
||||
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/typescript-estree" "8.60.1"
|
||||
"@typescript-eslint/utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@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.61.0":
|
||||
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.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
|
||||
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
|
||||
"@typescript-eslint/types@^8.61.0":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
|
||||
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
|
||||
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.1"
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/visitor-keys" "8.60.1"
|
||||
"@typescript-eslint/project-service" "8.61.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.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/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.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
|
||||
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
|
||||
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.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.60.1":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
|
||||
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
|
||||
"@typescript-eslint/visitor-keys@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
|
||||
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.60.1"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -5688,10 +5688,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.10.34, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.34"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz#dedb606362446777cfe328d30d4ee15056d06303"
|
||||
integrity sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==
|
||||
baseline-browser-mapping@^2.10.35, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.35"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz#f0f2232e0de2d2f82cc491bcf830b05ed05937c6"
|
||||
integrity sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -14499,15 +14499,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.61.0:
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
|
||||
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
|
||||
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.61.0"
|
||||
"@typescript-eslint/parser" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.16.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.16.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ spec:
|
||||
{{- if .Values.init.initContainers }}
|
||||
initContainers: {{- tpl (toYaml .Values.init.initContainers) . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.hostAliases }}
|
||||
hostAliases: {{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ template "superset.name" . }}-init-db
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=6.0.0, <7.0",
|
||||
"croniter>=0.3.28",
|
||||
"croniter>=6.2.2",
|
||||
"cron-descriptor",
|
||||
"cryptography>=42.0.4, <47.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
@@ -53,7 +53,7 @@ dependencies = [
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
"flask-login>=0.6.0, < 1.0",
|
||||
"flask-migrate>=3.1.0, <5.0",
|
||||
"flask-migrate>=4.1.0, <5.0",
|
||||
"flask-session>=0.4.0, <1.0",
|
||||
"flask-wtf>=1.3.0, <2.0",
|
||||
"geopy",
|
||||
@@ -97,7 +97,7 @@ dependencies = [
|
||||
"selenium>=4.44.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=3.15.0",
|
||||
"simplejson>=4.1.1",
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
|
||||
@@ -144,7 +144,7 @@ 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"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
excel = ["xlrd>=2.0.2, <2.1"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.2.4,<4.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
@@ -156,7 +156,7 @@ firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=26.4.0"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
|
||||
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
|
||||
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
@@ -173,11 +173,11 @@ motherduck = ["apache-superset[duckdb]"]
|
||||
mysql = ["mysqlclient>=2.1.0, <3"]
|
||||
ocient = [
|
||||
"sqlalchemy-ocient>=1.0.0",
|
||||
"pyocient>=1.0.15, <2",
|
||||
"pyocient>=1.0.15, <4",
|
||||
"shapely",
|
||||
"geojson",
|
||||
]
|
||||
oracle = ["cx-Oracle>8.0.0, <8.4"]
|
||||
oracle = ["oracledb>=2.0.0, <5"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.60.0, <2"]
|
||||
|
||||
@@ -84,7 +84,7 @@ colorama==0.4.6
|
||||
# flask-appbuilder
|
||||
cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
croniter==6.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==46.0.7
|
||||
# via
|
||||
@@ -141,7 +141,7 @@ flask-login==0.6.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
flask-migrate==4.1.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-session==0.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -384,7 +384,7 @@ setuptools==80.9.0
|
||||
# via -r requirements/base.in
|
||||
shillelagh==1.4.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
simplejson==3.20.1
|
||||
simplejson==4.1.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
six==1.17.0
|
||||
# via
|
||||
|
||||
@@ -174,7 +174,7 @@ cron-descriptor==1.4.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
croniter==6.0.0
|
||||
croniter==6.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -293,7 +293,7 @@ flask-login==0.6.3
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
flask-migrate==3.1.0
|
||||
flask-migrate==4.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -939,7 +939,7 @@ shillelagh==1.4.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
simplejson==3.20.1
|
||||
simplejson==4.1.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -107,7 +107,13 @@ module.exports = {
|
||||
[
|
||||
'babel-plugin-jsx-remove-data-test-id',
|
||||
{
|
||||
attributes: 'data-test',
|
||||
// The plugin matches attribute names exactly (no prefix match),
|
||||
// so each data-test* attribute must be listed explicitly.
|
||||
attributes: [
|
||||
'data-test',
|
||||
'data-test-drag-source-id',
|
||||
'data-test-drop-target-id',
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
@@ -27,6 +27,11 @@ module.exports = {
|
||||
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||
// mapping glyph-core to local package source
|
||||
'^@superset-ui/glyph-core$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src',
|
||||
'^@superset-ui/glyph-core/(.*)$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src/$1',
|
||||
// mapping plugins of superset-ui to source code
|
||||
'^@superset-ui/([^/]+)/(.*)$':
|
||||
'<rootDir>/node_modules/@superset-ui/$1/src/$2',
|
||||
|
||||
490
superset-frontend/package-lock.json
generated
490
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,7 @@
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
"@superset-ui/glyph-core": "file:./packages/superset-ui-glyph-core",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",
|
||||
@@ -178,14 +179,14 @@
|
||||
"echarts": "^5.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fuse.js": "^7.4.1",
|
||||
"fuse.js": "^7.4.2",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
"geostyler-openlayers-parser": "^5.7.0",
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"google-auth-library": "^10.7.0",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -203,13 +204,13 @@
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-arborist": "^3.10.1",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-json-tree": "^0.20.0",
|
||||
@@ -261,7 +262,7 @@
|
||||
"@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.14",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
@@ -270,7 +271,7 @@
|
||||
"@storybook/react-webpack5": "10.4.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/plugin-emotion": "^14.12.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
@@ -285,8 +286,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -344,18 +345,19 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"oxlint": "^1.69.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.8.4",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-dnd-test-backend": "^11.1.3",
|
||||
"react-refresh": "^0.18.0",
|
||||
"react-resizable": "^4.0.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.2",
|
||||
"storybook": "10.4.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.5",
|
||||
"jest": "^30.4.2",
|
||||
"yeoman-test": "^11.5.2"
|
||||
"yeoman-test": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*",
|
||||
"lodash": "^4.18.1",
|
||||
|
||||
@@ -16,13 +16,26 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export {}; // ensure this file is treated as a module so top-level declarations don't leak into global scope
|
||||
|
||||
type LoggingModule = typeof import('./index');
|
||||
|
||||
const loadLogging = (): LoggingModule['logging'] => {
|
||||
let logging: LoggingModule['logging'] | undefined;
|
||||
jest.isolateModules(() => {
|
||||
({ logging } = jest.requireActual<LoggingModule>(
|
||||
'@apache-superset/core/utils',
|
||||
));
|
||||
});
|
||||
return logging!;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should pipe to `console` methods', () => {
|
||||
const { logging } = require('@apache-superset/core/utils');
|
||||
const logging = loadLogging();
|
||||
|
||||
jest.spyOn(logging, 'debug').mockImplementation();
|
||||
jest.spyOn(logging, 'log').mockImplementation();
|
||||
@@ -50,20 +63,24 @@ test('should pipe to `console` methods', () => {
|
||||
});
|
||||
|
||||
test('should use noop functions when console unavailable', () => {
|
||||
const originalConsole = window.console;
|
||||
Object.assign(window, { console: undefined });
|
||||
const { logging } = require('@apache-superset/core/utils');
|
||||
try {
|
||||
const logging = loadLogging();
|
||||
|
||||
expect(() => {
|
||||
logging.debug();
|
||||
logging.log();
|
||||
logging.info();
|
||||
logging.warn('warn');
|
||||
logging.error('error');
|
||||
logging.trace();
|
||||
logging.table([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
}).not.toThrow();
|
||||
Object.assign(window, { console });
|
||||
expect(() => {
|
||||
logging.debug();
|
||||
logging.log();
|
||||
logging.info();
|
||||
logging.warn('warn');
|
||||
logging.error('error');
|
||||
logging.trace();
|
||||
logging.table([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
}).not.toThrow();
|
||||
} finally {
|
||||
Object.assign(window, { console: originalConsole });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
"memoize-one": "^5.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -415,6 +415,8 @@ export interface ControlPanelSectionConfig {
|
||||
props: ControlPanelsContainerProps,
|
||||
controlData: AnyDict,
|
||||
) => boolean;
|
||||
/** @internal Marks the auto-generated glyph "Chart Options" section */
|
||||
_glyphChartOptions?: boolean;
|
||||
}
|
||||
|
||||
export interface StandardizedControls {
|
||||
@@ -447,6 +449,8 @@ export interface ControlPanelConfig {
|
||||
sectionOverrides?: SectionOverrides;
|
||||
onInit?: (state: ControlStateMapping) => void;
|
||||
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
|
||||
/** @internal Raw glyph argument definitions from defineChart() – used for native control panel rendering */
|
||||
_glyphArgs?: unknown;
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
|
||||
@@ -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.9",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
@@ -101,8 +101,8 @@
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^5.26.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
|
||||
@@ -33,6 +33,14 @@ export enum Behavior {
|
||||
*/
|
||||
DrillToDetail = 'DRILL_TO_DETAIL',
|
||||
DrillBy = 'DRILL_BY',
|
||||
|
||||
/**
|
||||
* Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data
|
||||
* gracefully (e.g., showing a drop zone for drag-and-drop configuration).
|
||||
* Charts with this behavior will receive empty data instead of seeing
|
||||
* the "No results" message.
|
||||
*/
|
||||
AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS',
|
||||
}
|
||||
|
||||
export interface ContextMenuFilters {
|
||||
|
||||
@@ -17,15 +17,23 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Avatar as AntdAvatar } from 'antd';
|
||||
import type { AvatarProps, GroupProps as AvatarGroupProps } from './types';
|
||||
|
||||
export function Avatar(props: AvatarProps) {
|
||||
return <AntdAvatar {...props} />;
|
||||
}
|
||||
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => (
|
||||
<AntdAvatar ref={ref} {...props} />
|
||||
));
|
||||
|
||||
export function AvatarGroup(props: AvatarGroupProps) {
|
||||
return <AntdAvatar.Group {...props} />;
|
||||
}
|
||||
// antd Avatar.Group is a plain function component without forwardRef; wrap in
|
||||
// a span so this component can be a Tooltip / Popover trigger and skip the
|
||||
// findDOMNode fallback.
|
||||
export const AvatarGroup = forwardRef<HTMLSpanElement, AvatarGroupProps>(
|
||||
(props, ref) => (
|
||||
<span ref={ref}>
|
||||
<AntdAvatar.Group {...props} />
|
||||
</span>
|
||||
),
|
||||
);
|
||||
|
||||
export type { AvatarProps, AvatarGroupProps };
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Children, ReactElement, Fragment } from 'react';
|
||||
import { Children, ReactElement, Fragment, forwardRef, Ref } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button as AntdButton } from 'antd';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
@@ -100,7 +100,7 @@ const BUTTON_STYLE_MAP: Record<
|
||||
link: { type: 'link' },
|
||||
};
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
const {
|
||||
tooltip,
|
||||
placement,
|
||||
@@ -160,6 +160,7 @@ export function Button(props: ButtonProps) {
|
||||
|
||||
const button = (
|
||||
<AntdButton
|
||||
ref={ref as Ref<HTMLButtonElement & HTMLAnchorElement>}
|
||||
href={disabled ? undefined : href}
|
||||
disabled={disabled}
|
||||
type={antdType}
|
||||
@@ -235,4 +236,6 @@ export function Button(props: ButtonProps) {
|
||||
return button;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLElement, ButtonProps>(ButtonInner);
|
||||
|
||||
export type { ButtonProps, OnClickHandler };
|
||||
|
||||
@@ -75,7 +75,10 @@ export const DropdownButton = ({
|
||||
id={`${kebabCase(tooltip)}-tooltip`}
|
||||
title={tooltip}
|
||||
>
|
||||
{button}
|
||||
{/* antd Dropdown.Button is a plain function component without
|
||||
forwardRef; wrap in a span so the Tooltip can attach a ref to a
|
||||
real DOM node and skip the findDOMNode fallback. */}
|
||||
<span>{button}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,10 @@ export function EditableTitle({
|
||||
t("You don't have the rights to alter this title.")
|
||||
}
|
||||
>
|
||||
{titleComponent}
|
||||
{/* Wrap in span so the Tooltip can attach a ref to a DOM element.
|
||||
antd's Input.TextArea forwards a non-DOM imperative handle, which
|
||||
triggers a React 18 findDOMNode deprecation warning. */}
|
||||
<span>{titleComponent}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,47 +16,54 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { Button } from '../Button';
|
||||
import type { IconTooltipProps } from './types';
|
||||
|
||||
export const IconTooltip = ({
|
||||
children = null,
|
||||
className = '',
|
||||
onClick = () => undefined,
|
||||
placement = 'top',
|
||||
style = {},
|
||||
tooltip = null,
|
||||
mouseEnterDelay = 0.3,
|
||||
mouseLeaveDelay = 0.15,
|
||||
}: IconTooltipProps) => {
|
||||
const iconTooltip = (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: 0,
|
||||
...style,
|
||||
}}
|
||||
buttonStyle="link"
|
||||
className={`IconTooltip ${className}`}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
export const IconTooltip = forwardRef<HTMLElement, IconTooltipProps>(
|
||||
(
|
||||
{
|
||||
children = null,
|
||||
className = '',
|
||||
onClick = () => undefined,
|
||||
placement = 'top',
|
||||
style = {},
|
||||
tooltip = null,
|
||||
mouseEnterDelay = 0.3,
|
||||
mouseLeaveDelay = 0.15,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const iconTooltip = (
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: 0,
|
||||
...style,
|
||||
}}
|
||||
buttonStyle="link"
|
||||
className={`IconTooltip ${className}`}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
};
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id="tooltip"
|
||||
title={tooltip}
|
||||
placement={placement}
|
||||
mouseEnterDelay={mouseEnterDelay}
|
||||
mouseLeaveDelay={mouseLeaveDelay}
|
||||
>
|
||||
{iconTooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return iconTooltip;
|
||||
},
|
||||
);
|
||||
|
||||
export type { IconTooltipProps };
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
SlackOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { FC } from 'react';
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
@@ -323,19 +323,25 @@ type AntdIconNames = keyof typeof AntdIcons;
|
||||
|
||||
export const antdEnhancedIcons: Record<
|
||||
AntdIconNames,
|
||||
FC<IconType>
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
> = Object.keys(AntdIcons)
|
||||
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key as AntdIconNames] = (props: IconType) => (
|
||||
<BaseIconComponent
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={AntdIcons[key as AntdIconNames]}
|
||||
fileName={key}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<AntdIconNames, FC<IconType>>,
|
||||
{} as Record<
|
||||
AntdIconNames,
|
||||
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
|
||||
>,
|
||||
);
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC, SVGProps, useEffect, useRef, useState } from 'react';
|
||||
import { FC, SVGProps, forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import TransparentIcon from './svgs/transparent.svg';
|
||||
import { IconType } from './types';
|
||||
import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
|
||||
@@ -46,6 +46,7 @@ const AsyncIcon = (props: IconType) => {
|
||||
|
||||
return (
|
||||
<BaseIconComponent
|
||||
ref={ref}
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
fileName={fileName}
|
||||
customIcons={customIcons}
|
||||
@@ -55,6 +56,6 @@ const AsyncIcon = (props: IconType) => {
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default AsyncIcon;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, type ComponentType } from 'react';
|
||||
import { css, useTheme, getFontSize } from '@apache-superset/core/theme';
|
||||
import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
|
||||
|
||||
@@ -35,65 +36,78 @@ const genAriaLabel = (fileName: string) => {
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
export const BaseIconComponent: React.FC<
|
||||
export const BaseIconComponent = forwardRef<
|
||||
HTMLSpanElement | SVGSVGElement,
|
||||
BaseIconProps & Omit<IconType, 'component'>
|
||||
> = ({
|
||||
component: Component,
|
||||
iconColor,
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
? `${getFontSize(theme, iconSize)}px`
|
||||
: `${theme.fontSize}px`,
|
||||
cursor: rest?.onClick ? 'pointer' : undefined,
|
||||
};
|
||||
>(
|
||||
(
|
||||
{
|
||||
component: Component,
|
||||
iconColor,
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
? `${getFontSize(theme, iconSize)}px`
|
||||
: `${theme.fontSize}px`,
|
||||
cursor: rest?.onClick ? 'pointer' : undefined,
|
||||
};
|
||||
|
||||
return customIcons ? (
|
||||
<span
|
||||
role={whatRole}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
css={[
|
||||
css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<Component
|
||||
viewBox={viewBox || '0 0 24 24'}
|
||||
const AntdComponent = Component as ComponentType<
|
||||
Record<string, unknown> & {
|
||||
ref?: React.Ref<HTMLSpanElement | SVGSVGElement>;
|
||||
}
|
||||
>;
|
||||
return customIcons ? (
|
||||
<span
|
||||
ref={ref as React.Ref<HTMLSpanElement>}
|
||||
role={whatRole}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
css={[
|
||||
css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 0;
|
||||
vertical-align: middle;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<Component
|
||||
viewBox={viewBox || '0 0 24 24'}
|
||||
style={style}
|
||||
width={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
height={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
{...(rest as CustomIconType)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<AntdComponent
|
||||
ref={ref}
|
||||
role={whatRole}
|
||||
style={style}
|
||||
width={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
height={
|
||||
iconSize
|
||||
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
|
||||
: `${theme.fontSize}px`
|
||||
}
|
||||
{...(rest as CustomIconType)}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<Component
|
||||
role={whatRole}
|
||||
style={style}
|
||||
aria-label={ariaLabel}
|
||||
data-test={ariaLabel}
|
||||
{...(rest as AntdIconType)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,12 +17,16 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC } from 'react';
|
||||
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
|
||||
import { antdEnhancedIcons } from './AntdEnhanced';
|
||||
import AsyncIcon from './AsyncIcon';
|
||||
|
||||
import type { IconType } from './types';
|
||||
|
||||
type IconComponent = ForwardRefExoticComponent<
|
||||
IconType & RefAttributes<HTMLSpanElement>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Filename is going to be inferred from the icon name.
|
||||
* i.e. BigNumberChartTile => assets/images/icons/big_number_chart_tile
|
||||
@@ -58,15 +62,17 @@ const customIcons = [
|
||||
'Undo',
|
||||
] as const;
|
||||
|
||||
type CustomIconType = Record<(typeof customIcons)[number], FC<IconType>>;
|
||||
type CustomIconType = Record<(typeof customIcons)[number], IconComponent>;
|
||||
|
||||
const iconOverrides: CustomIconType = {} as CustomIconType;
|
||||
customIcons.forEach(customIcon => {
|
||||
const fileName = customIcon
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||
.toLowerCase();
|
||||
iconOverrides[customIcon] = (props: IconType) => (
|
||||
<AsyncIcon customIcons fileName={fileName} {...props} />
|
||||
iconOverrides[customIcon] = forwardRef<HTMLSpanElement, IconType>(
|
||||
(props, ref) => (
|
||||
<AsyncIcon ref={ref} customIcons fileName={fileName} {...props} />
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -74,7 +80,7 @@ export type IconNameType =
|
||||
| keyof typeof antdEnhancedIcons
|
||||
| keyof typeof iconOverrides;
|
||||
|
||||
type IconComponentType = Record<IconNameType, FC<IconType>>;
|
||||
type IconComponentType = Record<IconNameType, IconComponent>;
|
||||
|
||||
export const Icons: IconComponentType = {
|
||||
...antdEnhancedIcons,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tag } from '@superset-ui/core/components/Tag';
|
||||
import { css } from '@emotion/react';
|
||||
import { useTheme, getColorVariants } from '@apache-superset/core/theme';
|
||||
@@ -23,7 +24,7 @@ import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
|
||||
import { PublishedLabel } from './reusable/PublishedLabel';
|
||||
import type { LabelProps } from './types';
|
||||
|
||||
export function Label(props: LabelProps) {
|
||||
export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
const theme = useTheme();
|
||||
// Use Ant Design's motion duration instead of deprecated transitionTiming
|
||||
const {
|
||||
@@ -71,6 +72,7 @@ export function Label(props: LabelProps) {
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
role={onClick ? 'button' : undefined}
|
||||
style={style}
|
||||
@@ -81,6 +83,6 @@ export function Label(props: LabelProps) {
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
});
|
||||
export { DatasetTypeLabel, PublishedLabel };
|
||||
export type { LabelType } from './types';
|
||||
|
||||
@@ -371,6 +371,9 @@ const CustomModal = ({
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds ?? false}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
// Pass nodeRef so react-draggable does not fall back to
|
||||
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
|
||||
nodeRef={draggableRef}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Popover as AntdPopover } from 'antd';
|
||||
import { PopoverProps as AntdPopoverProps } from 'antd/es/popover';
|
||||
import type { TooltipRef } from 'antd/es/tooltip';
|
||||
|
||||
export interface PopoverProps extends AntdPopoverProps {
|
||||
forceRender?: boolean;
|
||||
}
|
||||
|
||||
export const Popover = (props: PopoverProps) => <AntdPopover {...props} />;
|
||||
export const Popover = forwardRef<TooltipRef, PopoverProps>((props, ref) => (
|
||||
<AntdPopover ref={ref} {...props} />
|
||||
));
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { MouseEventHandler, forwardRef } from 'react';
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { SupersetTheme } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import type { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
export interface RefreshLabelProps {
|
||||
@@ -32,25 +31,19 @@ const RefreshLabel = ({
|
||||
onClick,
|
||||
tooltipContent,
|
||||
disabled,
|
||||
}: RefreshLabelProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const IconWithoutRef = forwardRef((props: IconType, ref: any) => (
|
||||
<Icons.SyncOutlined iconSize="l" {...props} />
|
||||
));
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipContent}>
|
||||
<IconWithoutRef
|
||||
role="button"
|
||||
onClick={disabled ? undefined : onClick}
|
||||
css={(theme: SupersetTheme) => ({
|
||||
cursor: 'pointer',
|
||||
color: theme.colorIcon,
|
||||
'&:hover': { color: theme.colorPrimary },
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}: RefreshLabelProps) => (
|
||||
<Tooltip title={tooltipContent}>
|
||||
<Icons.SyncOutlined
|
||||
iconSize="l"
|
||||
role="button"
|
||||
onClick={disabled ? undefined : onClick}
|
||||
css={(theme: SupersetTheme) => ({
|
||||
cursor: 'pointer',
|
||||
color: theme.colorIcon,
|
||||
'&:hover': { color: theme.colorPrimary },
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export default RefreshLabel;
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import { Tooltip as AntdTooltip } from 'antd';
|
||||
import type { TooltipRef } from 'antd/es/tooltip';
|
||||
|
||||
import type { TooltipProps, TooltipPlacement } from './types';
|
||||
|
||||
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
|
||||
<AntdTooltip
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
|
||||
({ overlayStyle, ...props }, ref) => (
|
||||
<AntdTooltip
|
||||
ref={ref}
|
||||
styles={{
|
||||
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
root: overlayStyle ?? {},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
export type { TooltipProps, TooltipPlacement };
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
# Glyph Pattern Migration Guide
|
||||
|
||||
This guide documents how to migrate traditional Superset chart plugins to the single-file Glyph pattern.
|
||||
|
||||
## Overview
|
||||
|
||||
The Glyph pattern simplifies chart plugin development by:
|
||||
- **Arguments define BOTH controls AND render props** - No separate files needed
|
||||
- **No `controlPanel.ts`** - Generated from argument definitions
|
||||
- **No `transformProps.ts`** - Arguments are passed directly to render
|
||||
- **No `buildQuery.ts`** - Inferred from Metric/Dimension/Temporal arguments
|
||||
- **Single file** - Everything in one place (~200 lines vs 500+ across multiple files)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Analyze the Existing Chart
|
||||
|
||||
Identify from the original chart:
|
||||
- **Metrics/Dimensions**: What data does it query?
|
||||
- **Controls**: What options does the user configure?
|
||||
- **Styling**: What visual customizations exist?
|
||||
- **Rendering**: How is the data displayed?
|
||||
|
||||
### 2. Create the Glyph Chart File
|
||||
|
||||
Create a new file: `src/BigNumber/BigNumberGlyph/index.tsx`
|
||||
|
||||
```typescript
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Behavior, getNumberFormatter, CurrencyFormatter } from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
defineChart,
|
||||
Metric,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
NumberFormat,
|
||||
Currency,
|
||||
TimeFormat,
|
||||
ConditionalFormatting,
|
||||
} from '@superset-ui/glyph-core';
|
||||
```
|
||||
|
||||
### 3. Define Arguments (Controls + Props)
|
||||
|
||||
**CRITICAL: Use camelCase for argument names!**
|
||||
|
||||
Superset converts control names to camelCase in `formData`. If you use snake_case (`show_metric_name`), it won't match the camelCase key in formData (`showMetricName`).
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
// Data arguments
|
||||
metric: Metric.with({ label: t('Metric') }),
|
||||
|
||||
// Visual arguments - USE CAMELCASE!
|
||||
headerFontSize: Select.with({
|
||||
label: t('Font Size'),
|
||||
options: [
|
||||
{ label: t('Small'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.4 },
|
||||
],
|
||||
default: 0.4,
|
||||
}),
|
||||
|
||||
showMetricName: Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
default: false,
|
||||
}),
|
||||
|
||||
// Declarative visibility (preferred)
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Declarative disabled state
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { subtitle: '' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Available Argument Types
|
||||
|
||||
| Type | Control Generated | Value Type | Properties |
|
||||
|------|------------------|------------|------------|
|
||||
| `Metric` | MetricControl | `{ value, name, formattedValue }` | `label` |
|
||||
| `Dimension` | GroupByControl | `string[]` | `label` |
|
||||
| `Temporal` | TemporalControl | `string` | `label` |
|
||||
| `Select` | SelectControl | `string \| number` | `label`, `description`, `options`, `default` |
|
||||
| `Text` | TextControl | `string` | `label`, `description`, `default`, `placeholder` |
|
||||
| `Checkbox` | CheckboxControl | `boolean` | `label`, `description`, `default` |
|
||||
| `Int` | SliderControl | `number` | `label`, `description`, `default`, `min`, `max`, `step` |
|
||||
| `Color` | ColorPickerControl | `string` (hex) | `label`, `description`, `default` |
|
||||
| `NumberFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `Currency` | CurrencyControl | `{ symbol?, symbolPosition? }` | `label`, `description`, `default` |
|
||||
| `TimeFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `ConditionalFormatting` | ConditionalFormattingControl | `Rule[]` | `label`, `description` |
|
||||
|
||||
### 5. Declarative Visibility & Disabled States
|
||||
|
||||
Instead of Redux `mapStateToProps`, use declarative conditions:
|
||||
|
||||
```typescript
|
||||
// Simple equality check - visible when showMetricName is true
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Function check - visible when subtitle is not empty
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { subtitle: (val) => !!val },
|
||||
},
|
||||
|
||||
// Multiple conditions (AND) - visible when both conditions are met
|
||||
advancedOption: {
|
||||
arg: Checkbox.with({ ... }),
|
||||
visibleWhen: {
|
||||
showMetricName: true,
|
||||
subtitle: (val) => !!val,
|
||||
},
|
||||
},
|
||||
|
||||
// Disabled state (control visible but not editable)
|
||||
formatOption: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { forceTimestampFormatting: true },
|
||||
},
|
||||
```
|
||||
|
||||
### 6. Number, Currency, and Time Formatting
|
||||
|
||||
Use the built-in format argument types:
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
numberFormat: NumberFormat.with({
|
||||
label: t('Number Format'),
|
||||
description: t('D3 format string'),
|
||||
default: 'SMART_NUMBER',
|
||||
}),
|
||||
|
||||
currencyFormat: Currency.with({
|
||||
label: t('Currency Format'),
|
||||
}),
|
||||
|
||||
timeFormat: TimeFormat.with({
|
||||
label: t('Date Format'),
|
||||
default: 'smart_date',
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Then use them directly in the render function:
|
||||
|
||||
```typescript
|
||||
render: ({ numberFormat, currencyFormat, timeFormat, metric }) => {
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({
|
||||
currency: { symbol: currencyFormat.symbol, symbolPosition: currencyFormat.symbolPosition ?? 'prefix' },
|
||||
d3Format: numberFormat,
|
||||
})
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
return <div>{formatter(metric.value)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Conditional Formatting (Colors)
|
||||
|
||||
Use `ConditionalFormatting` for color-based rules:
|
||||
|
||||
```typescript
|
||||
import { getColorFormatters } from '@superset-ui/chart-controls';
|
||||
|
||||
arguments: {
|
||||
conditionalFormatting: ConditionalFormatting.with({
|
||||
label: t('Conditional Formatting'),
|
||||
description: t('Apply conditional color formatting to metric'),
|
||||
}),
|
||||
},
|
||||
|
||||
render: ({ conditionalFormatting, metric, data, theme }) => {
|
||||
let numberColor: string | undefined;
|
||||
|
||||
if (conditionalFormatting?.length > 0 && metric.value != null) {
|
||||
const colorFormatters = getColorFormatters(conditionalFormatting, data, theme, false);
|
||||
if (colorFormatters) {
|
||||
for (const formatter of colorFormatters) {
|
||||
const color = formatter.getColorFromValue(metric.value as number);
|
||||
if (color) {
|
||||
numberColor = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <BigNumberText color={numberColor}>{metric.formattedValue}</BigNumberText>;
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Styled Components
|
||||
|
||||
Use Superset's theme properties with template literal syntax:
|
||||
|
||||
```typescript
|
||||
const Container = styled.div<{ height: number }>`
|
||||
${({ theme, height }) => `
|
||||
height: ${height}px;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
font-family: ${theme.fontFamily};
|
||||
color: ${theme.colorText};
|
||||
`}
|
||||
`;
|
||||
```
|
||||
|
||||
**Common theme properties:**
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `theme.sizeUnit` | Base spacing unit (typically 4px) |
|
||||
| `theme.fontFamily` | Default font family |
|
||||
| `theme.fontWeightNormal` | Normal font weight |
|
||||
| `theme.fontWeightLight` | Light font weight |
|
||||
| `theme.fontSizeSM` | Small font size |
|
||||
| `theme.colorText` | Primary text color |
|
||||
| `theme.colorTextTertiary` | Muted/secondary text color |
|
||||
| `theme.borderRadius` | Standard border radius |
|
||||
|
||||
### 9. Render Function
|
||||
|
||||
The render function receives all arguments directly - no formData lookup needed:
|
||||
|
||||
```typescript
|
||||
render: ({
|
||||
metric,
|
||||
headerFontSize,
|
||||
showMetricName,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
conditionalFormatting,
|
||||
height,
|
||||
data,
|
||||
theme,
|
||||
}) => {
|
||||
// All arguments are directly available!
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({ currency: currencyFormat, d3Format: numberFormat })
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
const formattedValue = metric.value != null
|
||||
? formatter(metric.value as number)
|
||||
: t('No data');
|
||||
|
||||
return (
|
||||
<Container height={height}>
|
||||
{showMetricName && <MetricName>{metric.name}</MetricName>}
|
||||
<BigNumberText>{formattedValue}</BigNumberText>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
### 10. Register the Plugin
|
||||
|
||||
In `BigNumber/index.ts`:
|
||||
```typescript
|
||||
export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph';
|
||||
```
|
||||
|
||||
In `plugin-chart-echarts/src/index.ts`:
|
||||
```typescript
|
||||
export { BigNumberGlyphChartPlugin } from './BigNumber';
|
||||
```
|
||||
|
||||
In `MainPreset.js`:
|
||||
```typescript
|
||||
import { BigNumberGlyphChartPlugin } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
new BigNumberGlyphChartPlugin().configure({ key: 'big_number_glyph' }),
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Snake Case vs Camel Case
|
||||
- **WRONG**: `show_metric_name` - won't match formData
|
||||
- **RIGHT**: `showMetricName` - matches Superset's camelCase conversion
|
||||
|
||||
### 2. Theme Undefined
|
||||
- **WRONG**: `theme.gridUnit` - crashes if theme is undefined
|
||||
- **RIGHT**: `theme?.gridUnit ?? 4` - safe with fallback
|
||||
|
||||
### 3. Metric Value Extraction
|
||||
The Glyph core automatically extracts metric values from query results. The `metric` argument provides:
|
||||
- `metric.value` - The raw numeric value
|
||||
- `metric.name` - The metric label/name
|
||||
- `metric.formattedValue` - Basic string representation
|
||||
|
||||
### 4. Visibility vs Legacy Functions
|
||||
- **Prefer**: `visibleWhen: { showMetricName: true }` - declarative, clean
|
||||
- **Legacy**: `visibility: ({ controls }) => controls?.showMetricName?.value === true` - still works
|
||||
|
||||
## File Structure Comparison
|
||||
|
||||
### Traditional (5+ files, ~500 lines)
|
||||
```
|
||||
BigNumberTotal/
|
||||
├── index.ts # Plugin registration
|
||||
├── controlPanel.ts # Control definitions (~100 lines)
|
||||
├── transformProps.ts # Data transformation (~150 lines)
|
||||
├── buildQuery.ts # Query building (~50 lines)
|
||||
├── BigNumberViz.tsx # React component (~150 lines)
|
||||
└── types.ts # TypeScript types (~50 lines)
|
||||
```
|
||||
|
||||
### Glyph Pattern (1 file, ~250 lines)
|
||||
```
|
||||
BigNumberGlyph/
|
||||
└── index.tsx # Everything in one file!
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `BigNumber/BigNumberGlyph/index.tsx` for a complete working example with:
|
||||
- Metric display
|
||||
- Number/currency/time formatting
|
||||
- Conditional color formatting
|
||||
- Declarative visibility
|
||||
- Subtitle support
|
||||
- Font size controls
|
||||
@@ -0,0 +1,267 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# [SIP] Proposal for single-file chart plugins via `defineChart()` (the "Glyph" pattern)
|
||||
|
||||
> Draft — to be filed per the [SIP process](https://github.com/apache/superset/issues/5602).
|
||||
> The SIP will be numbered by a committer upon acceptance.
|
||||
|
||||
### Motivation
|
||||
|
||||
Building a Superset visualization plugin today requires authoring and keeping in
|
||||
sync five or more files per chart — `index.ts`, `controlPanel.ts`,
|
||||
`transformProps.ts`, `buildQuery.ts`, `types.ts`, plus the component — wired
|
||||
together through several layers of incidental complexity:
|
||||
|
||||
- **String-based control references.** Controls are referenced by name strings
|
||||
(`'metric'`, `'groupby'`, `'y_axis_format'`) that are resolved at runtime
|
||||
through registries and `expandControlConfig`. Typos fail silently; renames
|
||||
require grep-driven archaeology.
|
||||
- **Array-of-arrays layouts.** Control panel layout is expressed as
|
||||
`controlSetRows: [[control], [control, control], [<JSX/>]]` — positional
|
||||
nesting that conflates layout with configuration and is hostile to both
|
||||
reading and tooling.
|
||||
- **Triplicated knowledge.** The same fact (e.g. "this chart has a `showLegend`
|
||||
boolean defaulting to true") is restated in the control panel (control
|
||||
config), in transformProps (formData extraction + defaulting), and in the
|
||||
component's prop types. The three routinely drift.
|
||||
- **No type safety across the seam.** `transformProps` output and component
|
||||
props are connected only by convention; mismatches surface as silently
|
||||
`undefined` props at runtime, not compile errors.
|
||||
|
||||
The result is a high floor for contributing a chart, and a maintenance burden
|
||||
proportional to (charts × files × duplicated facts).
|
||||
|
||||
### Proposed Change
|
||||
|
||||
A new workspace package, **`@superset-ui/glyph-core`**, provides a
|
||||
**`defineChart()`** function that defines a complete chart plugin in a single
|
||||
file. The core idea: **argument definitions are the single source of truth**
|
||||
for the control panel, the query, the transform, and the render props.
|
||||
|
||||
```tsx
|
||||
export default defineChart({
|
||||
metadata: { name: t('My Chart'), thumbnail, behaviors: [Behavior.InteractiveChart] },
|
||||
arguments: {
|
||||
metric: Metric.with({ label: t('Metric') }),
|
||||
groupby: Dimension.with({ label: t('Dimension') }),
|
||||
showLegend: ShowLegend, // reusable preset
|
||||
legendType: { arg: LegendType, visibleWhen: { showLegend: true } },
|
||||
},
|
||||
transform: (chartProps, argValues) => ({ echartOptions: build(chartProps) }),
|
||||
render: props => <Echart {...props} />,
|
||||
});
|
||||
```
|
||||
|
||||
Key elements:
|
||||
|
||||
- **Argument classes** (`Metric`, `Dimension`, `Temporal`, `Select`, `Checkbox`,
|
||||
`Int`, `Slider`, `Color`, `ColorPicker`, `RadioButton`, `Bounds`, `Text`,
|
||||
`NumberFormat`, `Currency`, `TimeFormat`, `ConditionalFormatting`) declare
|
||||
what a chart needs; glyph-core generates the matching control configs,
|
||||
formData extraction with defaults, and typed render props.
|
||||
- **Declarative conditions** — `visibleWhen` / `disabledWhen` replace
|
||||
imperative `visibility:` functions reaching into Redux controls state, with a
|
||||
single shared evaluator (`evaluateGlyphCondition`) usable from both the
|
||||
legacy pipeline and native rendering.
|
||||
- **Generated `buildQuery`** for the common case (built on the core
|
||||
`getXAxisColumn`/`normalizeTimeColumn` helpers so time grain, adhoc x-axis
|
||||
columns, and the `DTTM_ALIAS` fallback behave identically to hand-written
|
||||
queries); charts with post-processing pass their own `buildQuery`.
|
||||
- **Generated `transformProps`** maps formData → typed render props (metric
|
||||
label resolution via core `getMetricLabel`); charts with heavy transforms
|
||||
supply a `transform` whose return value is merged into render props.
|
||||
- **Escape hatches for incremental migration** — `additionalControls`,
|
||||
`prependSections` / `middleSections` / `additionalSections`,
|
||||
`controlOverrides`, `formDataOverrides`, `onInit`, and
|
||||
`chartOptionsTabOverride` let complex charts keep hand-crafted sections while
|
||||
adopting the pattern for everything else.
|
||||
- **Reusable presets** (`presets.ts`) — `ShowLegend`, `LegendType`,
|
||||
`LegendOrientation`, `HeaderFontSize`, `Subtitle`, etc. — shared,
|
||||
pre-translated argument configurations that keep label strings and defaults
|
||||
consistent across chart families.
|
||||
- **Native Customize-tab rendering** — `GlyphOptionsPanel` renders glyph
|
||||
arguments directly from formData (value + visibility), bypassing the string
|
||||
expansion pipeline. The generated "Chart Options" section is identified by a
|
||||
structural `_glyphChartOptions` marker (never by label matching). Controls
|
||||
whose config requires `mapStateToProps` (e.g. ConditionalFormatting) and JSX
|
||||
rows (sub-section headers) route through the existing legacy renderer for
|
||||
full compatibility.
|
||||
|
||||
### Current Status (what has been done)
|
||||
|
||||
All work lives on `feat/glyph-single-file`. **Every chart family in the main
|
||||
preset has been consolidated** to `defineChart()`:
|
||||
|
||||
- **plugin-chart-echarts**: Pie, Funnel, Gauge, Sankey, Waterfall, Histogram,
|
||||
Tree, Bubble, BoxPlot, Sunburst, Radar, Treemap, Graph, Heatmap, Gantt,
|
||||
BigNumber, BigNumberWithTrendline, BigNumberTotal, BigNumberPeriodOverPeriod,
|
||||
Timeseries (Generic, Area, Bar, Line, Scatter, SmoothLine, Step),
|
||||
MixedTimeseries
|
||||
- **preset-chart-deckgl**: all 11 layers (Arc, Contour, Geojson, Grid, Heatmap,
|
||||
Hex, Path, Polygon, Scatter, Screengrid, Multi)
|
||||
- **legacy plugins**: calendar, horizon, chord, country-map, world-map,
|
||||
paired-t-test, parallel-coordinates, partition, rose
|
||||
- **legacy-preset-chart-nvd3**: Bubble, Bullet, Compare, TimePivot
|
||||
- **others**: table, pivot-table, ag-grid-table (light-touch), word-cloud,
|
||||
point-cluster-map, handlebars, cartodiagram
|
||||
|
||||
Net effect: **−13,000 lines** (+60k/−73k across 482 files).
|
||||
|
||||
**Test coverage added**: a 168-test suite spanning glyph-core unit tests
|
||||
(arguments, defineChart, presets, crossFilter — including regression tests for
|
||||
every audit finding below), `GlyphOptionsPanel` component tests, plugin-level
|
||||
smoke tests for migrated charts, and a Playwright E2E test for the Customize
|
||||
tab.
|
||||
|
||||
**A full-branch audit (2026-06-11)** ran a seven-angle review over the diff;
|
||||
17 findings were confirmed and 14 fixed in-tree, notably:
|
||||
|
||||
- `behaviors` no longer defaults to `[Behavior.InteractiveChart]` — charts must
|
||||
opt in, matching `ChartMetadata`'s own default (21 charts had acquired dead
|
||||
cross-filter UI).
|
||||
- The real `BigNumberTotalChartPlugin` is registered under
|
||||
`big_number_total` again (a demo plugin had displaced it, breaking saved
|
||||
charts' formats and subheaders).
|
||||
- `@superset-ui/glyph-core` added to root `package.json` so webpack's
|
||||
workspace-alias loop resolves live `src/` (fresh builds previously failed).
|
||||
- Explore's initial-query effect is one-shot again — filling in the last
|
||||
invalid control no longer auto-fires a query for any chart type.
|
||||
- `ChartDefinition.metadata` can now express `queryObjectCount`,
|
||||
`dynamicQueryObjectCount`, `parseMethod`, `suppressContextMenu`, and
|
||||
`enableNoResults` (Mixed Chart's second Results tab returned).
|
||||
- `disabledWhen` reads controls from the correct `mapStateToProps` argument
|
||||
(the API was previously non-functional).
|
||||
- Generated queries carry `timeGrain` on the x-axis column; generated metric
|
||||
extraction uses `getMetricLabel` and no longer guesses among multiple numeric
|
||||
columns.
|
||||
- `NumberFormat`/`TimeFormat` options derive from chart-controls'
|
||||
`D3_FORMAT_OPTIONS` (translated, with previews) instead of stale copies.
|
||||
- Dead parallel implementation (`generators.ts`, 419 lines + tests + orphaned
|
||||
types) deleted; condition evaluation deduplicated to a single function.
|
||||
- `scripts/check-custom-rules.js` suppression matching is line-based (real
|
||||
ESLint semantics) — one disable comment can no longer silence an entire
|
||||
subtree; the leaks it had hidden were fixed with properly scoped disables.
|
||||
|
||||
Branch health: full-monorepo `tsc --noEmit` clean, all 24 packages build, all
|
||||
tests and the complete pre-commit suite (mypy, ruff, pylint, oxlint, prettier,
|
||||
frontend type-check, custom rules) pass.
|
||||
|
||||
### New or Changed Public Interfaces
|
||||
|
||||
- **New package** `@superset-ui/glyph-core` (workspace package; public API:
|
||||
`defineChart`, argument classes, presets, `evaluateGlyphCondition`,
|
||||
`getGlyphControlConfig`, `resolveArgClass`, cross-filter utilities).
|
||||
- **`@superset-ui/chart-controls`**: `ControlPanelConfig._glyphArgs` and
|
||||
`ControlPanelSectionConfig._glyphChartOptions` — `@internal` structural
|
||||
markers consumed by explore.
|
||||
- **`@superset-ui/core`**: `Behavior.AllowsEmptyResults` added to the
|
||||
`Behavior` enum (no producer yet — see Remaining Work).
|
||||
- **Explore**: new `GlyphOptionsPanel` component; `ControlPanelsContainer`,
|
||||
`ExploreChartPanel`, `ExploreViewContainer`, `ChartRenderer`, and
|
||||
`getSectionsToRender` learned the glyph path alongside the legacy one.
|
||||
- **REST API**: `ChartDataExtrasSchema` accepts an optional
|
||||
`allow_empty_query` boolean (`load_default=False`); `get_sqla_query` honors
|
||||
it for validation while refusing to compile a zero-column SELECT.
|
||||
- No new CLI surface, no deployment changes.
|
||||
|
||||
### New dependencies
|
||||
|
||||
None. `@superset-ui/glyph-core` is a new in-repo workspace package depending
|
||||
only on existing workspace packages (`@superset-ui/core`,
|
||||
`@superset-ui/chart-controls`). No new npm or PyPI dependencies.
|
||||
|
||||
### Migration Plan and Compatibility
|
||||
|
||||
- **No database migrations.** Saved charts keep their `viz_type` keys and
|
||||
form data; `defineChart` plugins register under the same keys.
|
||||
- **Form-data compatibility** is preserved through generated controls using the
|
||||
same shared control names (`metric`, `groupby`, `adhoc_filters`, `x_axis`,
|
||||
`time_grain_sqla`) and per-chart transforms reading legacy keys. One known
|
||||
gap remains (camelCase arg keys vs. saved snake_case keys — see Remaining
|
||||
Work item 2).
|
||||
- **Incremental by design**: the legacy `controlPanel` pipeline still works
|
||||
untouched; glyph charts coexist with non-migrated charts. Removal of legacy
|
||||
controlPanel support is an explicit later phase, gated on the roadmap below.
|
||||
|
||||
### Remaining Work / Roadmap
|
||||
|
||||
Ordered by priority; items 1–3 should land before this branch merges or
|
||||
immediately after.
|
||||
|
||||
1. **Restore code-splitting (perf regression).**
|
||||
`loadChart: () => Promise.resolve(Component)` makes every migrated
|
||||
renderer — including maplibre-gl (~800 KB) and deck.gl — eager in the
|
||||
initial bundle, where master lazy-loaded them via
|
||||
`loadChart: () => import('./Chart')`. Add lazy-render support to
|
||||
`defineChart` (e.g. `render: () => import('./Render')` or a
|
||||
`loadRender` field) and move heavy renderers back behind dynamic imports.
|
||||
2. **Per-arg `formDataKey` aliases (saved-chart compatibility).**
|
||||
Some migrated charts renamed keys (e.g. BigNumberTotal's `yAxisFormat` /
|
||||
`subtitle` vs. saved `y_axis_format` / `subheader`). Rendering works via
|
||||
camelCase conversion and transform fallbacks, but explore controls show
|
||||
defaults for saved charts and write parallel keys. A
|
||||
`formDataKey: 'y_axis_format'` alias on argument definitions fixes this
|
||||
generally — and is the foundation for settings transfer between viz types
|
||||
(arguments sharing a semantic key carry over on viz-type switch).
|
||||
3. **Typed escape hatch for legacy transforms.**
|
||||
~14 plugins (table, pivot-table, ag-grid, all deckgl layers,
|
||||
point-cluster-map) use `transform: p => transformProps(p as any) as any`
|
||||
plus `render: props => <Comp {...(props as any)} />`. This double-cast class
|
||||
is what previously hid a silent render-props bug. Provide a
|
||||
`defineChart<Props>` overload (typed transform fully replaces render props)
|
||||
or a `wrapLegacyTransform(transformProps, Component)` helper so the next
|
||||
shape mismatch is a compile error.
|
||||
4. **Collapse the dual control-panel pipeline.**
|
||||
`defineChart` currently emits a full legacy `ControlPanelConfig` *and*
|
||||
native `_glyphArgs`; visibility/disabled/validation exist in both paths
|
||||
with different data sources (Redux controls vs. formData). Make the native
|
||||
definition canonical and derive the legacy config as a thin adapter — this
|
||||
is the enabler for the planned consistent section/area rendering schema.
|
||||
5. **Extract shared timeseries transform logic.**
|
||||
The six Timeseries-family charts each inline ~1,200 lines of largely
|
||||
duplicated transform code. Hoist a shared `timeseriesTransform` into
|
||||
glyph-core (or a glyph-echarts helper) before the copies drift.
|
||||
6. **Resolve the `allow_empty_query` plumbing.**
|
||||
Backend accepts the flag and `Behavior.AllowsEmptyResults` exists, but
|
||||
nothing produces either. Either wire the drag-and-drop empty-chart flow
|
||||
end-to-end (derive the extras flag from chart metadata in one place) or
|
||||
drop the speculative plumbing until the feature lands.
|
||||
7. **Finish the migration and remove legacy controlPanel support** once 1–4
|
||||
are in place, per the original plan: every plugin on `defineChart`, the
|
||||
string-expansion pipeline (`expandControlConfig`, array-of-arrays sections)
|
||||
deleted, and chart wrappers reduced to enable animation/realtime rendering.
|
||||
|
||||
### Rejected Alternatives
|
||||
|
||||
- **`createGlyphPlugin()` builder API** (a Map-based generator predating
|
||||
`defineChart`): shipped briefly as `generators.ts`, never adopted by any
|
||||
chart, drifted from the real implementation, and was deleted during the
|
||||
audit in favor of the single `defineChart()` entry point.
|
||||
- **JSON/YAML chart manifests**: a pure-data format can describe controls but
|
||||
not transforms or renderers, forcing a second mechanism anyway; a typed
|
||||
TypeScript API keeps definition, transform, and render in one
|
||||
compiler-checked file.
|
||||
- **Fixing the legacy controlPanel format in place** (e.g. typed control
|
||||
references over the existing array-of-arrays): preserves the triplication of
|
||||
knowledge across controlPanel/transformProps/component and the runtime
|
||||
string-resolution layer that motivates this SIP.
|
||||
- **Big-bang removal of the legacy pipeline**: rejected in favor of
|
||||
coexistence + escape hatches; third-party plugins and complex charts need a
|
||||
migration window.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@superset-ui/glyph-core",
|
||||
"version": "0.20.3",
|
||||
"description": "Glyph Core - A declarative visualization plugin framework for Apache Superset",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/packages/superset-ui-glyph-core"
|
||||
},
|
||||
"keywords": [
|
||||
"superset",
|
||||
"glyph",
|
||||
"visualization",
|
||||
"chart"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 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 {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ColumnType,
|
||||
SelectOptions,
|
||||
SelectOption,
|
||||
TextOptions,
|
||||
CheckboxOptions,
|
||||
IntOptions,
|
||||
ColorOptions,
|
||||
MetricOptions,
|
||||
DimensionOptions,
|
||||
NumberFormatOptions,
|
||||
CurrencyOptions,
|
||||
CurrencyValue,
|
||||
TimeFormatOptions,
|
||||
ConditionalFormattingOptions,
|
||||
ConditionalFormattingRule,
|
||||
SliderOptions,
|
||||
BoundsOptions,
|
||||
BoundsValue,
|
||||
ColorPickerOptions,
|
||||
RadioButtonOptions,
|
||||
RadioOption,
|
||||
RgbaColor,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Base Argument class - all argument types extend from this.
|
||||
*
|
||||
* Arguments define:
|
||||
* 1. What the chart needs (semantically)
|
||||
* 2. How to render controls in the control panel
|
||||
* 3. Default values and validation
|
||||
*/
|
||||
export class Argument {
|
||||
static label: string | null = null;
|
||||
static description: string | null = null;
|
||||
static columnType: ColumnType = ColumnType.Argument;
|
||||
static controlType: string = 'TextControl';
|
||||
|
||||
value: unknown;
|
||||
|
||||
constructor(value: unknown) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.)
|
||||
*
|
||||
* Maps to Superset's MetricsControl in the query section.
|
||||
*/
|
||||
export class Metric extends Argument {
|
||||
static override label: string | null = 'Metric';
|
||||
static override description: string | null =
|
||||
'A numeric aggregation (SUM, COUNT, AVG, etc.)';
|
||||
static override columnType = ColumnType.Metric;
|
||||
static override controlType = 'MetricsControl';
|
||||
static multi = false;
|
||||
|
||||
static with(options: MetricOptions): typeof Metric {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension - represents a categorical column for grouping data
|
||||
*
|
||||
* Maps to Superset's GroupByControl in the query section.
|
||||
*/
|
||||
export class Dimension extends Argument {
|
||||
static override label: string | null = 'Dimension';
|
||||
static override description: string | null =
|
||||
'A categorical column for grouping data';
|
||||
static override columnType = ColumnType.Dimension;
|
||||
static override controlType = 'GroupByControl';
|
||||
static multi = true;
|
||||
|
||||
static with(options: DimensionOptions): typeof Dimension {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporal - represents a time column
|
||||
*
|
||||
* Maps to Superset's temporal controls (x_axis, time_grain_sqla).
|
||||
*/
|
||||
export class Temporal extends Argument {
|
||||
static override label: string | null = 'Time Column';
|
||||
static override description: string | null =
|
||||
'A temporal column for time series data';
|
||||
static override columnType = ColumnType.Temporal;
|
||||
static override controlType = 'TemporalControl';
|
||||
|
||||
static with(options: {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}): typeof Temporal {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select - dropdown selection from predefined options
|
||||
*
|
||||
* Maps to Superset's SelectControl.
|
||||
*/
|
||||
export class Select extends Argument {
|
||||
static override label: string | null = 'Select';
|
||||
static override description: string | null = 'Choose from options';
|
||||
static override controlType = 'SelectControl';
|
||||
static default: string | number = '';
|
||||
static options: SelectOption[] = [];
|
||||
static clearable = false;
|
||||
|
||||
static with(options: SelectOptions): typeof Select {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options ?? Base.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text - free-form text input
|
||||
*
|
||||
* Maps to Superset's TextControl.
|
||||
*/
|
||||
export class Text extends Argument {
|
||||
static override label: string | null = 'Text';
|
||||
static override description: string | null = 'Text input';
|
||||
static override controlType = 'TextControl';
|
||||
static default: string = '';
|
||||
static placeholder: string = '';
|
||||
|
||||
static with(options: TextOptions): typeof Text {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override placeholder = options.placeholder ?? Base.placeholder;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox - boolean toggle
|
||||
*
|
||||
* Maps to Superset's CheckboxControl.
|
||||
*/
|
||||
export class Checkbox extends Argument {
|
||||
static override label: string | null = 'Checkbox';
|
||||
static override description: string | null = 'Toggle option';
|
||||
static override controlType = 'CheckboxControl';
|
||||
static default: boolean = false;
|
||||
|
||||
static with(options: CheckboxOptions): typeof Checkbox {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Int - numeric input with slider
|
||||
*
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Int extends Argument {
|
||||
static override label: string | null = 'Integer';
|
||||
static override description: string | null = 'A numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 100;
|
||||
static step: number = 1;
|
||||
|
||||
static with(options: IntOptions): typeof Int {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color - color picker
|
||||
*
|
||||
* Maps to Superset's ColorPickerControl.
|
||||
*/
|
||||
export class Color extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'A color value';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
static default: string = '#000000';
|
||||
|
||||
static with(options: ColorOptions): typeof Color {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NumberFormat - D3 number format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class NumberFormat extends Argument {
|
||||
static override label: string | null = 'Number Format';
|
||||
static override description: string | null =
|
||||
'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")';
|
||||
static override controlType = 'NumberFormatControl';
|
||||
static default: string = 'SMART_NUMBER';
|
||||
|
||||
// Standard D3 format options — derived from the canonical list in
|
||||
// @superset-ui/chart-controls so glyph charts offer the same formats
|
||||
// (with previews and translations) as legacy charts, and new formats
|
||||
// propagate automatically.
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = D3_FORMAT_OPTIONS.map(
|
||||
([value, label]) => ({ value, label }),
|
||||
);
|
||||
|
||||
static with(options: NumberFormatOptions): typeof NumberFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency - currency format with symbol and position
|
||||
*
|
||||
* Maps to Superset's CurrencyControl.
|
||||
* Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' }
|
||||
*/
|
||||
export class Currency extends Argument {
|
||||
static override label: string | null = 'Currency Format';
|
||||
static override description: string | null =
|
||||
'Currency symbol and position for formatting';
|
||||
static override controlType = 'CurrencyControl';
|
||||
static default: CurrencyValue = {};
|
||||
|
||||
static with(options: CurrencyOptions): typeof Currency {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TimeFormat - D3 time format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 time format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class TimeFormat extends Argument {
|
||||
static override label: string | null = 'Time Format';
|
||||
static override description: string | null =
|
||||
'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")';
|
||||
static override controlType = 'TimeFormatControl';
|
||||
static default: string = 'smart_date';
|
||||
|
||||
// Standard D3 time format options — derived from the canonical list in
|
||||
// @superset-ui/chart-controls (see NumberFormat.FORMAT_OPTIONS).
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = D3_TIME_FORMAT_OPTIONS.map(
|
||||
([value, label]) => ({ value, label }),
|
||||
);
|
||||
|
||||
static with(options: TimeFormatOptions): typeof TimeFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ConditionalFormatting - apply color rules based on metric values
|
||||
*
|
||||
* This is a special argument type that encapsulates the complex
|
||||
* mapStateToProps logic needed for conditional formatting controls.
|
||||
* The control automatically receives numeric column options from the chart response.
|
||||
*/
|
||||
export class ConditionalFormatting extends Argument {
|
||||
static override label: string | null = 'Conditional Formatting';
|
||||
static override description: string | null =
|
||||
'Apply conditional color formatting to metric values';
|
||||
static override controlType = 'ConditionalFormattingControl';
|
||||
static default: ConditionalFormattingRule[] = [];
|
||||
|
||||
static with(
|
||||
options: ConditionalFormattingOptions,
|
||||
): typeof ConditionalFormatting {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider - continuous floating point values with min/max/step
|
||||
*
|
||||
* Similar to Int but for float values.
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Slider extends Argument {
|
||||
static override label: string | null = 'Slider';
|
||||
static override description: string | null = 'A continuous numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 1;
|
||||
static step: number = 0.1;
|
||||
|
||||
static with(options: SliderOptions): typeof Slider {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds - min/max value pairs
|
||||
*
|
||||
* Used for axis bounds, value ranges, etc.
|
||||
* Maps to Superset's BoundsControl.
|
||||
*/
|
||||
export class Bounds extends Argument {
|
||||
static override label: string | null = 'Bounds';
|
||||
static override description: string | null = 'Min and max value bounds';
|
||||
static override controlType = 'BoundsControl';
|
||||
static default: BoundsValue = [null, null];
|
||||
|
||||
static with(options: BoundsOptions): typeof Bounds {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ColorPicker - RGBA color selection
|
||||
*
|
||||
* Different from Color (which uses hex strings).
|
||||
* Maps to Superset's ColorPickerControl with RGBA format.
|
||||
*/
|
||||
export class ColorPicker extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'Select a color';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
|
||||
|
||||
static with(options: ColorPickerOptions): typeof ColorPicker {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RadioButton - mutually exclusive options
|
||||
*
|
||||
* Use for small sets of exclusive choices (2-4 options).
|
||||
* Maps to Superset's RadioButtonControl.
|
||||
*/
|
||||
export class RadioButton extends Argument {
|
||||
static override label: string | null = 'Option';
|
||||
static override description: string | null = 'Select one option';
|
||||
static override controlType = 'RadioButtonControl';
|
||||
static default: string | boolean = '';
|
||||
static options: RadioOption[] = [];
|
||||
|
||||
static with(options: RadioButtonOptions): typeof RadioButton {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ConditionalFormatting type
|
||||
*/
|
||||
export function isConditionalFormattingArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ConditionalFormatting {
|
||||
return argClass.controlType === 'ConditionalFormattingControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a TimeFormat type
|
||||
*/
|
||||
export function isTimeFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof TimeFormat {
|
||||
return argClass.controlType === 'TimeFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a NumberFormat type
|
||||
*/
|
||||
export function isNumberFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof NumberFormat {
|
||||
return argClass.controlType === 'NumberFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Currency type
|
||||
*/
|
||||
export function isCurrencyArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Currency {
|
||||
return argClass.controlType === 'CurrencyControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Select type
|
||||
*/
|
||||
export function isSelectArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Select {
|
||||
return (
|
||||
'options' in argClass && Array.isArray((argClass as typeof Select).options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Checkbox type
|
||||
*/
|
||||
export function isCheckboxArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Checkbox {
|
||||
return (
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof Checkbox).default === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Text type
|
||||
*/
|
||||
export function isTextArg(argClass: typeof Argument): argClass is typeof Text {
|
||||
return (
|
||||
argClass.controlType === 'TextControl' ||
|
||||
(argClass.prototype instanceof Text &&
|
||||
!isSelectArg(argClass) &&
|
||||
!isCheckboxArg(argClass))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is an Int type
|
||||
*/
|
||||
export function isIntArg(argClass: typeof Argument): argClass is typeof Int {
|
||||
return 'min' in argClass && 'max' in argClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Color type
|
||||
*/
|
||||
export function isColorArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Color {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' ||
|
||||
argClass.prototype instanceof Color
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Metric type
|
||||
*/
|
||||
export function isMetricArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Metric {
|
||||
return argClass.columnType === ColumnType.Metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Dimension type
|
||||
*/
|
||||
export function isDimensionArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Dimension {
|
||||
return argClass.columnType === ColumnType.Dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Temporal type
|
||||
*/
|
||||
export function isTemporalArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Temporal {
|
||||
return argClass.columnType === ColumnType.Temporal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Slider type
|
||||
*/
|
||||
export function isSliderArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Slider {
|
||||
return (
|
||||
argClass.controlType === 'SliderControl' &&
|
||||
'step' in argClass &&
|
||||
typeof (argClass as typeof Slider).step === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Bounds type
|
||||
*/
|
||||
export function isBoundsArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Bounds {
|
||||
return argClass.controlType === 'BoundsControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ColorPicker type
|
||||
*/
|
||||
export function isColorPickerArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ColorPicker {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' &&
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof ColorPicker).default === 'object' &&
|
||||
'r' in ((argClass as typeof ColorPicker).default as object)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a RadioButton type
|
||||
*/
|
||||
export function isRadioButtonArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof RadioButton {
|
||||
return argClass.controlType === 'RadioButtonControl';
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cross-Filter Utilities for Glyph Charts
|
||||
*
|
||||
* This module provides helpers for implementing cross-filtering in Glyph charts.
|
||||
* Cross-filtering allows charts to filter other charts on the dashboard when
|
||||
* users click on data points.
|
||||
*
|
||||
* ## Quick Start
|
||||
*
|
||||
* 1. Add behaviors to metadata:
|
||||
* ```typescript
|
||||
* metadata: {
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. Extract cross-filter props in transform:
|
||||
* ```typescript
|
||||
* transform: (chartProps) => {
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap);
|
||||
* return { transformedProps: { ...otherProps, ...crossFilterProps } };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 3. Use event handlers in render:
|
||||
* ```typescript
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChartProps,
|
||||
FilterState,
|
||||
QueryFormColumn,
|
||||
SetDataMaskHook,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Props needed for cross-filtering in the render component.
|
||||
* These are typically returned from the transform function and passed to Echart.
|
||||
*/
|
||||
export interface CrossFilterRenderProps {
|
||||
/** Groupby columns used for filtering */
|
||||
groupby: QueryFormColumn[];
|
||||
/** Maps series names to their groupby column values */
|
||||
labelMap: Record<string, string[]>;
|
||||
/** Callback to emit cross-filter data mask */
|
||||
setDataMask: SetDataMaskHook;
|
||||
/** Maps series indices to selected value names */
|
||||
selectedValues: Record<number, string>;
|
||||
/** Whether cross-filters are enabled for this chart */
|
||||
emitCrossFilters?: boolean;
|
||||
/** Context menu handler for drill actions */
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
/** Column type mapping for formatting */
|
||||
coltypeMapping?: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a selectedValues map from filterState.
|
||||
*
|
||||
* The selectedValues map is used by the Echart component to track which
|
||||
* data points are currently selected (for highlighting).
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param seriesNames - Array of series/data point names
|
||||
* @returns Map of index -> name for selected values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const selectedValues = createSelectedValuesMap(
|
||||
* filterState,
|
||||
* transformedData.map(d => d.name),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createSelectedValuesMap(
|
||||
filterState: FilterState | undefined,
|
||||
seriesNames: string[],
|
||||
): Record<number, string> {
|
||||
return (filterState?.selectedValues || []).reduce(
|
||||
(acc: Record<number, string>, selectedValue: string) => {
|
||||
const index = seriesNames.findIndex(name => name === selectedValue);
|
||||
if (index >= 0) {
|
||||
return { ...acc, [index]: selectedValue };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cross-filter related props from ChartProps.
|
||||
*
|
||||
* This is a convenience function that extracts all the props needed for
|
||||
* cross-filtering from the standard ChartProps object.
|
||||
*
|
||||
* @param chartProps - The chart props from Superset
|
||||
* @param groupby - The groupby columns (dimensions) from form data
|
||||
* @param labelMap - A map from series names to their groupby values
|
||||
* @param seriesNames - Array of series/data point names for selectedValues mapping
|
||||
* @param coltypeMapping - Optional column type mapping
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In transform function:
|
||||
* const labelMap = data.reduce((acc, datum) => ({
|
||||
* ...acc,
|
||||
* [extractGroupbyLabel({ datum, groupby })]: groupby.map(col => datum[col]),
|
||||
* }), {});
|
||||
*
|
||||
* const crossFilterProps = extractCrossFilterProps(
|
||||
* chartProps,
|
||||
* groupby,
|
||||
* labelMap,
|
||||
* transformedData.map(d => d.name),
|
||||
* coltypeMapping,
|
||||
* );
|
||||
*
|
||||
* return {
|
||||
* transformedProps: {
|
||||
* echartOptions,
|
||||
* formData,
|
||||
* width,
|
||||
* height,
|
||||
* refs,
|
||||
* ...crossFilterProps,
|
||||
* },
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export function extractCrossFilterProps(
|
||||
chartProps: ChartProps,
|
||||
groupby: QueryFormColumn[],
|
||||
labelMap: Record<string, string[]>,
|
||||
seriesNames: string[],
|
||||
coltypeMapping?: Record<string, number>,
|
||||
): CrossFilterRenderProps {
|
||||
const { hooks, filterState, emitCrossFilters, formData } = chartProps;
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks ?? {};
|
||||
|
||||
const selectedValues = createSelectedValuesMap(filterState, seriesNames);
|
||||
|
||||
return {
|
||||
groupby,
|
||||
labelMap,
|
||||
setDataMask,
|
||||
selectedValues,
|
||||
emitCrossFilters,
|
||||
onContextMenu,
|
||||
coltypeMapping,
|
||||
// Also include formData for context menu formatting
|
||||
formData,
|
||||
} as CrossFilterRenderProps & { formData: unknown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a data point is currently filtered (should be dimmed).
|
||||
*
|
||||
* Use this in the transform function to apply opacity/styling to
|
||||
* data points that are not part of the current filter selection.
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param name - The name/label of the data point to check
|
||||
* @returns true if the data point should be dimmed, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isFiltered = isDataPointFiltered(filterState, datum.name);
|
||||
* const opacity = isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent;
|
||||
* ```
|
||||
*/
|
||||
export function isDataPointFiltered(
|
||||
filterState: FilterState | undefined,
|
||||
name: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
filterState?.selectedValues &&
|
||||
filterState.selectedValues.length > 0 &&
|
||||
!filterState.selectedValues.includes(name),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a labelMap from data records.
|
||||
*
|
||||
* The labelMap maps series names (like "USA" or "2024-01") to their
|
||||
* corresponding groupby column values. This is needed for the cross-filter
|
||||
* event handlers to construct proper filter clauses.
|
||||
*
|
||||
* @param data - Array of data records
|
||||
* @param groupbyLabels - Array of groupby column labels
|
||||
* @param extractLabel - Function to extract the series label from a datum
|
||||
* @returns Map of label -> groupby values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const labelMap = createLabelMap(
|
||||
* data,
|
||||
* groupbyLabels,
|
||||
* datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createLabelMap<T extends Record<string, unknown>>(
|
||||
data: T[],
|
||||
groupbyLabels: string[],
|
||||
extractLabel: (datum: T) => string,
|
||||
): Record<string, string[]> {
|
||||
return data.reduce((acc: Record<string, string[]>, datum: T) => {
|
||||
const label = extractLabel(datum);
|
||||
return {
|
||||
...acc,
|
||||
[label]: groupbyLabels.map(col => datum[col] as string),
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Core - A declarative visualization plugin framework
|
||||
*
|
||||
* This module enables single-file visualization plugins where:
|
||||
* 1. Arguments define both the chart's inputs AND the control panel
|
||||
* 2. transformProps is auto-generated from argument definitions
|
||||
* 3. The chart component is a simple function receiving typed arguments
|
||||
*
|
||||
* Features:
|
||||
* - Single-file chart definitions with defineChart()
|
||||
* - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.)
|
||||
* - Conditional visibility with visibleWhen/disabledWhen
|
||||
* - Cross-filtering support with extractCrossFilterProps() and allEventHandlers()
|
||||
* - Reusable presets (ShowLegend, HeaderFontSize, etc.)
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* export default defineChart({
|
||||
* metadata: {
|
||||
* name: 'My Chart',
|
||||
* thumbnail,
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* },
|
||||
* arguments: {
|
||||
* metric: Metric.with({ label: 'Metric' }),
|
||||
* groupby: Dimension.with({ label: 'Breakdowns' }),
|
||||
* fontSize: Select.with({
|
||||
* label: 'Font Size',
|
||||
* options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }],
|
||||
* default: 0.3,
|
||||
* }),
|
||||
* },
|
||||
* transform: (chartProps, argValues) => {
|
||||
* // Extract cross-filter props for interactive filtering
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames);
|
||||
* return { transformedProps: { echartOptions, ...crossFilterProps } };
|
||||
* },
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export everything
|
||||
export * from './types';
|
||||
export * from './arguments';
|
||||
export * from './defineChart';
|
||||
export * from './presets';
|
||||
export * from './crossFilter';
|
||||
408
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
408
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Presets - Reusable argument configurations
|
||||
*
|
||||
* This module contains pre-configured arguments that are commonly
|
||||
* used across multiple visualization types. Charts can import these
|
||||
* directly or use .with() to customize them further.
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* import { HeaderFontSize, Subtitle } from '../../glyph-core/presets';
|
||||
*
|
||||
* arguments: {
|
||||
* headerFontSize: HeaderFontSize,
|
||||
* subtitle: Subtitle,
|
||||
* // Override defaults when needed:
|
||||
* customSize: HeaderFontSize.with({ default: 0.5 }),
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Select, Text, Checkbox } from './arguments';
|
||||
import { SelectOption } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// Font Size Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Large font size options - for primary/header text elements
|
||||
* Values are multipliers of container height (0.2 = 20% of height)
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Small font size options - for secondary text elements (subtitles, labels)
|
||||
* Values are multipliers of container height
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Pre-configured Arguments
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Header/primary font size selector
|
||||
* Used for main display elements like big numbers, titles
|
||||
*/
|
||||
export const HeaderFontSize = Select.with({
|
||||
label: t('Font Size'),
|
||||
description: t('Font size for the primary display element'),
|
||||
options: FONT_SIZE_OPTIONS_LARGE,
|
||||
default: 0.4,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subheader/secondary font size selector
|
||||
* Used for subtitles, labels, secondary text
|
||||
*/
|
||||
export const SubheaderFontSize = Select.with({
|
||||
label: t('Subheader Font Size'),
|
||||
description: t('Font size for secondary text elements'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subtitle text input
|
||||
* Generic subtitle/description field used by many chart types
|
||||
*/
|
||||
export const Subtitle = Text.with({
|
||||
label: t('Subtitle'),
|
||||
description: t('Description text displayed below the main content'),
|
||||
default: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* Show legend toggle
|
||||
* Common toggle for charts with legends
|
||||
*/
|
||||
export const ShowLegend = Checkbox.with({
|
||||
// Strings match plugin-chart-echarts' legend controls so existing i18n
|
||||
// catalogs apply and legend UX stays consistent across chart families.
|
||||
label: t('Show legend'),
|
||||
description: t('Whether to display a legend for the chart'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Force timestamp formatting toggle
|
||||
* Used when a value might be a timestamp but isn't auto-detected
|
||||
*/
|
||||
export const ForceTimestampFormatting = Checkbox.with({
|
||||
label: t('Force Date Format'),
|
||||
description: t(
|
||||
'Use date formatting even when the value is not detected as a timestamp',
|
||||
),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Legend Options
|
||||
// ============================================================================
|
||||
|
||||
export const LEGEND_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Scroll'), value: 'scroll' },
|
||||
{ label: t('List'), value: 'plain' },
|
||||
];
|
||||
|
||||
export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
];
|
||||
|
||||
export const LEGEND_SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('No sort'), value: '' },
|
||||
{ label: t('Ascending'), value: 'asc' },
|
||||
{ label: t('Descending'), value: 'desc' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Legend type selector
|
||||
* Choose between scrollable or plain list legend
|
||||
*/
|
||||
export const LegendType = Select.with({
|
||||
label: t('Type'),
|
||||
description: t('Legend type'),
|
||||
options: LEGEND_TYPE_OPTIONS,
|
||||
default: 'scroll',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend orientation selector
|
||||
* Position the legend relative to the chart
|
||||
*/
|
||||
export const LegendOrientation = Select.with({
|
||||
label: t('Orientation'),
|
||||
description: t('Legend Orientation'),
|
||||
options: LEGEND_ORIENTATION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend sort selector
|
||||
* Sort legend items alphabetically
|
||||
*/
|
||||
export const LegendSort = Select.with({
|
||||
label: t('Legend Sort'),
|
||||
description: t('Sort order for legend items'),
|
||||
options: LEGEND_SORT_OPTIONS,
|
||||
default: '',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show labels toggle
|
||||
* Common toggle for chart labels
|
||||
*/
|
||||
export const ShowLabels = Checkbox.with({
|
||||
label: t('Show Labels'),
|
||||
description: t('Whether to display labels on the chart'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show value toggle
|
||||
* Common toggle for showing values on chart elements
|
||||
*/
|
||||
export const ShowValue = Checkbox.with({
|
||||
label: t('Show Value'),
|
||||
description: t('Whether to display values on the chart'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Metric Name Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show metric name toggle
|
||||
* Used in BigNumber charts to optionally show the metric name
|
||||
*/
|
||||
export const ShowMetricName = Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Metric name font size selector
|
||||
* Typically used with visibility tied to ShowMetricName
|
||||
*/
|
||||
export const MetricNameFontSize = Select.with({
|
||||
label: t('Metric Name Font Size'),
|
||||
description: t('Font size for the metric name'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Type Options (shared by Pie, Funnel, etc.)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Standard label content type options
|
||||
* Used by Pie, Funnel, and other category-based charts
|
||||
*/
|
||||
export const LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Percentage'), value: 'percent' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
{ label: t('Category and Percentage'), value: 'key_percent' },
|
||||
{ label: t('Category, Value and Percentage'), value: 'key_value_percent' },
|
||||
{ label: t('Value and Percentage'), value: 'value_percent' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label type selector for category-based charts
|
||||
*/
|
||||
export const LabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sort Options
|
||||
// ============================================================================
|
||||
|
||||
export const SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Descending'), value: 'descending' },
|
||||
{ label: t('Ascending'), value: 'ascending' },
|
||||
{ label: t('None'), value: 'none' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Sort by metric toggle
|
||||
* Common for charts that need to sort data by metric value
|
||||
*/
|
||||
export const SortByMetric = Checkbox.with({
|
||||
label: t('Sort by Metric'),
|
||||
description: t('Sort results by the selected metric'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Position Options
|
||||
// ============================================================================
|
||||
|
||||
export const LABEL_POSITION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Inside'), value: 'inside' },
|
||||
{ label: t('Inside Left'), value: 'insideLeft' },
|
||||
{ label: t('Inside Right'), value: 'insideRight' },
|
||||
{ label: t('Inside Top'), value: 'insideTop' },
|
||||
{ label: t('Inside Bottom'), value: 'insideBottom' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label position selector
|
||||
* Position labels relative to chart elements
|
||||
*/
|
||||
export const LabelPosition = Select.with({
|
||||
label: t('Label Position'),
|
||||
description: t('Position of labels on the chart'),
|
||||
options: LABEL_POSITION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Simple Label Type (key/value variants only)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Simple label type options - for charts with fewer label display options
|
||||
* Used by Radar, Sunburst, etc.
|
||||
*/
|
||||
export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple label type selector
|
||||
* For charts that only need key/value/key_value options
|
||||
*/
|
||||
export const SimpleLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: SIMPLE_LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
/**
|
||||
* Value-only label type options - for charts like Radar
|
||||
*/
|
||||
export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Value label type selector
|
||||
* For charts that show value or category+value
|
||||
*/
|
||||
export const ValueLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: VALUE_LABEL_TYPE_OPTIONS,
|
||||
default: 'value',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Totals and Aggregates
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show total toggle
|
||||
* For charts that can display aggregate totals
|
||||
*/
|
||||
export const ShowTotal = Checkbox.with({
|
||||
label: t('Show Total'),
|
||||
description: t('Whether to display the aggregate total'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Threshold Controls
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Label percentage threshold
|
||||
* Minimum percentage for showing labels (avoids clutter on small slices)
|
||||
*/
|
||||
export const LabelThreshold = Text.with({
|
||||
label: t('Percentage Threshold'),
|
||||
description: t('Minimum threshold in percentage points for showing labels'),
|
||||
default: '5',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Shape Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Circle shape toggle (used by Radar)
|
||||
*/
|
||||
export const CircleShape = Checkbox.with({
|
||||
label: t('Circle Shape'),
|
||||
description: t('Use circular shape instead of polygon'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Data Zoom
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Enable data zoom toggle
|
||||
* For charts with zoomable data areas
|
||||
*/
|
||||
export const DataZoom = Checkbox.with({
|
||||
label: t('Data Zoom'),
|
||||
description: t('Enable data zooming controls'),
|
||||
default: false,
|
||||
});
|
||||
257
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
257
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Option for Select controls
|
||||
*/
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Select argument type
|
||||
*/
|
||||
export interface SelectOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | number;
|
||||
options?: SelectOption[];
|
||||
clearable?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Text argument type
|
||||
*/
|
||||
export interface TextOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Checkbox argument type
|
||||
*/
|
||||
export interface CheckboxOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Int argument type (slider)
|
||||
*/
|
||||
export interface IntOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Color argument type
|
||||
*/
|
||||
export interface ColorOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Metric argument type
|
||||
*/
|
||||
export interface MetricOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Dimension argument type
|
||||
*/
|
||||
export interface DimensionOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for NumberFormat argument type
|
||||
*/
|
||||
export interface NumberFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency value structure
|
||||
*/
|
||||
export interface CurrencyValue {
|
||||
symbol?: string;
|
||||
symbolPosition?: 'prefix' | 'suffix';
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Currency argument type
|
||||
*/
|
||||
export interface CurrencyOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: CurrencyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for TimeFormat argument type
|
||||
*/
|
||||
export interface TimeFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for ConditionalFormatting argument type
|
||||
*/
|
||||
export interface ConditionalFormattingOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Slider argument type (continuous float values)
|
||||
*/
|
||||
export interface SliderOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Bounds argument type (min/max pairs)
|
||||
*/
|
||||
export interface BoundsOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: [number | null, number | null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds value type - tuple of [min, max] where either can be null
|
||||
*/
|
||||
export type BoundsValue = [number | null, number | null];
|
||||
|
||||
/**
|
||||
* Configuration options for ColorPicker argument type (RGBA colors)
|
||||
*/
|
||||
export interface ColorPickerOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: RgbaColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for RadioButton argument type
|
||||
*/
|
||||
export interface RadioButtonOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | boolean;
|
||||
options: RadioOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Option for RadioButton controls
|
||||
*/
|
||||
export interface RadioOption {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional formatting rule value
|
||||
*/
|
||||
export interface ConditionalFormattingRule {
|
||||
column?: string;
|
||||
operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between';
|
||||
targetValue?: number;
|
||||
targetValueLeft?: number;
|
||||
targetValueRight?: number;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column type enum for data arguments
|
||||
*/
|
||||
export enum ColumnType {
|
||||
Metric = 'metric',
|
||||
Dimension = 'dimension',
|
||||
Temporal = 'temporal',
|
||||
Argument = 'argument',
|
||||
}
|
||||
|
||||
/**
|
||||
* RGBA color format used by Superset's ColorPickerControl
|
||||
*/
|
||||
export interface RgbaColor {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility function for conditional control display (legacy)
|
||||
*/
|
||||
export type VisibilityFn = (state: {
|
||||
controls: Record<string, { value: unknown }>;
|
||||
}) => boolean;
|
||||
|
||||
/**
|
||||
* Declarative condition for argument visibility/disabled state.
|
||||
*
|
||||
* Keys are argument names, values define the condition:
|
||||
* - Literal value: equality check (e.g., { showMetricName: true })
|
||||
* - Function: custom check (e.g., { subtitle: (val) => !!val })
|
||||
*
|
||||
* Multiple keys are AND'd together.
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true
|
||||
* visibleWhen: { showMetricName: true }
|
||||
*
|
||||
* @example
|
||||
* // Visible when subtitle is not empty
|
||||
* visibleWhen: { subtitle: (val) => !!val }
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true AND subtitle is not empty
|
||||
* visibleWhen: { showMetricName: true, subtitle: (val) => !!val }
|
||||
*/
|
||||
export type ArgumentCondition = Record<
|
||||
string,
|
||||
unknown | ((value: unknown) => boolean)
|
||||
>;
|
||||
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* 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 {
|
||||
Argument,
|
||||
Bounds,
|
||||
Checkbox,
|
||||
Color,
|
||||
ColorPicker,
|
||||
ConditionalFormatting,
|
||||
Currency,
|
||||
Dimension,
|
||||
Int,
|
||||
isBoundsArg,
|
||||
isCheckboxArg,
|
||||
isColorArg,
|
||||
isColorPickerArg,
|
||||
isConditionalFormattingArg,
|
||||
isCurrencyArg,
|
||||
isDimensionArg,
|
||||
isIntArg,
|
||||
isMetricArg,
|
||||
isNumberFormatArg,
|
||||
isRadioButtonArg,
|
||||
isSelectArg,
|
||||
isSliderArg,
|
||||
isTemporalArg,
|
||||
isTextArg,
|
||||
isTimeFormatArg,
|
||||
Metric,
|
||||
NumberFormat,
|
||||
RadioButton,
|
||||
Select,
|
||||
Slider,
|
||||
Temporal,
|
||||
Text,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import { ColumnType } from '@superset-ui/glyph-core/types';
|
||||
|
||||
describe('Argument base class', () => {
|
||||
test('stores its constructor value', () => {
|
||||
const a = new Argument(42);
|
||||
expect(a.value).toBe(42);
|
||||
});
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Argument.label).toBeNull();
|
||||
expect(Argument.description).toBeNull();
|
||||
expect(Argument.columnType).toBe(ColumnType.Argument);
|
||||
expect(Argument.controlType).toBe('TextControl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metric', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Metric.label).toBe('Metric');
|
||||
expect(Metric.columnType).toBe(ColumnType.Metric);
|
||||
expect(Metric.controlType).toBe('MetricsControl');
|
||||
expect(Metric.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const M = Metric.with({
|
||||
label: 'Sales',
|
||||
description: 'Total sales',
|
||||
multi: true,
|
||||
});
|
||||
expect(M.label).toBe('Sales');
|
||||
expect(M.description).toBe('Total sales');
|
||||
expect(M.multi).toBe(true);
|
||||
// unaltered ancestor metadata still present
|
||||
expect(M.columnType).toBe(ColumnType.Metric);
|
||||
expect(M.controlType).toBe('MetricsControl');
|
||||
});
|
||||
|
||||
test('.with() falls back to parent defaults when option omitted', () => {
|
||||
const M = Metric.with({ label: 'X' });
|
||||
expect(M.label).toBe('X');
|
||||
expect(M.multi).toBe(Metric.multi);
|
||||
expect(M.description).toBe(Metric.description);
|
||||
});
|
||||
|
||||
test('isMetricArg type guard', () => {
|
||||
expect(isMetricArg(Metric)).toBe(true);
|
||||
expect(isMetricArg(Metric.with({ label: 'X' }))).toBe(true);
|
||||
expect(isMetricArg(Dimension)).toBe(false);
|
||||
expect(isMetricArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dimension', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Dimension.label).toBe('Dimension');
|
||||
expect(Dimension.columnType).toBe(ColumnType.Dimension);
|
||||
expect(Dimension.controlType).toBe('GroupByControl');
|
||||
expect(Dimension.multi).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const D = Dimension.with({
|
||||
label: 'Region',
|
||||
multi: false,
|
||||
});
|
||||
expect(D.label).toBe('Region');
|
||||
expect(D.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('isDimensionArg type guard', () => {
|
||||
expect(isDimensionArg(Dimension)).toBe(true);
|
||||
expect(isDimensionArg(Dimension.with({ label: 'X' }))).toBe(true);
|
||||
expect(isDimensionArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Temporal', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Temporal.label).toBe('Time Column');
|
||||
expect(Temporal.columnType).toBe(ColumnType.Temporal);
|
||||
expect(Temporal.controlType).toBe('TemporalControl');
|
||||
});
|
||||
|
||||
test('.with() overrides label and description', () => {
|
||||
const T = Temporal.with({ label: 'Order Date' });
|
||||
expect(T.label).toBe('Order Date');
|
||||
});
|
||||
|
||||
test('isTemporalArg type guard', () => {
|
||||
expect(isTemporalArg(Temporal)).toBe(true);
|
||||
expect(isTemporalArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select', () => {
|
||||
const OPTIONS = [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Select.label).toBe('Select');
|
||||
expect(Select.controlType).toBe('SelectControl');
|
||||
expect(Select.options).toEqual([]);
|
||||
expect(Select.default).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, options', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choice',
|
||||
default: 'a',
|
||||
options: OPTIONS,
|
||||
});
|
||||
expect(S.label).toBe('Choice');
|
||||
expect(S.default).toBe('a');
|
||||
expect(S.options).toEqual(OPTIONS);
|
||||
});
|
||||
|
||||
test('isSelectArg type guard', () => {
|
||||
expect(isSelectArg(Select.with({ options: OPTIONS }))).toBe(true);
|
||||
expect(isSelectArg(Checkbox)).toBe(false);
|
||||
expect(isSelectArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Text.controlType).toBe('TextControl');
|
||||
expect(Text.default).toBe('');
|
||||
expect(Text.placeholder).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter title',
|
||||
});
|
||||
expect(T.label).toBe('Title');
|
||||
expect(T.default).toBe('Untitled');
|
||||
expect(T.placeholder).toBe('Enter title');
|
||||
});
|
||||
|
||||
test('isTextArg type guard accepts Text but not Select/Checkbox', () => {
|
||||
expect(isTextArg(Text)).toBe(true);
|
||||
expect(isTextArg(Text.with({ label: 'X' }))).toBe(true);
|
||||
expect(isTextArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Checkbox.controlType).toBe('CheckboxControl');
|
||||
expect(Checkbox.default).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() applies label, description, default', () => {
|
||||
const C = Checkbox.with({
|
||||
label: 'Show legend',
|
||||
default: true,
|
||||
});
|
||||
expect(C.label).toBe('Show legend');
|
||||
expect(C.default).toBe(true);
|
||||
});
|
||||
|
||||
test('isCheckboxArg type guard', () => {
|
||||
expect(isCheckboxArg(Checkbox)).toBe(true);
|
||||
expect(isCheckboxArg(Checkbox.with({ default: true }))).toBe(true);
|
||||
expect(isCheckboxArg(Text)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Int', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Int.controlType).toBe('SliderControl');
|
||||
expect(Int.default).toBe(0);
|
||||
expect(Int.min).toBe(0);
|
||||
expect(Int.max).toBe(100);
|
||||
expect(Int.step).toBe(1);
|
||||
});
|
||||
|
||||
test('.with() applies label, default, min, max, step', () => {
|
||||
const I = Int.with({
|
||||
label: 'Limit',
|
||||
default: 50,
|
||||
min: 10,
|
||||
max: 1000,
|
||||
step: 5,
|
||||
});
|
||||
expect(I.label).toBe('Limit');
|
||||
expect(I.default).toBe(50);
|
||||
expect(I.min).toBe(10);
|
||||
expect(I.max).toBe(1000);
|
||||
expect(I.step).toBe(5);
|
||||
});
|
||||
|
||||
test('isIntArg type guard', () => {
|
||||
expect(isIntArg(Int)).toBe(true);
|
||||
expect(isIntArg(Slider)).toBe(true); // Slider also has min/max
|
||||
expect(isIntArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Color.controlType).toBe('ColorPickerControl');
|
||||
expect(Color.default).toBe('#000000');
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
expect(C.label).toBe('Fill');
|
||||
expect(C.default).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('isColorArg type guard', () => {
|
||||
expect(isColorArg(Color)).toBe(true);
|
||||
expect(isColorArg(Color.with({ default: '#ff0000' }))).toBe(true);
|
||||
expect(isColorArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumberFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(NumberFormat.controlType).toBe('NumberFormatControl');
|
||||
expect(NumberFormat.default).toBe('SMART_NUMBER');
|
||||
expect(NumberFormat.FORMAT_OPTIONS.length).toBeGreaterThan(10);
|
||||
expect(
|
||||
NumberFormat.FORMAT_OPTIONS.some(o => o.value === 'SMART_NUMBER'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const N = NumberFormat.with({ label: 'Amount', default: '.2f' });
|
||||
expect(N.label).toBe('Amount');
|
||||
expect(N.default).toBe('.2f');
|
||||
});
|
||||
|
||||
test('isNumberFormatArg type guard', () => {
|
||||
expect(isNumberFormatArg(NumberFormat)).toBe(true);
|
||||
expect(isNumberFormatArg(TimeFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currency', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Currency.controlType).toBe('CurrencyControl');
|
||||
expect(Currency.default).toEqual({});
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Currency.with({
|
||||
label: 'Money',
|
||||
default: { symbol: 'USD', symbolPosition: 'prefix' },
|
||||
});
|
||||
expect(C.label).toBe('Money');
|
||||
expect(C.default).toEqual({ symbol: 'USD', symbolPosition: 'prefix' });
|
||||
});
|
||||
|
||||
test('isCurrencyArg type guard', () => {
|
||||
expect(isCurrencyArg(Currency)).toBe(true);
|
||||
expect(isCurrencyArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimeFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(TimeFormat.controlType).toBe('TimeFormatControl');
|
||||
expect(TimeFormat.default).toBe('smart_date');
|
||||
expect(
|
||||
TimeFormat.FORMAT_OPTIONS.some(o => o.value === 'smart_date'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const T = TimeFormat.with({ label: 'When', default: '%Y-%m-%d' });
|
||||
expect(T.label).toBe('When');
|
||||
expect(T.default).toBe('%Y-%m-%d');
|
||||
});
|
||||
|
||||
test('isTimeFormatArg type guard', () => {
|
||||
expect(isTimeFormatArg(TimeFormat)).toBe(true);
|
||||
expect(isTimeFormatArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConditionalFormatting', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ConditionalFormatting.controlType).toBe(
|
||||
'ConditionalFormattingControl',
|
||||
);
|
||||
expect(ConditionalFormatting.default).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies label and description (not default)', () => {
|
||||
const CF = ConditionalFormatting.with({ label: 'Format' });
|
||||
expect(CF.label).toBe('Format');
|
||||
});
|
||||
|
||||
test('isConditionalFormattingArg type guard', () => {
|
||||
expect(isConditionalFormattingArg(ConditionalFormatting)).toBe(true);
|
||||
expect(isConditionalFormattingArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slider', () => {
|
||||
test('has expected float-friendly defaults', () => {
|
||||
expect(Slider.controlType).toBe('SliderControl');
|
||||
expect(Slider.default).toBe(0);
|
||||
expect(Slider.min).toBe(0);
|
||||
expect(Slider.max).toBe(1);
|
||||
expect(Slider.step).toBe(0.1);
|
||||
});
|
||||
|
||||
test('.with() applies all numeric fields', () => {
|
||||
const S = Slider.with({
|
||||
label: 'Opacity',
|
||||
default: 0.8,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
});
|
||||
expect(S.label).toBe('Opacity');
|
||||
expect(S.default).toBe(0.8);
|
||||
expect(S.step).toBe(0.05);
|
||||
});
|
||||
|
||||
test('isSliderArg type guard requires float step', () => {
|
||||
expect(isSliderArg(Slider)).toBe(true);
|
||||
// Int is also SliderControl + has step but step is integer-valued — still
|
||||
// numeric so the guard recognizes it (current behavior); document it.
|
||||
expect(isSliderArg(Int)).toBe(true);
|
||||
expect(isSliderArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bounds', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Bounds.controlType).toBe('BoundsControl');
|
||||
expect(Bounds.default).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const B = Bounds.with({ label: 'Range', default: [0, 100] });
|
||||
expect(B.label).toBe('Range');
|
||||
expect(B.default).toEqual([0, 100]);
|
||||
});
|
||||
|
||||
test('isBoundsArg type guard', () => {
|
||||
expect(isBoundsArg(Bounds)).toBe(true);
|
||||
expect(isBoundsArg(Int)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ColorPicker.controlType).toBe('ColorPickerControl');
|
||||
expect(ColorPicker.default).toEqual({ r: 0, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const CP = ColorPicker.with({
|
||||
label: 'Pick',
|
||||
default: { r: 255, g: 0, b: 0, a: 0.5 },
|
||||
});
|
||||
expect(CP.label).toBe('Pick');
|
||||
expect(CP.default).toEqual({ r: 255, g: 0, b: 0, a: 0.5 });
|
||||
});
|
||||
|
||||
test('isColorPickerArg distinguishes from Color (string)', () => {
|
||||
expect(isColorPickerArg(ColorPicker)).toBe(true);
|
||||
expect(isColorPickerArg(Color)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RadioButton', () => {
|
||||
const RADIO_OPTIONS = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(RadioButton.controlType).toBe('RadioButtonControl');
|
||||
expect(RadioButton.default).toBe('');
|
||||
expect(RadioButton.options).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies all fields', () => {
|
||||
const RB = RadioButton.with({
|
||||
label: 'Toggle',
|
||||
default: true,
|
||||
options: RADIO_OPTIONS,
|
||||
});
|
||||
expect(RB.label).toBe('Toggle');
|
||||
expect(RB.default).toBe(true);
|
||||
expect(RB.options).toEqual(RADIO_OPTIONS);
|
||||
});
|
||||
|
||||
test('isRadioButtonArg type guard', () => {
|
||||
expect(isRadioButtonArg(RadioButton)).toBe(true);
|
||||
expect(isRadioButtonArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Argument inheritance via .with()', () => {
|
||||
test('chained .with() calls compose overrides', () => {
|
||||
const Base = Select.with({
|
||||
label: 'Pick one',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const Tighter = Base.with({ label: 'Pick exactly one' });
|
||||
expect(Tighter.label).toBe('Pick exactly one');
|
||||
expect(Tighter.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
});
|
||||
|
||||
test('original class is unmodified after .with()', () => {
|
||||
const before = Metric.multi;
|
||||
Metric.with({ multi: !before });
|
||||
expect(Metric.multi).toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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 { ChartProps, FilterState } from '@superset-ui/core';
|
||||
import {
|
||||
createLabelMap,
|
||||
createSelectedValuesMap,
|
||||
extractCrossFilterProps,
|
||||
isDataPointFiltered,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
describe('createSelectedValuesMap', () => {
|
||||
test('returns empty object when filterState is undefined', () => {
|
||||
expect(createSelectedValuesMap(undefined, ['a', 'b'])).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is undefined', () => {
|
||||
expect(
|
||||
createSelectedValuesMap({} as FilterState, ['a', 'b']),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is empty', () => {
|
||||
expect(
|
||||
createSelectedValuesMap(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
['a', 'b'],
|
||||
),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('maps selected value to its index in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['b'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 1: 'b' });
|
||||
});
|
||||
|
||||
test('maps multiple selected values to their indices', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['a', 'c'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a', 2: 'c' });
|
||||
});
|
||||
|
||||
test('ignores selected values not in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['x', 'a'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDataPointFiltered', () => {
|
||||
test('returns false when no filterState', () => {
|
||||
expect(isDataPointFiltered(undefined, 'a')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when selectedValues is empty', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when name is in selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when name is NOT in non-empty selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'c',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLabelMap', () => {
|
||||
test('returns empty object for empty data', () => {
|
||||
expect(createLabelMap([], ['col1'], () => 'label')).toEqual({});
|
||||
});
|
||||
|
||||
test('maps each record to its label and groupby column values', () => {
|
||||
const data = [
|
||||
{ country: 'USA', region: 'North' },
|
||||
{ country: 'Brazil', region: 'South' },
|
||||
];
|
||||
const result = createLabelMap(
|
||||
data,
|
||||
['country', 'region'],
|
||||
d => d.country as string,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
USA: ['USA', 'North'],
|
||||
Brazil: ['Brazil', 'South'],
|
||||
});
|
||||
});
|
||||
|
||||
test('last record wins when extractLabel collides', () => {
|
||||
const data = [
|
||||
{ name: 'X', value: 1 },
|
||||
{ name: 'X', value: 2 },
|
||||
];
|
||||
const result = createLabelMap(data, ['value'], d => d.name as string);
|
||||
// collision: later entry overwrites
|
||||
expect(result).toEqual({ X: [2] });
|
||||
});
|
||||
|
||||
test('groupbyLabels controls the columns extracted, not the label', () => {
|
||||
const data = [{ a: 1, b: 2, c: 3 }];
|
||||
const result = createLabelMap(data, ['c'], () => 'only-key');
|
||||
expect(result).toEqual({ 'only-key': [3] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCrossFilterProps', () => {
|
||||
const baseChartProps = {
|
||||
hooks: { setDataMask: jest.fn(), onContextMenu: jest.fn() },
|
||||
filterState: {
|
||||
selectedValues: ['USA'],
|
||||
} as unknown as FilterState,
|
||||
emitCrossFilters: true,
|
||||
formData: { viz_type: 'test' },
|
||||
} as unknown as ChartProps;
|
||||
|
||||
test('returns all expected fields', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{ USA: ['USA'] },
|
||||
['USA', 'Brazil'],
|
||||
);
|
||||
expect(result.groupby).toEqual(['country']);
|
||||
expect(result.labelMap).toEqual({ USA: ['USA'] });
|
||||
expect(result.selectedValues).toEqual({ 0: 'USA' });
|
||||
expect(result.emitCrossFilters).toBe(true);
|
||||
expect(result.setDataMask).toBe(baseChartProps.hooks!.setDataMask);
|
||||
expect(result.onContextMenu).toBe(baseChartProps.hooks!.onContextMenu);
|
||||
});
|
||||
|
||||
test('coltypeMapping pass-through when provided', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
{ country: 1 },
|
||||
);
|
||||
expect(result.coltypeMapping).toEqual({ country: 1 });
|
||||
});
|
||||
|
||||
test('defaults setDataMask to a no-op when hooks omits it', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
hooks: {},
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, []);
|
||||
expect(typeof result.setDataMask).toBe('function');
|
||||
// No throw when invoked
|
||||
expect(() =>
|
||||
result.setDataMask({ filterState: {} } as unknown as Parameters<
|
||||
typeof result.setDataMask
|
||||
>[0]),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('formData is included in the returned shape (for context menu formatting)', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
) as ReturnType<typeof extractCrossFilterProps> & { formData: unknown };
|
||||
expect(result.formData).toEqual({ viz_type: 'test' });
|
||||
});
|
||||
|
||||
test('selectedValues is empty when filterState has none', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
filterState: {} as FilterState,
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, ['x']);
|
||||
expect(result.selectedValues).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,789 @@
|
||||
/**
|
||||
* 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 {
|
||||
Behavior,
|
||||
ChartLabel,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
Bounds,
|
||||
Checkbox,
|
||||
ColorPicker,
|
||||
defineChart,
|
||||
Dimension,
|
||||
evaluateGlyphCondition,
|
||||
getArgVisibleWhen,
|
||||
getGlyphControlConfig,
|
||||
Metric,
|
||||
RadioButton,
|
||||
resolveArgClass,
|
||||
Select,
|
||||
Temporal,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import type { ChartDefinition } from '@superset-ui/glyph-core/defineChart';
|
||||
|
||||
// Helper: instantiate a plugin and reach its controlPanel config.
|
||||
function instantiate(PluginClass: ReturnType<typeof defineChart>) {
|
||||
const plugin = new PluginClass();
|
||||
// ChartPlugin internals expose the panel under .controlPanel
|
||||
// (set via super({ controlPanel }) in defineChart's GlyphChartPlugin).
|
||||
return {
|
||||
plugin,
|
||||
controlPanel: (
|
||||
plugin as unknown as { controlPanel: Record<string, unknown> }
|
||||
).controlPanel,
|
||||
metadata: (plugin as unknown as { metadata: ChartMetadata }).metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const MIN_THUMBNAIL = 'thumb.png';
|
||||
|
||||
describe('resolveArgClass', () => {
|
||||
test('returns the bare class form unchanged', () => {
|
||||
expect(resolveArgClass(Metric)).toBe(Metric);
|
||||
});
|
||||
|
||||
test('unwraps the { arg, visibleWhen } object form', () => {
|
||||
const M = Metric.with({ label: 'Sales' });
|
||||
const argDef = { arg: M, visibleWhen: { show: true } };
|
||||
expect(resolveArgClass(argDef)).toBe(M);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArgVisibleWhen', () => {
|
||||
test('returns undefined for bare class form', () => {
|
||||
expect(getArgVisibleWhen(Metric)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns the condition for object form', () => {
|
||||
const argDef = { arg: Metric, visibleWhen: { show: true } };
|
||||
expect(getArgVisibleWhen(argDef)).toEqual({ show: true });
|
||||
});
|
||||
|
||||
test('returns undefined when object form has no visibleWhen', () => {
|
||||
expect(getArgVisibleWhen({ arg: Metric })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateGlyphCondition', () => {
|
||||
test('returns true for empty condition', () => {
|
||||
expect(evaluateGlyphCondition({}, { foo: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when equality check matches', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: true })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when equality check fails', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: false })).toBe(false);
|
||||
});
|
||||
|
||||
test('handles missing formData keys as undefined', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, {})).toBe(false);
|
||||
});
|
||||
|
||||
test('supports function-valued conditions', () => {
|
||||
const cond = { subtitle: (val: unknown) => !!val };
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: 'hi' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: '' })).toBe(false);
|
||||
});
|
||||
|
||||
test('requires all keys in the condition to pass (AND semantics)', () => {
|
||||
const cond = { a: true, b: 'x' };
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'x' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'y' })).toBe(false);
|
||||
expect(evaluateGlyphCondition(cond, { a: false, b: 'x' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - basic plugin construction', () => {
|
||||
test('returns a ChartPlugin subclass', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const p = new Plugin();
|
||||
expect(p).toBeInstanceOf(ChartPlugin);
|
||||
});
|
||||
|
||||
test('plugin metadata is a ChartMetadata instance with required fields', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
description: 'A test chart',
|
||||
category: 'Charts',
|
||||
tags: ['test'],
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata).toBeInstanceOf(ChartMetadata);
|
||||
expect(metadata.name).toBe('Test');
|
||||
expect(metadata.description).toBe('A test chart');
|
||||
expect(metadata.category).toBe('Charts');
|
||||
expect(metadata.tags).toEqual(['test']);
|
||||
expect(metadata.thumbnail).toBe(MIN_THUMBNAIL);
|
||||
});
|
||||
|
||||
test('metadata defaults behaviors to [] when omitted (matching ChartMetadata)', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
// Charts must opt in to InteractiveChart — defaulting it on would enroll
|
||||
// non-interactive charts in dashboard cross-filter UI and configuration.
|
||||
expect(metadata.behaviors).toEqual([]);
|
||||
});
|
||||
|
||||
test('metadata forwards queryObjectCount and related fields', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
queryObjectCount: 2,
|
||||
suppressContextMenu: true,
|
||||
enableNoResults: false,
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.queryObjectCount).toBe(2);
|
||||
expect(metadata.suppressContextMenu).toBe(true);
|
||||
expect(metadata.enableNoResults).toBe(false);
|
||||
});
|
||||
|
||||
test('metadata behaviors override the default when provided', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toEqual([
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
]);
|
||||
});
|
||||
|
||||
test('passes label, canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes through', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
label: ChartLabel.Deprecated,
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
useLegacyApi: true,
|
||||
supportedAnnotationTypes: ['FORMULA'],
|
||||
credits: ['https://example.com'],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.label).toBe(ChartLabel.Deprecated);
|
||||
expect(metadata.canBeAnnotationTypes).toEqual(['EVENT', 'INTERVAL']);
|
||||
expect(metadata.useLegacyApi).toBe(true);
|
||||
expect(metadata.supportedAnnotationTypes).toEqual(['FORMULA']);
|
||||
expect(metadata.credits).toEqual(['https://example.com']);
|
||||
});
|
||||
|
||||
test('exampleGallery + thumbnailDark are preserved', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
thumbnailDark: 'thumb-dark.png',
|
||||
exampleGallery: [{ url: 'a.png', urlDark: 'a-dark.png' }],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.thumbnailDark).toBe('thumb-dark.png');
|
||||
expect(metadata.exampleGallery).toEqual([
|
||||
{ url: 'a.png', urlDark: 'a-dark.png' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - controlPanel generation from arguments', () => {
|
||||
test('Query section is auto-generated from Metric/Dimension arguments', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// Query section should be auto-generated
|
||||
expect(sections.some(s => s?.label === 'Query')).toBe(true);
|
||||
});
|
||||
|
||||
test('suppressQuerySection: true skips the auto Query section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
},
|
||||
suppressQuerySection: true,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// The auto-generated Query section is suppressed.
|
||||
// (Charts using suppressQuerySection typically provide their own via
|
||||
// prependSections — see legacy nvd3 / deckgl consolidations.)
|
||||
const autoQuery = sections.find(s => s?.label === 'Query');
|
||||
expect(autoQuery).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Chart Options section is generated when there are non-data args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
|
||||
test('Chart Options section is hidden when there are no customize args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// No Customize-tab content → Chart Options auto-hides.
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - prependSections / middleSections / additionalSections', () => {
|
||||
test('prependSections appears before the auto Query section', () => {
|
||||
const TIME_SECTION = {
|
||||
label: 'Time',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
prependSections: [TIME_SECTION],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const timeIdx = sections.findIndex(s => s?.label === 'Time');
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
expect(timeIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(queryIdx).toBeGreaterThan(timeIdx);
|
||||
});
|
||||
|
||||
test('additionalSections appears after Chart Options', () => {
|
||||
const TIME_COMP = {
|
||||
label: 'Time Comparison',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
additionalSections: [TIME_COMP],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
const timeCompIdx = sections.findIndex(s => s?.label === 'Time Comparison');
|
||||
expect(chartOptsIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(timeCompIdx).toBeGreaterThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('middleSections appears between Query and Chart Options', () => {
|
||||
const MIDDLE = {
|
||||
label: 'Middle',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
middleSections: [MIDDLE],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
const middleIdx = sections.findIndex(s => s?.label === 'Middle');
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
expect(queryIdx).toBeLessThan(middleIdx);
|
||||
expect(middleIdx).toBeLessThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('chartOptionsTabOverride sets tabOverride on the generated section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
chartOptionsTabOverride: 'data',
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
tabOverride?: string;
|
||||
}>;
|
||||
const chartOpts = sections.find(s => s?.label === 'Chart Options');
|
||||
expect(chartOpts?.tabOverride).toBe('data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - overrides + formDataOverrides + onInit', () => {
|
||||
test('additionalControlOverrides land on controlPanel.controlOverrides', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
additionalControlOverrides: {
|
||||
size: { label: 'Custom Size Label' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(
|
||||
(controlPanel.controlOverrides as Record<string, unknown>)?.size,
|
||||
).toEqual({ label: 'Custom Size Label' });
|
||||
});
|
||||
|
||||
test('controlOverrides + additionalControlOverrides merge', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
controlOverrides: {
|
||||
a: { label: 'A' },
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
b: { label: 'B' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const merged = controlPanel.controlOverrides as Record<string, unknown>;
|
||||
expect(merged.a).toEqual({ label: 'A' });
|
||||
expect(merged.b).toEqual({ label: 'B' });
|
||||
});
|
||||
|
||||
test('formDataOverrides is preserved on controlPanel', () => {
|
||||
const fdo = ((formData: Record<string, unknown>) => ({
|
||||
...formData,
|
||||
custom: 'extra',
|
||||
})) as unknown as ChartDefinition<
|
||||
Record<string, never>
|
||||
>['formDataOverrides'];
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
formDataOverrides: fdo,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('onInit is preserved on controlPanel', () => {
|
||||
const onInit = (state: unknown) => state;
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
onInit,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.onInit).toBe(onInit);
|
||||
});
|
||||
|
||||
test('_glyphArgs is attached to the controlPanel for native rendering', () => {
|
||||
const args = {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: args,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toEqual(args);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - custom buildQuery / transform', () => {
|
||||
test('custom buildQuery is invoked via the plugin loader', async () => {
|
||||
const customBuildQuery = jest.fn(() => ({ queries: [{ marker: 1 }] }));
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
buildQuery: customBuildQuery as unknown as never,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
// ChartPlugin stores it as a sanitized loader
|
||||
const loader = (
|
||||
p as unknown as { loadBuildQuery?: () => Promise<Function> }
|
||||
).loadBuildQuery;
|
||||
expect(loader).toBeDefined();
|
||||
const fn = await (loader as () => Promise<Function>)();
|
||||
fn({ viz_type: 'test', datasource: '1__table' });
|
||||
expect(customBuildQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('transform receives chartProps and argValues', async () => {
|
||||
const captured: unknown[] = [];
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
transform: (chartProps, argValues) => {
|
||||
captured.push({ chartProps, argValues });
|
||||
return { transformed: true };
|
||||
},
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
const loader = (
|
||||
p as unknown as { loadTransformProps: () => Promise<Function> }
|
||||
).loadTransformProps;
|
||||
const transformProps = await loader();
|
||||
transformProps({
|
||||
width: 100,
|
||||
height: 100,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [] }],
|
||||
});
|
||||
expect(captured).toHaveLength(1);
|
||||
expect((captured[0] as { chartProps: unknown }).chartProps).toBeDefined();
|
||||
expect((captured[0] as { argValues: unknown }).argValues).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - Text-only argument behavior', () => {
|
||||
test('a Text-only chart still wires up a working plugin', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'TextOnly', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
title: Text.with({ label: 'Title', default: 'Hi' }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel, metadata } = instantiate(Plugin);
|
||||
expect(metadata.name).toBe('TextOnly');
|
||||
// Customize args present → Chart Options shows up
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - visibleWhen with object-form ArgDef', () => {
|
||||
test('attaches a visibility derivation to the underlying control', () => {
|
||||
// Build a plugin where one arg is visibleWhen another is true.
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'V', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
legendPosition: {
|
||||
arg: Select.with({
|
||||
label: 'Position',
|
||||
default: 'right',
|
||||
options: [{ label: 'R', value: 'right' }],
|
||||
}),
|
||||
visibleWhen: { showLegend: true },
|
||||
},
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toBeDefined();
|
||||
const glyphArgs = controlPanel._glyphArgs as Record<string, unknown>;
|
||||
// The visibleWhen is preserved on the glyph args
|
||||
const lp = glyphArgs.legendPosition as { visibleWhen?: unknown };
|
||||
expect(lp.visibleWhen).toEqual({ showLegend: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - getGlyphControlConfig argument class coverage', () => {
|
||||
test('RadioButton maps to RadioButtonControl (not SelectControl)', () => {
|
||||
const Choice = RadioButton.with({
|
||||
label: 'Mode',
|
||||
default: 'a',
|
||||
options: [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
],
|
||||
});
|
||||
const config = getGlyphControlConfig(Choice, 'mode');
|
||||
expect(config.type).toBe('RadioButtonControl');
|
||||
expect(config.default).toBe('a');
|
||||
expect(config.options).toEqual([
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('ColorPicker keeps its RGBA object default (no hex coercion)', () => {
|
||||
const red = { r: 255, g: 0, b: 0, a: 1 };
|
||||
const Picker = ColorPicker.with({ label: 'Stroke', default: red });
|
||||
const config = getGlyphControlConfig(Picker, 'stroke');
|
||||
expect(config.type).toBe('ColorPickerControl');
|
||||
expect(config.default).toEqual(red);
|
||||
});
|
||||
|
||||
test('Bounds maps to BoundsControl (not TextControl)', () => {
|
||||
const Range = Bounds.with({ label: 'Y bounds', default: [0, 100] });
|
||||
const config = getGlyphControlConfig(Range, 'y_bounds');
|
||||
expect(config.type).toBe('BoundsControl');
|
||||
expect(config.default).toEqual([0, 100]);
|
||||
});
|
||||
|
||||
test('Select honors clearable from the argument class', () => {
|
||||
const config = getGlyphControlConfig(
|
||||
Select.with({ label: 'S', options: [{ label: 'A', value: 'a' }] }),
|
||||
's',
|
||||
);
|
||||
expect(config.clearable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - disabledWhen wiring', () => {
|
||||
test('mapStateToProps reads controls from the state (first) argument', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'D', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
subtitle: Text.with({ label: 'Subtitle', default: '' }),
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [{ label: 'M', value: 'm' }],
|
||||
}),
|
||||
disabledWhen: { subtitle: (val: unknown) => !val },
|
||||
},
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
controlSetRows: Array<
|
||||
Array<{ name: string; config: Record<string, Function> }>
|
||||
>;
|
||||
}>;
|
||||
const control = sections
|
||||
.flatMap(s => s.controlSetRows)
|
||||
.flat()
|
||||
.find(c => c?.name === 'subtitleFontSize');
|
||||
expect(control).toBeDefined();
|
||||
const { mapStateToProps, shouldMapStateToProps } = control!.config;
|
||||
expect(shouldMapStateToProps()).toBe(true);
|
||||
|
||||
// Both real call sites pass the controls-bearing state as the FIRST arg
|
||||
const disabledResult = mapStateToProps(
|
||||
{ controls: { subtitle: { value: '' } } },
|
||||
{ value: 'm' },
|
||||
);
|
||||
expect(disabledResult.disabled).toBe(true);
|
||||
|
||||
const enabledResult = mapStateToProps(
|
||||
{ controls: { subtitle: { value: 'hello' } } },
|
||||
{ value: 'm' },
|
||||
);
|
||||
expect(enabledResult.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - generated buildQuery temporal axis', () => {
|
||||
test('x_axis column is normalized to BASE_AXIS with timeGrain', async () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'T', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { time: Temporal, metric: Metric },
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
const loader = (p as unknown as { loadBuildQuery: () => Promise<Function> })
|
||||
.loadBuildQuery;
|
||||
const buildQuery = await loader();
|
||||
const queryContext = buildQuery({
|
||||
datasource: '1__table',
|
||||
viz_type: 'test',
|
||||
x_axis: 'ds',
|
||||
time_grain_sqla: 'P1D',
|
||||
metrics: ['count'],
|
||||
});
|
||||
const axisColumn = queryContext.queries[0].columns.find(
|
||||
(col: { columnType?: string }) =>
|
||||
typeof col === 'object' && col?.columnType === 'BASE_AXIS',
|
||||
);
|
||||
expect(axisColumn).toBeDefined();
|
||||
expect(axisColumn.sqlExpression).toBe('ds');
|
||||
// The grain selected in the control panel must reach the query
|
||||
expect(axisColumn.timeGrain).toBe('P1D');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - generated metric extraction', () => {
|
||||
async function getTransformProps(
|
||||
PluginClass: ReturnType<typeof defineChart>,
|
||||
) {
|
||||
const p = new PluginClass();
|
||||
const loader = (
|
||||
p as unknown as { loadTransformProps: () => Promise<Function> }
|
||||
).loadTransformProps;
|
||||
return loader();
|
||||
}
|
||||
|
||||
const MetricPlugin = defineChart({
|
||||
metadata: { name: 'M', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
test('label-less SQL adhoc metrics resolve via sqlExpression', async () => {
|
||||
const transformProps = await getTransformProps(MetricPlugin);
|
||||
const props = transformProps({
|
||||
width: 1,
|
||||
height: 1,
|
||||
formData: {
|
||||
metric: { expressionType: 'SQL', sqlExpression: 'SUM(x)/SUM(y)' },
|
||||
},
|
||||
queriesData: [{ data: [{ 'SUM(x)/SUM(y)': 42 }] }],
|
||||
});
|
||||
expect(props.metric.name).toBe('SUM(x)/SUM(y)');
|
||||
expect(props.metric.value).toBe(42);
|
||||
});
|
||||
|
||||
test('does NOT guess among multiple numeric columns on label miss', async () => {
|
||||
const transformProps = await getTransformProps(MetricPlugin);
|
||||
const props = transformProps({
|
||||
width: 1,
|
||||
height: 1,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [{ year: 2024, total: 9 }] }],
|
||||
});
|
||||
// Guessing could render a numeric dimension (e.g. a year) as the metric
|
||||
expect(props.metric.value).toBeUndefined();
|
||||
});
|
||||
|
||||
test('falls back to the only numeric column when unambiguous', async () => {
|
||||
const transformProps = await getTransformProps(MetricPlugin);
|
||||
const props = transformProps({
|
||||
width: 1,
|
||||
height: 1,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [{ name: 'a', total: 9 }] }],
|
||||
});
|
||||
expect(props.metric.value).toBe(9);
|
||||
expect(props.metric.name).toBe('total');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - Chart Options section marker', () => {
|
||||
test('the generated section carries the structural _glyphChartOptions flag', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'S', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { title: Text.with({ label: 'Title', default: '' }) },
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
_glyphChartOptions?: boolean;
|
||||
}>;
|
||||
const chartOptions = sections.find(s => s?._glyphChartOptions);
|
||||
expect(chartOptions).toBeDefined();
|
||||
expect(chartOptions?.label).toBe('Chart Options');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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 {
|
||||
Checkbox,
|
||||
CircleShape,
|
||||
DataZoom,
|
||||
ForceTimestampFormatting,
|
||||
HeaderFontSize,
|
||||
isCheckboxArg,
|
||||
isSelectArg,
|
||||
isTextArg,
|
||||
LabelPosition,
|
||||
LabelType,
|
||||
LabelThreshold,
|
||||
LegendOrientation,
|
||||
LegendSort,
|
||||
LegendType,
|
||||
MetricNameFontSize,
|
||||
Select,
|
||||
ShowLabels,
|
||||
ShowLegend,
|
||||
ShowMetricName,
|
||||
ShowTotal,
|
||||
ShowValue,
|
||||
SimpleLabelType,
|
||||
SortByMetric,
|
||||
Subtitle,
|
||||
SubheaderFontSize,
|
||||
Text,
|
||||
ValueLabelType,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import {
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
LABEL_TYPE_OPTIONS,
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
LEGEND_SORT_OPTIONS,
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
SORT_OPTIONS,
|
||||
} from '@superset-ui/glyph-core/presets';
|
||||
|
||||
describe('Font-size presets', () => {
|
||||
test('HeaderFontSize is a Select with large font options', () => {
|
||||
expect(isSelectArg(HeaderFontSize)).toBe(true);
|
||||
expect((HeaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('SubheaderFontSize is a Select with small font options', () => {
|
||||
expect(isSelectArg(SubheaderFontSize)).toBe(true);
|
||||
expect((SubheaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
});
|
||||
|
||||
test('FONT_SIZE_OPTIONS_LARGE and _SMALL are non-empty option arrays', () => {
|
||||
expect(FONT_SIZE_OPTIONS_LARGE.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_SMALL.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('label');
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('value');
|
||||
});
|
||||
|
||||
test('MetricNameFontSize is a Select preset', () => {
|
||||
expect(isSelectArg(MetricNameFontSize)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text presets', () => {
|
||||
test('Subtitle is a Text preset', () => {
|
||||
expect(isTextArg(Subtitle)).toBe(true);
|
||||
expect(Subtitle.prototype).toBeInstanceOf(Text);
|
||||
});
|
||||
|
||||
test('LabelThreshold is a Text preset', () => {
|
||||
expect(isTextArg(LabelThreshold)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox presets', () => {
|
||||
test.each([
|
||||
['ShowLegend', ShowLegend],
|
||||
['ShowLabels', ShowLabels],
|
||||
['ShowValue', ShowValue],
|
||||
['ShowMetricName', ShowMetricName],
|
||||
['ShowTotal', ShowTotal],
|
||||
['SortByMetric', SortByMetric],
|
||||
['CircleShape', CircleShape],
|
||||
['DataZoom', DataZoom],
|
||||
['ForceTimestampFormatting', ForceTimestampFormatting],
|
||||
])('%s is a Checkbox preset', (_name, preset) => {
|
||||
expect(isCheckboxArg(preset)).toBe(true);
|
||||
expect(preset.prototype).toBeInstanceOf(Checkbox);
|
||||
});
|
||||
|
||||
test('Checkbox presets have a label and a description', () => {
|
||||
[ShowLegend, ShowLabels, ShowValue, ShowMetricName, ShowTotal].forEach(
|
||||
preset => {
|
||||
expect(preset.label).toBeTruthy();
|
||||
expect(preset.description).toBeTruthy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legend Select presets', () => {
|
||||
test('LegendType uses LEGEND_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendType)).toBe(true);
|
||||
expect((LegendType as unknown as typeof Select).options).toBe(
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendOrientation uses LEGEND_ORIENTATION_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendOrientation)).toBe(true);
|
||||
expect((LegendOrientation as unknown as typeof Select).options).toBe(
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendSort uses LEGEND_SORT_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendSort)).toBe(true);
|
||||
expect((LegendSort as unknown as typeof Select).options).toBe(
|
||||
LEGEND_SORT_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('legend option sets are non-empty', () => {
|
||||
expect(LEGEND_TYPE_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_ORIENTATION_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Label / value-label Select presets', () => {
|
||||
test('LabelType is a Select with LABEL_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LabelType)).toBe(true);
|
||||
expect((LabelType as unknown as typeof Select).options).toBe(
|
||||
LABEL_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('SimpleLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(SimpleLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('ValueLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(ValueLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('LabelPosition is a Select preset', () => {
|
||||
expect(isSelectArg(LabelPosition)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sort options', () => {
|
||||
test('SORT_OPTIONS is non-empty', () => {
|
||||
expect(SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('label');
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Preset extensibility', () => {
|
||||
test('ShowLegend.with() overrides label while keeping the Checkbox shape', () => {
|
||||
const Custom = ShowLegend.with({
|
||||
label: 'Display legend',
|
||||
default: false,
|
||||
});
|
||||
expect(isCheckboxArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Display legend');
|
||||
expect(Custom.default).toBe(false);
|
||||
});
|
||||
|
||||
test('HeaderFontSize.with() overrides label, default keeps options', () => {
|
||||
const Custom = HeaderFontSize.with({
|
||||
label: 'Title size',
|
||||
default: 0.4,
|
||||
});
|
||||
expect(isSelectArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Title size');
|
||||
expect(Custom.default).toBe(0.4);
|
||||
expect((Custom as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
||||
"baseUrl": "../..",
|
||||
|
||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
||||
// but packages need paths relative to their own directory
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"declarationDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
|
||||
"references": [
|
||||
{ "path": "../superset-core" },
|
||||
{ "path": "../superset-ui-core" },
|
||||
{ "path": "../superset-ui-chart-controls" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Smoke test: Glyph-defined chart's Customize tab renders via GlyphOptionsPanel.
|
||||
*
|
||||
* Glyph charts (those built with defineChart) replace the legacy controlPanel.ts
|
||||
* file with declarative arg definitions. Their Customize tab is rendered by
|
||||
* GlyphOptionsPanel (a native React renderer) instead of the legacy
|
||||
* ControlSetRow pipeline. This test verifies that path actually works end-to-end:
|
||||
* open the Pie chart explore page, switch to Customize, and confirm a known
|
||||
* glyph control (Show legend) is rendered.
|
||||
*/
|
||||
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { ExplorePage } from '../../pages/ExplorePage';
|
||||
import { apiPostChart } from '../../helpers/api/chart';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
|
||||
const test = testWithAssets;
|
||||
|
||||
test('Pie chart Customize tab renders glyph controls', async ({
|
||||
page,
|
||||
testAssets,
|
||||
}) => {
|
||||
// 1. Create a real Pie chart via API so we have something to load.
|
||||
const dataset = await getDatasetByName(page, 'members_channels_2');
|
||||
if (!dataset) {
|
||||
throw new Error(
|
||||
'members_channels_2 dataset not found — run Superset with --load-examples',
|
||||
);
|
||||
}
|
||||
|
||||
const name = `glyph_pie_${Date.now()}_${test.info().parallelIndex}`;
|
||||
const response = await apiPostChart(page, {
|
||||
slice_name: name,
|
||||
datasource_id: dataset.id,
|
||||
datasource_type: 'table',
|
||||
viz_type: 'pie',
|
||||
params: JSON.stringify({
|
||||
viz_type: 'pie',
|
||||
groupby: ['channel'],
|
||||
metric: {
|
||||
aggregate: 'COUNT',
|
||||
column: null,
|
||||
expressionType: 'SIMPLE',
|
||||
label: 'count',
|
||||
},
|
||||
adhoc_filters: [],
|
||||
row_limit: 100,
|
||||
show_legend: true,
|
||||
}),
|
||||
});
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create Pie chart: ${response.status()}`);
|
||||
}
|
||||
const body = await response.json();
|
||||
const id = body.result?.id ?? body.id;
|
||||
testAssets.trackChart(id);
|
||||
|
||||
// 2. Open the Explore page for this chart.
|
||||
await page.goto(`/explore/?slice_id=${id}`);
|
||||
|
||||
const explore = new ExplorePage(page);
|
||||
await explore.waitForPageLoad();
|
||||
|
||||
// 3. Switch to the Customize tab. This is what triggers GlyphOptionsPanel
|
||||
// to render — without it we'd be looking at the Data tab.
|
||||
await page.getByRole('tab', { name: 'Customize' }).click();
|
||||
|
||||
// 4. The Customize tab should show at least one Chart Options collapse header.
|
||||
// Both the section label and a known glyph arg (Show legend) belong to Pie,
|
||||
// so we assert on the arg label to prove the native glyph renderer ran.
|
||||
await expect(page.getByText('Show legend').first()).toBeVisible();
|
||||
});
|
||||
@@ -32,8 +32,9 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,205 +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 { legacyValidateInteger } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
{
|
||||
name: 'cell_size',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
default: 10,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
label: t('Cell Size'),
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cell_padding',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
label: t('Cell Padding'),
|
||||
description: t('The distance between cells, in pixels'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'cell_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 0,
|
||||
label: t('Cell Radius'),
|
||||
description: t('The pixel radius'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'steps',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 10,
|
||||
label: t('Color Steps'),
|
||||
description: t('The number color "steps"'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'y_axis_format',
|
||||
{
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_legend',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Legend'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_values',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Values'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Names'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
},
|
||||
},
|
||||
null,
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,58 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Calendar Heatmap'),
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CalendarChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCalendar'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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 { getNumberFormatter } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
defineChart,
|
||||
Int,
|
||||
Checkbox,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCalendar = require('./ReactCalendar').default;
|
||||
|
||||
type CalendarExtra = {
|
||||
timeFormatter: (ts: number | string) => string;
|
||||
valueFormatter: (val: unknown) => string;
|
||||
verboseMap: Record<string, string>;
|
||||
domainGranularity: string;
|
||||
subdomainGranularity: string;
|
||||
linearColorScheme: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CalendarExtra>({
|
||||
metadata: {
|
||||
name: t('Calendar Heatmap'),
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
},
|
||||
arguments: {
|
||||
cell_size: Int.with({
|
||||
label: 'Cell Size',
|
||||
description: 'The size of the square cell, in pixels',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 100,
|
||||
}),
|
||||
cell_padding: Int.with({
|
||||
label: 'Cell Padding',
|
||||
description: 'The distance between cells, in pixels',
|
||||
default: 2,
|
||||
min: 0,
|
||||
max: 20,
|
||||
}),
|
||||
cell_radius: Int.with({
|
||||
label: 'Cell Radius',
|
||||
description: 'The pixel radius',
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 50,
|
||||
}),
|
||||
steps: Int.with({
|
||||
label: 'Color Steps',
|
||||
description: 'The number color "steps"',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 50,
|
||||
}),
|
||||
x_axis_time_format: TimeFormat.with({
|
||||
label: 'Time Format',
|
||||
description: D3_FORMAT_DOCS,
|
||||
default: 'smart_date',
|
||||
}),
|
||||
show_legend: Checkbox.with({
|
||||
label: 'Legend',
|
||||
description: 'Whether to display the legend (toggles)',
|
||||
default: true,
|
||||
}),
|
||||
show_values: Checkbox.with({
|
||||
label: 'Show Values',
|
||||
description: 'Whether to display the numerical values within the cells',
|
||||
default: false,
|
||||
}),
|
||||
show_metric_name: Checkbox.with({
|
||||
label: 'Show Metric Names',
|
||||
description: 'Whether to display the metric name as a title',
|
||||
default: true,
|
||||
}),
|
||||
},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
],
|
||||
chartOptions: [['linear_color_scheme'], ['y_axis_format']],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
transform: (chartProps, { x_axis_time_format }) => {
|
||||
const { formData, datasource } = chartProps;
|
||||
const {
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
yAxisFormat,
|
||||
} = formData as Record<string, string>;
|
||||
|
||||
const verboseMap =
|
||||
(datasource as { verboseMap?: Record<string, string> })?.verboseMap ?? {};
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, x_axis_time_format as string);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
timeFormatter,
|
||||
valueFormatter: valueFormatter as (val: unknown) => string,
|
||||
verboseMap,
|
||||
domainGranularity: domainGranularity ?? 'month',
|
||||
subdomainGranularity: subdomainGranularity ?? 'day',
|
||||
linearColorScheme: linearColorScheme ?? '',
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
height,
|
||||
data,
|
||||
cell_size: cellSize,
|
||||
cell_padding: cellPadding,
|
||||
cell_radius: cellRadius,
|
||||
steps,
|
||||
show_legend: showLegend,
|
||||
show_values: showValues,
|
||||
show_metric_name: showMetricName,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
}) => (
|
||||
<ReactCalendar
|
||||
height={height}
|
||||
data={data}
|
||||
cellSize={cellSize}
|
||||
cellPadding={cellPadding}
|
||||
cellRadius={cellRadius}
|
||||
steps={steps}
|
||||
showLegend={showLegend}
|
||||
showValues={showValues}
|
||||
showMetricName={showMetricName}
|
||||
timeFormatter={timeFormatter}
|
||||
valueFormatter={valueFormatter}
|
||||
verboseMap={verboseMap}
|
||||
domainGranularity={domainGranularity}
|
||||
subdomainGranularity={subdomainGranularity}
|
||||
linearColorScheme={linearColorScheme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,62 +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 { ChartProps, getNumberFormatter } from '@superset-ui/core';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
xAxisTimeFormat,
|
||||
yAxisFormat,
|
||||
} = formData;
|
||||
|
||||
const { verboseMap } = datasource;
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*"
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +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 { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
['columns'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
['row_limit'],
|
||||
['sort_by_metric'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(col => !ensureIsArray(formData.columns).includes(col));
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,57 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
name: t('Chord Diagram'),
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ChordChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactChord'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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 { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import { getStandardizedControls } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactChord = require('./ReactChord').default;
|
||||
|
||||
type ChordExtra = {
|
||||
colorScheme: string;
|
||||
numberFormat: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, ChordExtra>({
|
||||
metadata: {
|
||||
name: t('Chord Diagram'),
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [['groupby'], ['columns'], ['metric']],
|
||||
query: [['row_limit'], ['sort_by_metric']],
|
||||
chartOptions: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(
|
||||
(col: string) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!ensureIsArray((formData as any).columns).includes(col),
|
||||
);
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData as Record<
|
||||
string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any
|
||||
>;
|
||||
return {
|
||||
colorScheme: colorScheme ?? '',
|
||||
numberFormat: yAxisFormat ?? '',
|
||||
sliceId: sliceId ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScheme, numberFormat, sliceId }) => (
|
||||
<ReactChord
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScheme={colorScheme}
|
||||
numberFormat={numberFormat}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,33 +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 { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData;
|
||||
|
||||
return {
|
||||
colorScheme,
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
numberFormat: yAxisFormat,
|
||||
width,
|
||||
sliceId,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +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 { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['currency_format'],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,65 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
name: t('Country Map'),
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CountryMapChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCountryMap'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { default as countries } from './countries';
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCountryMap = require('./ReactCountryMap').default;
|
||||
|
||||
export { default as countries } from './countries';
|
||||
|
||||
type CountryMapExtra = {
|
||||
country: string | null;
|
||||
linearColorScheme: string;
|
||||
numberFormat: string;
|
||||
colorScheme: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CountryMapExtra>({
|
||||
metadata: {
|
||||
name: t('Country Map'),
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
} = formData as Record<string, unknown>;
|
||||
return {
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme: (linearColorScheme as string) ?? '',
|
||||
numberFormat: (numberFormat as string) ?? '',
|
||||
colorScheme: (colorScheme as string) ?? '',
|
||||
sliceId: (sliceId as number) ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
country,
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
}) => (
|
||||
<ReactCountryMap
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
country={country}
|
||||
linearColorScheme={linearColorScheme}
|
||||
numberFormat={numberFormat}
|
||||
colorScheme={colorScheme}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,63 +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 { ChartProps, getValueFormatter } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
metric,
|
||||
} = formData;
|
||||
|
||||
const {
|
||||
currencyFormats = {},
|
||||
columnFormats = {},
|
||||
currencyCodeColumn,
|
||||
} = datasource;
|
||||
const { data, detected_currency: detectedCurrency } = queriesData[0];
|
||||
|
||||
const formatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
undefined, // key - not needed for single-metric charts
|
||||
data,
|
||||
currencyCodeColumn,
|
||||
detectedCurrency,
|
||||
);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme,
|
||||
numberFormat, // left for backward compatibility
|
||||
colorScheme,
|
||||
sliceId,
|
||||
formatter,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,105 +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 {
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,51 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Horizon Chart'),
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class HorizonChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./HorizonChart'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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 { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HorizonChart = require('./HorizonChart').default;
|
||||
|
||||
type HorizonExtra = {
|
||||
colorScale: string;
|
||||
seriesHeight: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, HorizonExtra>({
|
||||
metadata: {
|
||||
name: t('Horizon Chart'),
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [['metrics']],
|
||||
query: [
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { horizonColorScale, seriesHeight } = formData as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
return {
|
||||
colorScale: horizonColorScale ?? 'series',
|
||||
seriesHeight: parseInt(seriesHeight ?? '25', 10),
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScale, seriesHeight }) => (
|
||||
<HorizonChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScale={colorScale}
|
||||
seriesHeight={seriesHeight}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -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 { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, width, formData, queriesData } = chartProps;
|
||||
const {
|
||||
horizon_color_scale: horizonColorScale,
|
||||
series_height: seriesHeight,
|
||||
} = formData;
|
||||
|
||||
// Only include colorScale if defined, otherwise let defaultProps apply
|
||||
return {
|
||||
...(horizonColorScale !== undefined && {
|
||||
colorScale: horizonColorScale as string,
|
||||
}),
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
seriesHeight: parseInt(String(seriesHeight ?? 20), 10),
|
||||
width,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
declare module "*.png" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
declare module "*.jpg" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,103 +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 { validateNonEmpty } from '@superset-ui/core';
|
||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
[
|
||||
{
|
||||
name: 'groupby',
|
||||
override: {
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Parameters'),
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'significance_level',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Significance Level'),
|
||||
default: 0.05,
|
||||
description: t(
|
||||
'Threshold alpha level for determining significance',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'pvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('p-value precision'),
|
||||
default: 6,
|
||||
description: t(
|
||||
'Number of decimal places with which to display p-values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'liftvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Lift percent precision'),
|
||||
default: 4,
|
||||
description: t(
|
||||
'Number of decimal places with which to display lift values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,50 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Correlation'),
|
||||
description: t(
|
||||
'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Paired t-test Table'),
|
||||
tags: [t('Legacy'), t('Statistical'), t('Tabular')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class PairedTTestChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./PairedTTest'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 { validateNonEmpty } from '@superset-ui/core';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const PairedTTest = require('./PairedTTest').default;
|
||||
|
||||
type PairedTTestExtra = {
|
||||
alpha: number;
|
||||
groups: string[];
|
||||
liftValPrec: number;
|
||||
metrics: string[];
|
||||
pValPrec: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, PairedTTestExtra>({
|
||||
metadata: {
|
||||
name: t('Paired t-test Table'),
|
||||
description: t(
|
||||
'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.',
|
||||
),
|
||||
category: t('Correlation'),
|
||||
tags: [t('Legacy'), t('Statistical'), t('Tabular')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [['metrics']],
|
||||
query: [
|
||||
[
|
||||
{
|
||||
name: 'groupby',
|
||||
override: {
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
additionalSections: [
|
||||
{
|
||||
label: t('Parameters'),
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'significance_level',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Significance Level'),
|
||||
default: 0.05,
|
||||
description: t(
|
||||
'Threshold alpha level for determining significance',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'pvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('p-value precision'),
|
||||
default: 6,
|
||||
description: t(
|
||||
'Number of decimal places with which to display p-values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'liftvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Lift percent precision'),
|
||||
default: 4,
|
||||
description: t(
|
||||
'Number of decimal places with which to display lift values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const {
|
||||
groupby,
|
||||
liftvaluePrecision,
|
||||
metrics,
|
||||
pvaluePrecision,
|
||||
significanceLevel,
|
||||
} = formData as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
alpha: (significanceLevel as number) ?? 0.05,
|
||||
groups: (groupby as string[]) ?? [],
|
||||
liftValPrec: parseInt(String(liftvaluePrecision ?? '4'), 10),
|
||||
metrics: ((metrics as Array<string | { label: string }>) ?? []).map(
|
||||
metric => (typeof metric === 'string' ? metric : metric.label),
|
||||
),
|
||||
pValPrec: parseInt(String(pvaluePrecision ?? '6'), 10),
|
||||
};
|
||||
},
|
||||
render: ({ alpha, groups, liftValPrec, metrics, pValPrec, data }) => (
|
||||
<PairedTTest
|
||||
alpha={alpha}
|
||||
data={data}
|
||||
groups={groups}
|
||||
liftValPrec={liftValPrec}
|
||||
metrics={metrics}
|
||||
pValPrec={pValPrec}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { formData, queriesData } = chartProps;
|
||||
const {
|
||||
groupby,
|
||||
liftvaluePrecision,
|
||||
metrics,
|
||||
pvaluePrecision,
|
||||
significanceLevel,
|
||||
} = formData;
|
||||
|
||||
return {
|
||||
alpha: significanceLevel,
|
||||
data: queriesData[0].data,
|
||||
groups: groupby,
|
||||
liftValPrec: parseInt(liftvaluePrecision, 10),
|
||||
metrics: (metrics as (string | { label: string })[]).map(
|
||||
(metric: string | { label: string }) =>
|
||||
typeof metric === 'string' ? metric : metric.label,
|
||||
),
|
||||
pValPrec: parseInt(pvaluePrecision, 10),
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,67 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'distributions' {
|
||||
class Studentt {
|
||||
constructor(degreesOfFreedom: number);
|
||||
cdf(x: number): number;
|
||||
}
|
||||
const dist: {
|
||||
Studentt: typeof Studentt;
|
||||
};
|
||||
export default dist;
|
||||
}
|
||||
|
||||
declare module 'reactable' {
|
||||
import { ComponentType, ReactNode } from 'react';
|
||||
|
||||
interface TableProps {
|
||||
className?: string;
|
||||
id?: string;
|
||||
sortable?: (
|
||||
| string
|
||||
| {
|
||||
column: string;
|
||||
sortFunction: (a: string, b: string) => number;
|
||||
}
|
||||
)[];
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface TrProps {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface TdProps {
|
||||
className?: string;
|
||||
column?: string;
|
||||
data?: string | number | boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface ThProps {
|
||||
column?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface TheadProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Table: ComponentType<TableProps>;
|
||||
export const Tr: ComponentType<TrProps>;
|
||||
export const Td: ComponentType<TdProps>;
|
||||
export const Th: ComponentType<ThProps>;
|
||||
export const Thead: ComponentType<TheadProps>;
|
||||
}
|
||||
declare module 'distributions';
|
||||
declare module 'reactable';
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +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 { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series'],
|
||||
['metrics'],
|
||||
['secondary_metric'],
|
||||
['adhoc_filters'],
|
||||
['limit', 'row_limit'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'show_datatable',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Data Table'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the interactive data table'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'include_series',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Include Series'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t('Include series name as an axis'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,56 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/example1.jpg';
|
||||
import example1Dark from './images/example1-dark.jpg';
|
||||
import example2 from './images/example2.jpg';
|
||||
import example2Dark from './images/example2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Ranking'),
|
||||
credits: ['https://syntagmatic.github.io/parallel-coordinates'],
|
||||
description: t(
|
||||
'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.',
|
||||
),
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
name: t('Parallel Coordinates'),
|
||||
tags: [t('Directional'), t('Legacy'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ParallelCoordinatesChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactParallelCoordinates'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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 { defineChart } from '@superset-ui/glyph-core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/example1.jpg';
|
||||
import example1Dark from './images/example1-dark.jpg';
|
||||
import example2 from './images/example2.jpg';
|
||||
import example2Dark from './images/example2-dark.jpg';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactParallelCoordinates = require('./ReactParallelCoordinates').default;
|
||||
|
||||
type ParallelCoordinatesExtra = {
|
||||
includeSeries: boolean;
|
||||
linearColorScheme: string;
|
||||
metrics: string[];
|
||||
colorMetric: string | undefined;
|
||||
series: string;
|
||||
showDatatable: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, ParallelCoordinatesExtra>({
|
||||
metadata: {
|
||||
name: t('Parallel Coordinates'),
|
||||
description: t(
|
||||
'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.',
|
||||
),
|
||||
category: t('Ranking'),
|
||||
credits: ['https://syntagmatic.github.io/parallel-coordinates'],
|
||||
tags: [t('Directional'), t('Legacy'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [['series'], ['metrics'], ['secondary_metric']],
|
||||
query: [
|
||||
['limit', 'row_limit'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
],
|
||||
},
|
||||
middleSections: [
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'show_datatable',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Data Table'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the interactive data table'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'include_series',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Include Series'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t('Include series name as an axis'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const {
|
||||
includeSeries,
|
||||
linearColorScheme,
|
||||
metrics,
|
||||
secondaryMetric,
|
||||
series,
|
||||
showDatatable,
|
||||
} = formData as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
includeSeries: (includeSeries as boolean) ?? false,
|
||||
linearColorScheme: (linearColorScheme as string) ?? '',
|
||||
metrics: ((metrics as Array<string | { label: string }>) ?? []).map(
|
||||
m => (m as { label?: string }).label || (m as string),
|
||||
),
|
||||
colorMetric:
|
||||
secondaryMetric && (secondaryMetric as { label?: string }).label
|
||||
? (secondaryMetric as { label: string }).label
|
||||
: (secondaryMetric as string | undefined),
|
||||
series: (series as string) ?? '',
|
||||
showDatatable: (showDatatable as boolean) ?? false,
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
includeSeries,
|
||||
linearColorScheme,
|
||||
metrics,
|
||||
colorMetric,
|
||||
series,
|
||||
showDatatable,
|
||||
}) => (
|
||||
<ReactParallelCoordinates
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
includeSeries={includeSeries}
|
||||
linearColorScheme={linearColorScheme}
|
||||
metrics={metrics}
|
||||
colorMetric={colorMetric}
|
||||
series={series}
|
||||
showDatatable={showDatatable}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,48 +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 { ChartProps } from '@superset-ui/core';
|
||||
import { isThemeDark } from '@apache-superset/core/theme';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData, theme } = chartProps;
|
||||
const {
|
||||
includeSeries,
|
||||
linearColorScheme,
|
||||
metrics,
|
||||
secondaryMetric,
|
||||
series,
|
||||
showDatatable,
|
||||
} = formData;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
defaultLineColor: theme.colorTextTertiary,
|
||||
includeSeries,
|
||||
isDarkMode: isThemeDark(theme),
|
||||
linearColorScheme,
|
||||
metrics: metrics.map((m: { label?: string } | string) =>
|
||||
typeof m === 'string' ? m : m.label || m,
|
||||
),
|
||||
colorMetric: secondaryMetric?.label || secondaryMetric,
|
||||
series,
|
||||
showDatatable,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,18 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'd3v3' {
|
||||
const d3: Record<string, Function>;
|
||||
export = d3;
|
||||
}
|
||||
declare module 'd3v3';
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
declare module "*.png" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
declare module "*.jpg" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user