Compare commits

..

62 Commits

Author SHA1 Message Date
Vitor Avila
d40a5cad5d fix(OAuth2): Re-query the OAuth2 token to avoid stale reference (#40071) 2026-05-18 13:07:54 -03:00
Evan Rusackas
38546d7a3d chore(deps): coordinated bump ag-grid-community + ag-grid-react 35.2.1→35.3.0 (#40205)
Co-authored-by: Claude <claude@anthropic.com>
2026-05-18 22:18:37 +07:00
dependabot[bot]
6e5dfa0dd4 chore(deps): bump baseline-browser-mapping from 2.10.29 to 2.10.30 in /docs (#40211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 22:14:27 +07:00
SkinnyPigeon
70419e9d8f feat: Allow specific mcp tools to be disabled (#39835) 2026-05-18 07:22:02 -07:00
Evan Rusackas
34281f54a6 test(prophet): pin yhat_lower can be negative for negative series (#21734) (#40141)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-18 07:21:04 -07:00
Evan Rusackas
53d5c41a72 test(security): regression test for session cookie after logout (#24713) (#40201)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-18 07:20:51 -07:00
Evan Rusackas
453f49ce33 test(api): regression test for Admin empty dashboard/chart list (#25890) (#40202)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-18 07:20:37 -07:00
Mafi
b66c104fde fix(sqllab): execute prequeries on streaming connection to fix PostgreSQL CSV export (#40194)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:43:06 -04:00
dependabot[bot]
61b77fa35d chore(deps-dev): bump ip-address from 10.1.0 to 10.2.0 in /superset-frontend (#40199)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-18 06:29:05 -07:00
dependabot[bot]
0da0767780 chore(deps-dev): bump eslint from 10.3.0 to 10.4.0 in /superset-websocket (#40208)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:28:43 -07:00
dependabot[bot]
e2ff2d5d41 chore(deps): bump reselect from 5.1.1 to 5.2.0 in /docs (#40209)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:28:25 -07:00
dependabot[bot]
6a6be4c385 chore(deps): bump antd from 6.4.2 to 6.4.3 in /docs (#40210)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:28:00 -07:00
dependabot[bot]
cf831388d8 chore(deps): bump caniuse-lite from 1.0.30001792 to 1.0.30001793 in /docs (#40212)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:27:36 -07:00
dependabot[bot]
684a66aee6 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#40215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:27:09 -07:00
dependabot[bot]
80a200820c chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40217)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:26:50 -07:00
dependabot[bot]
f47300102c chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 (#40218)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 06:26:20 -07:00
Alejandro Solares
dd523c1a7b fix(deps): patch fast-xml-parser CVE-2026-33036 and CVE-2026-33349 (#40118) 2026-05-18 08:30:17 +01:00
dependabot[bot]
02a8196a6d chore(deps): update dompurify requirement from ^3.4.1 to ^3.4.2 in /superset-frontend/packages/superset-ui-core (#39808)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-17 20:16:45 -07:00
dependabot[bot]
4e13512ed8 chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/plugins/plugin-chart-handlebars (#40015)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:16:14 -07:00
dependabot[bot]
268dadbb5b chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/plugins/plugin-chart-pivot-table (#40018)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:15:58 -07:00
dependabot[bot]
427e7e53cd chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/packages/generator-superset (#40019)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:15:44 -07:00
dependabot[bot]
78f54b68ac chore(deps): update dompurify requirement from ^3.4.1 to ^3.4.3 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-17 20:15:07 -07:00
dependabot[bot]
6c4c3dc71c chore(deps): bump serialize-javascript and terser-webpack-plugin in /superset-frontend/cypress-base (#40174)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 20:13:36 -07:00
dependabot[bot]
26925af9ed chore(deps): bump minimatch from 3.1.3 to 3.1.5 in /superset-frontend/cypress-base (#40198)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 19:59:44 -07:00
dependabot[bot]
fdb62d8f35 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40154)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-17 19:59:29 -07:00
Evan Rusackas
3a9c54a672 fix(date_parser): suppress noisy parsedatetime DEBUG logs (#33365) (#40144)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-17 19:58:08 -07:00
Evan Rusackas
e6755d508d fix(rls): align view permission name with REST API canonical name (#33744) (#40145)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-17 19:57:57 -07:00
dependabot[bot]
b09ef7a406 chore(deps): bump minimatch from 3.1.2 to 3.1.5 in /superset-embedded-sdk (#40176)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:27:28 -07:00
dependabot[bot]
9eecc5a2a6 chore(deps): bump axios from 1.15.0 to 1.16.1 in /docs (#40177)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:26:44 -07:00
dependabot[bot]
d308649a65 chore(deps-dev): bump @types/node from 25.7.0 to 25.8.0 in /superset-frontend (#40157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:25:08 -07:00
dependabot[bot]
dd4e2e2e44 chore(deps-dev): update sqlalchemy-exasol requirement from <3.0,>=2.4.0 to >=2.4.0,<8.0 (#40182)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:24:53 -07:00
dependabot[bot]
6165a2531f chore(deps): bump fast-uri from 3.0.6 to 3.1.2 in /superset-frontend (#40175)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:24:24 -07:00
dependabot[bot]
9439d4db09 chore(deps-dev): update clickhouse-connect requirement from <1.0,>=0.13.0 to >=0.13.0,<2.0 (#40184)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:24:10 -07:00
dependabot[bot]
6b425ab559 chore(deps-dev): bump hdbcli from 2.4.162 to 2.28.20 (#40185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:23:48 -07:00
dependabot[bot]
4ded665495 chore(deps): bump flask-migrate from 3.1.0 to 4.1.0 (#40187)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:23:23 -07:00
dependabot[bot]
37638e750d chore(deps): bump greenlet from 3.1.1 to 3.5.0 (#40188)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 10:23:06 -07:00
Evan Rusackas
8a86ab7319 chore(docs): rename default docs plugin to user_docs for consistent versioned dir naming (#40171)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-15 22:26:43 -07:00
Elizabeth Thompson
8d2b655c22 fix(reports): narrow spinner checks to viewport and tighten exception handling (#39895)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 13:35:07 -07:00
Abdul Rehman
29b94ced71 fix(i18n): correct Czech translation variables for SQL Lab query message (#40166)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-15 14:06:25 -04:00
Beto Dealmeida
736a51c13f fix: OAuth2 exception should be 403 (#40074) 2026-05-15 14:53:02 -03:00
dependabot[bot]
34c28f7b76 chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:59 -07:00
dependabot[bot]
62c86abcd1 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40152)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:45 -07:00
dependabot[bot]
caa357e0d2 chore(deps): bump @ant-design/icons from 6.2.2 to 6.2.3 in /superset-frontend (#40112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-15 10:35:33 -07:00
dependabot[bot]
cc21683118 chore(deps): bump fast-xml-builder from 1.1.5 to 1.2.0 in /superset-frontend (#40103)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:21 -07:00
dependabot[bot]
114d88468b chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#39821)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-15 10:35:06 -07:00
dependabot[bot]
48c0bea906 chore(deps): bump d3-cloud from 1.2.8 to 1.2.9 in /superset-frontend (#39699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:34:51 -07:00
dependabot[bot]
a46925d431 chore(deps-dev): bump @types/node from 25.7.0 to 25.8.0 in /superset-websocket (#40148)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:49 -07:00
dependabot[bot]
0df9cc986a chore(deps): bump immer from 11.1.7 to 11.1.8 in /superset-frontend (#40158)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:23 -07:00
dependabot[bot]
ade901ed04 chore(deps): bump react-arborist from 3.5.0 to 3.6.1 in /superset-frontend (#40159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:07 -07:00
Richard Fogaca Nienkotter
1e2d0b5f5b fix(mcp): defer chart preview command imports (#40164) 2026-05-15 12:15:33 -03:00
dependabot[bot]
59b5f69627 chore(deps): bump antd from 6.3.7 to 6.4.2 in /docs (#40149)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:54:22 +07:00
dependabot[bot]
c2980c7c42 chore(deps-dev): bump webpack-dev-server from 5.2.3 to 5.2.4 in /superset-frontend (#40161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:53:22 +07:00
dependabot[bot]
982881ac1c chore(deps-dev): bump tsx from 4.21.0 to 4.22.0 in /superset-frontend (#40162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:52:50 +07:00
Shaitan
2e7a2b1f2d fix: escape SQL identifiers in db engine spec prequeries and metadata queries (#39840)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:48:38 -04:00
Michael S. Molina
a06e6ea19b fix(extensions): add cache headers and strip Vary: Cookie for extension static assets (#40120)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:23:39 -03:00
Shaitan
ee9eec25f9 fix(dataset): validate datasource access during import (#39998)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:06:47 +01:00
Shaitan
ffa32414ef fix(query): restrict query cancellation to the query owner (#39996)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:05:38 +01:00
Shaitan
407321e394 fix(database): extend shillelagh URI pattern to cover all driver variants (#39995)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:04:34 +01:00
JUST.in DO IT
2b71d964cc fix(sqllab): missing estimate action button (#40101) 2026-05-14 14:43:07 -07:00
dependabot[bot]
f02e5b7e83 chore(deps-dev): bump babel-jest from 30.3.0 to 30.4.1 in /superset-frontend (#40090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 13:52:53 -07:00
dependabot[bot]
5fa9657528 chore(deps): update @ant-design/icons requirement from ^6.2.2 to ^6.2.3 in /superset-frontend/packages/superset-ui-core (#40092)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sadpandajoe <jcli38@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:52:37 -07:00
dependabot[bot]
d853930840 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 13:52:14 -07:00
584 changed files with 54419 additions and 35804 deletions

View File

@@ -1 +0,0 @@
{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094}

View File

@@ -41,7 +41,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,6 +53,6 @@ jobs:
- name: Perform CodeQL Analysis
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -111,8 +111,6 @@ 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
@@ -164,7 +162,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: false
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"

View File

@@ -34,14 +34,6 @@ 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`

View File

@@ -52,11 +52,11 @@ yarn serve # Serve built site locally
# For maximum-detail databases.json, drop the `database-diagnostics`
# artifact from Python-Integration CI at src/data/databases.json before
# cutting. See README.md "Before You Cut".
yarn version:add:docs <version> # Add new docs version
yarn version:add:user_docs <version> # Add new docs version
yarn version:add:admin_docs <version> # Add admin docs version
yarn version:add:developer_docs <version> # Add developer docs version
yarn version:add:components <version> # Add components version
yarn version:remove:docs <version> # Remove docs version
yarn version:remove:user_docs <version> # Remove docs version
yarn version:remove:admin_docs <version> # Remove admin docs version
yarn version:remove:developer_docs <version> # Remove developer docs version
yarn version:remove:components <version> # Remove components version
@@ -228,17 +228,20 @@ Versions are managed through `versions-config.json`:
```bash
# ✅ CORRECT - Updates both Docusaurus and versions-config.json
yarn version:add:docs 6.1.0
yarn version:add:user_docs 6.1.0
# ❌ WRONG - Only updates Docusaurus, breaks version dropdown
yarn docusaurus docs:version 6.1.0
```
### Version Files Created
When versioning, these files are created:
- `versioned_docs/version-X.X.X/` - Snapshot of current docs
- `versioned_sidebars/version-X.X.X-sidebars.json` - Sidebar config
- `versions.json` - List of all versions
When versioning, these files are created (per section, with the
section's plugin id as prefix):
- `<section>_versioned_docs/version-X.X.X/` - Snapshot of current docs
- `<section>_versioned_sidebars/version-X.X.X-sidebars.json` - Sidebar config
- `<section>_versions.json` - List of all versions
Section plugin ids: `user_docs`, `admin_docs`, `developer_docs`, `components`.
## 🎨 Styling and Theming
@@ -386,7 +389,7 @@ Docusaurus includes Algolia DocSearch integration configured in `docusaurus.conf
## 🚫 Common Pitfalls to Avoid
1. **Never use `yarn docusaurus docs:version`** - Use `yarn version:add:docs` instead
1. **Never use `yarn docusaurus docs:version`** - Use `yarn version:add:user_docs` instead
2. **Don't edit versioned docs directly** - Edit current docs and create new version
3. **Avoid absolute paths in links** - Use relative paths for maintainability
4. **Don't forget frontmatter** - Every doc needs title and description
@@ -416,7 +419,7 @@ yarn eslint
### Version Issues
If versions don't appear in dropdown:
1. Check `versions-config.json` includes the version
2. Verify version files exist in `versioned_docs/`
2. Verify version files exist in `<section>_versioned_docs/`
3. Restart dev server
## 📚 Resources

View File

@@ -53,7 +53,7 @@ Also: confirm `master` CI is green, and that your local checkout matches the SHA
```bash
# Main Documentation
yarn version:add:docs 1.2.0
yarn version:add:user_docs 1.2.0
# Admin Docs
yarn version:add:admin_docs 1.2.0
@@ -98,7 +98,7 @@ If creating versions manually, you'll need to:
- **Versioned sidebars**: `[section]_versioned_sidebars/version-X.X.X-sidebars.json`
- **Versions list**: `[section]_versions.json`
Note: For main docs, the prefix is omitted (e.g., `versioned_docs/` instead of `docs_versioned_docs/`)
All four sections (`user_docs`, `admin_docs`, `developer_docs`, `components`) follow this naming pattern uniformly.
3. **Important**: After adding a version, restart the development server to see changes:
```bash
@@ -111,7 +111,7 @@ If creating versions manually, you'll need to:
#### Using Automated Scripts (Recommended)
```bash
# Main Documentation
yarn version:remove:docs 1.0.0
yarn version:remove:user_docs 1.0.0
# Admin Docs
yarn version:remove:admin_docs 1.0.0
@@ -127,19 +127,19 @@ yarn version:remove:components 1.0.0
To manually remove a version:
1. **Delete the version folder** from the appropriate location:
- Main docs: `versioned_docs/version-X.X.X/` (no prefix for main)
- User Docs: `user_docs_versioned_docs/version-X.X.X/`
- Admin Docs: `admin_docs_versioned_docs/version-X.X.X/`
- Developer Docs: `developer_docs_versioned_docs/version-X.X.X/`
- Components: `components_versioned_docs/version-X.X.X/`
2. **Delete the version metadata file**:
- Main docs: `versioned_sidebars/version-X.X.X-sidebars.json` (no prefix)
- User Docs: `user_docs_versioned_sidebars/version-X.X.X-sidebars.json`
- Admin Docs: `admin_docs_versioned_sidebars/version-X.X.X-sidebars.json`
- Developer Docs: `developer_docs_versioned_sidebars/version-X.X.X-sidebars.json`
- Components: `components_versioned_sidebars/version-X.X.X-sidebars.json`
3. **Update the versions list file**:
- Main docs: `versions.json`
- User Docs: `user_docs_versions.json`
- Admin Docs: `admin_docs_versions.json`
- Developer Docs: `developer_docs_versions.json`
- Components: `components_versions.json`
@@ -212,8 +212,8 @@ docs: {
If you accidentally used `yarn docusaurus docs:version` instead of `yarn version:add`:
1. **Problem**: The version files were created but `versions-config.json` wasn't updated
2. **Solution**: Either:
- Revert the changes: `git restore versions.json && rm -rf versioned_docs/ versioned_sidebars/`
- Then use the correct command: `yarn version:add:docs <version>`
- Revert the changes: `git restore user_docs_versions.json && rm -rf user_docs_versioned_docs/ user_docs_versioned_sidebars/`
- Then use the correct command: `yarn version:add:user_docs <version>`
For other issues:
- **Restart the server**: Changes to version configuration require a server restart

View File

@@ -502,6 +502,7 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
| `MCP_DEBUG` | `False` | Enable debug logging |
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
### Authentication
@@ -825,6 +826,32 @@ while True:
page += 1
```
## Disabling built-in tools
If you have deployed a custom tool via a Superset extension that supersedes one of the built-in Superset tools, you can suppress the built-in version so AI clients only discover your replacement. Disabled tools are removed from the server at startup and are never advertised during tool discovery.
Set `MCP_DISABLED_TOOLS` in your `superset_config.py` to a set of tool names:
```python
# superset_config.py
# Disable one tool
MCP_DISABLED_TOOLS = {"execute_sql"}
# Disable multiple tools
MCP_DISABLED_TOOLS = {"execute_sql", "health_check"}
```
Tool names match the function name used in the `@tool` decorator (e.g., `execute_sql`, `list_charts`, `health_check`). Extension-prefixed tools can also be disabled using their full prefixed name:
```python
MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
```
:::note
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
:::
## Security Best Practices
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS

View File

@@ -36,6 +36,42 @@ const versionsConfig = JSON.parse(fs.readFileSync(versionsConfigPath, 'utf8'));
// Build plugins array dynamically based on disabled flags
const dynamicPlugins = [];
// Add user_docs (formerly the preset-classic default docs instance) as an
// explicit plugin instance, so its versioned dirs follow the same
// `<id>_versioned_docs` / `<id>_versioned_sidebars` / `<id>_versions.json`
// naming as the other sections instead of the bare `versioned_*` prefix
// Docusaurus uses for the default plugin id.
if (!versionsConfig.user_docs.disabled) {
dynamicPlugins.push([
'@docusaurus/plugin-content-docs',
{
id: 'user_docs',
path: 'docs',
routeBasePath: 'user-docs',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: ({ versionDocsDirPath, docPath }: { versionDocsDirPath: string; docPath: string }) => {
if (docPath === 'intro.md') {
return 'https://github.com/apache/superset/edit/master/README.md';
}
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
},
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
admonitions: {
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
extendDefaults: true,
},
docItemComponent: '@theme/DocItem',
includeCurrentVersion: versionsConfig.user_docs.includeCurrentVersion,
lastVersion: versionsConfig.user_docs.lastVersion,
onlyIncludeVersions: versionsConfig.user_docs.onlyIncludeVersions,
versions: versionsConfig.user_docs.versions,
disableVersioning: false,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
]);
}
// Add components plugin if not disabled
if (!versionsConfig.components.disabled) {
dynamicPlugins.push([
@@ -703,29 +739,12 @@ const config: Config = {
[
'@docusaurus/preset-classic',
{
docs: {
routeBasePath: 'user-docs',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: ({ versionDocsDirPath, docPath }) => {
if (docPath === 'intro.md') {
return 'https://github.com/apache/superset/edit/master/README.md';
}
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
},
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
admonitions: {
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
extendDefaults: true,
},
includeCurrentVersion: versionsConfig.docs.includeCurrentVersion,
lastVersion: versionsConfig.docs.lastVersion, // Make 'next' the default
onlyIncludeVersions: versionsConfig.docs.onlyIncludeVersions,
versions: versionsConfig.docs.versions,
disableVersioning: false,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
docItemComponent: '@theme/DocItem',
},
// The user-docs section is configured as an explicit plugin
// instance above (id: 'user_docs') rather than the preset's
// default docs slot, so that all four sections use parallel
// `<id>_versioned_docs` / `<id>_versioned_sidebars` /
// `<id>_versions.json` naming on disk.
docs: false,
blog: {
showReadingTime: true,
// Please change this to your repo.

View File

@@ -33,11 +33,11 @@
"lint:docs-links": "node scripts/lint-docs-links.mjs",
"version:add": "node scripts/manage-versions.mjs add",
"version:remove": "node scripts/manage-versions.mjs remove",
"version:add:docs": "node scripts/manage-versions.mjs add docs",
"version:add:user_docs": "node scripts/manage-versions.mjs add user_docs",
"version:add:admin_docs": "node scripts/manage-versions.mjs add admin_docs",
"version:add:developer_docs": "node scripts/manage-versions.mjs add developer_docs",
"version:add:components": "node scripts/manage-versions.mjs add components",
"version:remove:docs": "node scripts/manage-versions.mjs remove docs",
"version:remove:user_docs": "node scripts/manage-versions.mjs remove user_docs",
"version:remove:admin_docs": "node scripts/manage-versions.mjs remove admin_docs",
"version:remove:developer_docs": "node scripts/manage-versions.mjs remove developer_docs",
"version:remove:components": "node scripts/manage-versions.mjs remove components"
@@ -71,9 +71,9 @@
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.29",
"caniuse-lite": "^1.0.30001792",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.30",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.1.1",
@@ -87,7 +87,7 @@
"react-svg-pan-zoom": "^3.13.1",
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"reselect": "^5.2.0",
"storybook": "^8.6.18",
"swagger-ui-react": "^5.32.5",
"swc-loader": "^0.2.7",

View File

@@ -34,7 +34,7 @@ const rawArgs = process.argv.slice(2);
const skipGenerate = rawArgs.includes('--skip-generate');
const args = rawArgs.filter((a) => a !== '--skip-generate');
const command = args[0]; // 'add' or 'remove'
const section = args[1]; // 'docs', 'admin_docs', 'developer_docs', or 'components'
const section = args[1]; // 'user_docs', 'admin_docs', 'developer_docs', or 'components'
const version = args[2]; // version string like '1.2.0'
function loadConfig() {
@@ -54,13 +54,13 @@ function freezeDataImports(section, version) {
// historical version's content silently changes whenever the data file
// is updated. Copy each escaping data import into a snapshot-local
// `_versioned_data/` dir and rewrite the import to point there.
const sectionRoot = section === 'docs'
? path.join(__dirname, '..', 'docs')
: path.join(__dirname, '..', section);
// The user_docs section's source content lives in `docs/docs/` (the
// historical folder name), while admin_docs / developer_docs /
// components match their plugin id 1:1.
const sectionDir = section === 'user_docs' ? 'docs' : section;
const sectionRoot = path.join(__dirname, '..', sectionDir);
const docsRoot = path.join(__dirname, '..');
const versionedDocsDir = section === 'docs'
? `versioned_docs/version-${version}`
: `${section}_versioned_docs/version-${version}`;
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
const versionedDocsPath = path.join(__dirname, '..', versionedDocsDir);
const frozenDataDir = path.join(versionedDocsPath, '_versioned_data');
@@ -148,9 +148,7 @@ function fixVersionedImports(section, version) {
// Versioned content lands one directory deeper than the source content,
// so any `../../src/` or `../../data/` imports in .md/.mdx files need
// an extra `../` to keep reaching docs/src and docs/data.
const versionedDocsDir = section === 'docs'
? `versioned_docs/version-${version}`
: `${section}_versioned_docs/version-${version}`;
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
const versionedDocsPath = path.join(__dirname, '..', versionedDocsDir);
if (!fs.existsSync(versionedDocsPath)) {
@@ -238,9 +236,7 @@ function addVersion(section, version) {
}
// Run Docusaurus version command
const docusaurusCommand = section === 'docs'
? `yarn docusaurus docs:version ${version}`
: `yarn docusaurus docs:version:${section} ${version}`;
const docusaurusCommand = `yarn docusaurus docs:version:${section} ${version}`;
try {
execSync(docusaurusCommand, { stdio: 'inherit' });
@@ -262,10 +258,9 @@ function addVersion(section, version) {
config[section].onlyIncludeVersions.splice(versionIndex, 0, version);
// Add version metadata
const versionPath = section === 'docs' ? version : version;
config[section].versions[version] = {
label: version,
path: versionPath,
path: version,
banner: 'none'
};
@@ -305,13 +300,8 @@ function removeVersion(section, version) {
console.log(`Removing version ${version} from ${section}...`);
// Determine file paths based on section
const versionedDocsDir = section === 'docs'
? `versioned_docs/version-${version}`
: `${section}_versioned_docs/version-${version}`;
const versionedSidebarsFile = section === 'docs'
? `versioned_sidebars/version-${version}-sidebars.json`
: `${section}_versioned_sidebars/version-${version}-sidebars.json`;
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
const versionedSidebarsFile = `${section}_versioned_sidebars/version-${version}-sidebars.json`;
// Remove versioned files
const docsPath = path.join(__dirname, '..', versionedDocsDir);
@@ -328,9 +318,7 @@ function removeVersion(section, version) {
}
// Update versions.json file
const versionsJsonFile = section === 'docs'
? 'versions.json'
: `${section}_versions.json`;
const versionsJsonFile = `${section}_versions.json`;
const versionsJsonPath = path.join(__dirname, '..', versionsJsonFile);
if (fs.existsSync(versionsJsonPath)) {
@@ -377,14 +365,14 @@ Usage:
node scripts/manage-versions.mjs remove <section> <version>
Where:
- section: 'docs', 'developer_docs', 'admin_docs', or 'components'
- section: 'user_docs', 'admin_docs', 'developer_docs', or 'components'
- version: version string (e.g., '1.2.0', '2.0.0')
- --skip-generate: skip refreshing auto-generated docs before snapshotting
(use when you've already placed a fresh databases.json
from CI and want to preserve it)
Examples:
node scripts/manage-versions.mjs add docs 2.0.0
node scripts/manage-versions.mjs add user_docs 2.0.0
node scripts/manage-versions.mjs add developer_docs 1.3.0
node scripts/manage-versions.mjs remove components 1.0.0
`);

View File

@@ -31,17 +31,15 @@ import { DownOutlined } from '@ant-design/icons';
import styles from './styles.module.css';
// Map each versioned plugin id to the URL prefix it actually serves
// content from. Three of the four routeBasePath values differ from
// their pluginId — the default preset-classic docs plugin lives at
// `/user-docs`, and admin_docs / developer_docs use hyphens in their
// URLs even though the plugin ids use underscores. Without this map
// the basePath derivation below would mis-split the pathname for
// those sections and the version dropdown would jump to the section
// root instead of preserving the current page.
// content from. The plugin ids use underscores while several
// routeBasePath values use hyphens (and `user_docs` → `/user-docs`),
// so without this map the basePath derivation below would mis-split
// the pathname for those sections and the version dropdown would
// jump to the section root instead of preserving the current page.
//
// Keep in sync with the `routeBasePath` values in docusaurus.config.ts.
const PLUGIN_ID_TO_BASE_PATH = {
default: '/user-docs',
user_docs: '/user-docs',
components: '/components',
admin_docs: '/admin-docs',
developer_docs: '/developer-docs',

View File

@@ -1,5 +1,5 @@
{
"docs": {
"user_docs": {
"disabled": false,
"lastVersion": "current",
"includeCurrentVersion": true,

View File

@@ -212,7 +212,7 @@
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons@^6.1.1", "@ant-design/icons@^6.2.3":
"@ant-design/icons@^6.2.3":
version "6.2.3"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
@@ -1158,10 +1158,10 @@
dependencies:
core-js-pure "^3.43.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4":
version "7.28.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4", "@babel/runtime@^7.29.2":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
"@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.28.6":
version "7.28.6"
@@ -2924,13 +2924,13 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
"@rc-component/cascader@~1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.15.0.tgz#554cba8e01e94a1288547cec96422b2cfc73ff40"
integrity sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
@@ -2968,10 +2968,10 @@
dependencies:
"@rc-component/util" "^1.3.0"
"@rc-component/dialog@~1.8.4":
version "1.8.4"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
"@rc-component/dialog@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.9.0.tgz#3134f8fa8644d9bc228c862668b90de048c7ea1a"
integrity sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==
dependencies:
"@rc-component/motion" "^1.1.3"
"@rc-component/portal" "^2.1.0"
@@ -3025,30 +3025,30 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/input@~1.1.0", "@rc-component/input@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz"
integrity sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==
"@rc-component/input@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.0.tgz#a8c113000bbc39089cf75337bec68120115b9e05"
integrity sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==
dependencies:
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/mentions@~1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz"
integrity sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==
"@rc-component/mentions@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.9.0.tgz#1e133d607835854430e264b681b7b32c4b49daa7"
integrity sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==
dependencies:
"@rc-component/input" "~1.1.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/textarea" "~1.1.0"
"@rc-component/input" "~1.3.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/menu@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz"
integrity sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==
"@rc-component/menu@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/menu/-/menu-1.3.0.tgz#fc70d81ca76ae6013b0d7955f20a2393adef04b3"
integrity sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/overflow" "^1.0.0"
@@ -3078,13 +3078,13 @@
dependencies:
"@rc-component/util" "^1.2.0"
"@rc-component/notification@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz"
integrity sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==
"@rc-component/notification@~2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@rc-component/notification/-/notification-2.0.7.tgz#f2450a482f87e4698285833c4a8efcac169acabb"
integrity sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/util" "^1.2.1"
"@rc-component/util" "^1.11.0"
clsx "^2.1.1"
"@rc-component/overflow@^1.0.0":
@@ -3105,10 +3105,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/picker@~1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.1.tgz#7ffcb1e4d4655fe2f3d712773e1d3ab9cd5c2a5c"
integrity sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==
"@rc-component/picker@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.10.0.tgz#6989f0ae67fca8db00e31f81a8217c8bc370cd34"
integrity sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/resize-observer" "^1.0.0"
@@ -3199,10 +3199,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.9.1":
version "1.9.1"
resolved "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz"
integrity sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==
"@rc-component/table@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.0.tgz#7a98d68176f23f50a762df464f4c9142e7db3942"
integrity sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3210,28 +3210,18 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/tabs@~1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz"
integrity sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==
"@rc-component/tabs@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.9.0.tgz#8f3e3755450e5a90d240d1ed3dc140d520b1fbef"
integrity sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==
dependencies:
"@rc-component/dropdown" "~1.0.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/motion" "^1.1.3"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/textarea@~1.1.0", "@rc-component/textarea@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz"
integrity sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==
dependencies:
"@rc-component/input" "~1.1.0"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tooltip@~1.4.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz"
@@ -3241,30 +3231,30 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tour@~2.3.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz"
integrity sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==
"@rc-component/tour@~2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tour/-/tour-2.4.0.tgz#caf89cf8f2f9fb68f1fb0e0c867610015d01f432"
integrity sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==
dependencies:
"@rc-component/portal" "^2.2.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
"@rc-component/tree-select@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.9.0.tgz#13ea516478b6cb558e04181abb0a01ae6fbdd31f"
integrity sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.4.tgz#cb4f7d818118b3447763e74d3a82fba6454c7317"
integrity sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==
"@rc-component/tree@~1.3.0", "@rc-component/tree@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.1.tgz#6983ca6bd9d5f6d04dd7258d00cb0fe71cdfe661"
integrity sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.8.1"
@@ -3290,10 +3280,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.10.1.tgz#213c84c77e8b2001095530d3b0dc47c49c34ffe3"
integrity sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.11.0.tgz#965c8b44a3f57fc96dc14e5072afbe32e422fd4d"
integrity sha512-jHG3/BYgUWiP5c7RZHiaUNToyw1L3nlPSKG2RPu+YoiD9b3ajiJwBWhsjO+ZELmCsKFAjNR5DelbKdlF0e2BDA==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -5352,6 +5342,13 @@ address@^1.0.1:
resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz"
integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz"
@@ -5501,36 +5498,36 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.7.tgz#620354ec04135356cbc5ce0a666871ddc73e4117"
integrity sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw==
antd@^6.4.3:
version "6.4.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.4.3.tgz#80a7aab9c13c35daa0e0e7eea80585ba57cb7203"
integrity sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
"@ant-design/cssinjs-utils" "^2.1.2"
"@ant-design/fast-color" "^3.0.1"
"@ant-design/icons" "^6.1.1"
"@ant-design/icons" "^6.2.3"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.28.4"
"@rc-component/cascader" "~1.14.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.15.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.1.1"
"@rc-component/dialog" "~1.8.4"
"@rc-component/dialog" "~1.9.0"
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.8.1"
"@rc-component/image" "~1.9.0"
"@rc-component/input" "~1.1.2"
"@rc-component/input" "~1.3.0"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.6.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/mentions" "~1.9.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/motion" "^1.3.2"
"@rc-component/mutate-observer" "^2.0.1"
"@rc-component/notification" "~1.2.0"
"@rc-component/notification" "~2.0.7"
"@rc-component/pagination" "~1.2.0"
"@rc-component/picker" "~1.9.1"
"@rc-component/picker" "~1.10.0"
"@rc-component/progress" "~1.0.2"
"@rc-component/qrcode" "~1.1.1"
"@rc-component/rate" "~1.0.1"
@@ -5540,16 +5537,15 @@ antd@^6.3.7:
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.9.1"
"@rc-component/tabs" "~1.7.0"
"@rc-component/textarea" "~1.1.2"
"@rc-component/table" "~1.10.0"
"@rc-component/tabs" "~1.9.0"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.3.0"
"@rc-component/tree" "~1.2.4"
"@rc-component/tree-select" "~1.8.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.3.1"
"@rc-component/tree-select" "~1.9.0"
"@rc-component/trigger" "^3.9.0"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.10.1"
"@rc-component/util" "^1.11.0"
clsx "^2.1.1"
dayjs "^1.11.11"
scroll-into-view-if-needed "^3.1.0"
@@ -5737,12 +5733,13 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f"
integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==
version "1.16.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12"
integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==
dependencies:
follow-redirects "^1.15.11"
follow-redirects "^1.16.0"
form-data "^4.0.5"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0"
babel-loader@^9.2.1:
@@ -5813,10 +5810,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.29, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.29"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz#47bdc13027af28d341f367a4f35a07ce872e27b4"
integrity sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==
baseline-browser-mapping@^2.10.30, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.30"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz#58915c74388b05f3b3504026194ea9fa98f6e6b6"
integrity sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==
batch@0.6.1:
version "0.6.1"
@@ -6054,10 +6051,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001792:
version "1.0.30001792"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001793:
version "1.0.30001793"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
ccount@^2.0.0:
version "2.0.1"
@@ -7101,12 +7098,7 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.11:
version "1.11.13"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dayjs@^1.11.19:
dayjs@^1.11.11, dayjs@^1.11.19:
version "1.11.20"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
@@ -8261,7 +8253,7 @@ flatted@^3.2.9:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
follow-redirects@^1.0.0, follow-redirects@^1.15.11:
follow-redirects@^1.0.0, follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
@@ -8955,6 +8947,14 @@ http2-wrapper@^2.1.10:
quick-lru "^5.1.1"
resolve-alpn "^1.2.0"
https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
@@ -13291,10 +13291,10 @@ reselect@^4.0.0:
resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
reselect@^5.1.0, reselect@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
reselect@^5.1.0, reselect@^5.1.1, reselect@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.2.0.tgz#f380ef7664332d26ea06c1cba04bdbbdcaa955f1"
integrity sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==
resize-observer-polyfill@1.5.1:
version "1.5.1"

View File

@@ -53,11 +53,11 @@ 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, <4.0",
"flask-migrate>=3.1.0, <5.0",
"flask-session>=0.4.0, <1.0",
"flask-wtf>=1.1.0, <2.0",
"geopy",
"greenlet>=3.0.3, <=3.1.1",
"greenlet>=3.0.3, <=3.5.0",
"gunicorn>=22.0.0; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
@@ -121,7 +121,7 @@ bigquery = [
"sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=0.13.0, <1.0"]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
d1 = [
@@ -143,7 +143,7 @@ duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
@@ -156,7 +156,7 @@ firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",

View File

@@ -6756,9 +6756,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -12811,9 +12811,9 @@
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"

View File

@@ -6523,9 +6523,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -7076,15 +7076,6 @@
}
]
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"peer": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
@@ -7554,15 +7545,6 @@
"semver": "bin/semver.js"
}
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"peer": true,
"dependencies": {
"randombytes": "^2.1.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -7924,15 +7906,14 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.16",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
"peer": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
@@ -7946,12 +7927,39 @@
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@minify-html/node": {
"optional": true
},
"@swc/core": {
"optional": true
},
"@swc/css": {
"optional": true
},
"@swc/html": {
"optional": true
},
"clean-css": {
"optional": true
},
"cssnano": {
"optional": true
},
"csso": {
"optional": true
},
"esbuild": {
"optional": true
},
"html-minifier-terser": {
"optional": true
},
"lightningcss": {
"optional": true
},
"postcss": {
"optional": true
},
"uglify-js": {
"optional": true
}
@@ -13499,9 +13507,9 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"minimatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -13902,15 +13910,6 @@
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"peer": true,
"requires": {
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
@@ -14273,15 +14272,6 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
},
"serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"peer": true,
"requires": {
"randombytes": "^2.1.0"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -14566,15 +14556,14 @@
}
},
"terser-webpack-plugin": {
"version": "5.3.16",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
"peer": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
}
},

View File

@@ -27,11 +27,6 @@ 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',

File diff suppressed because it is too large Load Diff

View File

@@ -109,9 +109,7 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@googleapis/sheets": "^13.0.1",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -163,8 +161,8 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "35.2.1",
"ag-grid-react": "35.2.1",
"ag-grid-community": "35.3.0",
"ag-grid-react": "35.3.0",
"antd": "^5.26.0",
"chrono-node": "^2.9.1",
"classnames": "^2.2.5",
@@ -185,7 +183,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"immer": "^11.1.7",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -201,11 +199,10 @@
"nanoid": "^5.1.11",
"ol": "^10.9.0",
"pretty-ms": "^9.3.0",
"prop-types": "^15.8.1",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-arborist": "^3.6.1",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -292,7 +289,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/node": "^25.8.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
@@ -308,7 +305,7 @@
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.0.2",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -370,7 +367,7 @@
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.21.0",
"tsx": "^4.22.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -378,7 +375,7 @@
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
@@ -414,7 +411,8 @@
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5"
"@luma.gl/webgl": "~9.2.5",
"fast-xml-parser": "^5.8.0"
},
"readme": "ERROR: No README data found!",
"scarfSettings": {

View File

@@ -30,13 +30,13 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.1.2",
"yosay": "^3.0.0"
},
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.3.5",
"jest": "^30.3.0",
"jest": "^30.4.2",
"yeoman-test": "^11.5.2"
},
"engines": {

View File

@@ -441,8 +441,6 @@ 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 = {

View File

@@ -24,14 +24,14 @@
"lib"
],
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@ant-design/icons": "^6.2.3",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.2",
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^3.12.0",
"ace-builds": "^1.44.0",
"ag-grid-community": "35.2.1",
"ag-grid-react": "35.2.1",
"ag-grid-community": "35.3.0",
"ag-grid-react": "35.3.0",
"brace": "^0.11.1",
"classnames": "^2.5.1",
"core-js": "^3.49.0",
@@ -42,7 +42,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.20",
"dompurify": "^3.4.1",
"dompurify": "^3.4.2",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -76,7 +76,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.0",
"@types/lodash": "^4.17.24",
"@types/node": "^25.7.0",
"@types/node": "^25.8.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",

View File

@@ -33,14 +33,6 @@ 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 {

View File

@@ -1,335 +0,0 @@
# 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

View File

@@ -1,38 +0,0 @@
{
"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"
}
}

View File

@@ -1,646 +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 {
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
static readonly FORMAT_OPTIONS: SelectOption[] = [
{ label: 'Adaptive formatting', value: 'SMART_NUMBER' },
{ label: 'Original value', value: '~g' },
{ label: '12,345.432', value: ',.3f' },
{ label: '12,345.43', value: ',.2f' },
{ label: '12,345.4', value: ',.1f' },
{ label: '12,345', value: ',.0f' },
{ label: '12345.432', value: '.3f' },
{ label: '12345.43', value: '.2f' },
{ label: '12345.4', value: '.1f' },
{ label: '12345', value: '.0f' },
{ label: '12K', value: '.0s' },
{ label: '12.3K', value: '.1s' },
{ label: '12.35K', value: '.2s' },
{ label: '12.346K', value: '.3s' },
{ label: '1234543.21%', value: '.2%' },
{ label: '1234543%', value: '.0%' },
{ label: '12.34%', value: '.2r' },
{ label: '+12,345.4', value: '+,.1f' },
{ label: '$12,345.43', value: '$,.2f' },
{ label: 'Duration (1m 6s)', value: 'DURATION' },
{ label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' },
];
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
static readonly FORMAT_OPTIONS: SelectOption[] = [
{ label: 'Adaptive formatting', value: 'smart_date' },
{ label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' },
{ label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' },
{ label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' },
{ label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' },
{
label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10',
value: '%Y-%m-%d %H:%M:%S',
},
{
label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10',
value: '%d-%m-%Y %H:%M:%S',
},
{ label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' },
];
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';
}

View File

@@ -1,245 +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.
*/
/**
* 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),
};
}, {});
}

View File

@@ -1,419 +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 type {
ControlPanelConfig,
ControlSetRow,
} from '@superset-ui/chart-controls';
import type { ChartProps } from '@superset-ui/core';
import {
Argument,
Select,
Text,
Checkbox,
Int,
Color,
isSelectArg,
isCheckboxArg,
isIntArg,
isColorArg,
isMetricArg,
isDimensionArg,
isTemporalArg,
} from './arguments';
import type { VisibilityFn, RgbaColor } from './types';
/**
* Configuration for a glyph argument with optional visibility control
*/
export interface GlyphArgConfig {
arg: typeof Argument;
visibility?: VisibilityFn;
resetOnHide?: boolean;
}
/**
* Arguments map - parameter name to argument class or config
*/
export type GlyphArguments = Map<string, typeof Argument | GlyphArgConfig>;
/**
* Convert hex color string to RGBA object for Superset's ColorPickerControl
*/
function hexToRgba(hex: string): RgbaColor {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result && result[1] && result[2] && result[3]) {
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: 1,
};
}
return { r: 0, g: 0, b: 0, a: 1 };
}
/**
* Convert RGBA object to hex color string
*/
function rgbaToHex(rgba: RgbaColor): string {
const toHex = (n: number) => n.toString(16).padStart(2, '0');
return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`;
}
/**
* Get the argument class from a config (handles both direct class and config object)
*/
function getArgClass(
argOrConfig: typeof Argument | GlyphArgConfig,
): typeof Argument {
return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig;
}
/**
* Get visibility config if present
*/
function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): {
visibility?: VisibilityFn;
resetOnHide?: boolean;
} {
if ('arg' in argOrConfig) {
return {
visibility: argOrConfig.visibility,
resetOnHide: argOrConfig.resetOnHide,
};
}
return {};
}
/**
* Generate Superset control config from a glyph Argument class
*/
export function getControlConfig(
argClass: typeof Argument,
paramName: string,
): Record<string, unknown> & { type: string } {
const label = argClass.label || paramName;
const description = argClass.description || '';
// Select control
if (isSelectArg(argClass)) {
return {
type: 'SelectControl',
label,
description,
default: argClass.default,
options: argClass.options,
clearable: argClass.clearable ?? false,
renderTrigger: true,
};
}
// Checkbox control
if (isCheckboxArg(argClass)) {
return {
type: 'CheckboxControl',
label,
description,
default: argClass.default,
renderTrigger: true,
};
}
// Int/Slider control
if (isIntArg(argClass)) {
return {
type: 'SliderControl',
label,
description,
default: argClass.default,
min: argClass.min,
max: argClass.max,
step: argClass.step ?? 1,
renderTrigger: true,
};
}
// Color control
if (isColorArg(argClass)) {
// eslint-disable-next-line theme-colors/no-literal-colors
const hexDefault = argClass.default ?? '#000000';
return {
type: 'ColorPickerControl',
label,
description,
default: hexToRgba(hexDefault),
renderTrigger: true,
};
}
// Default to TextControl
const textClass = argClass as typeof Text;
return {
type: 'TextControl',
label,
description,
default: textClass.default ?? '',
placeholder: textClass.placeholder ?? '',
renderTrigger: true,
};
}
/**
* Options for control panel generation
*/
export interface ControlPanelOptions {
/** Additional control rows for the query section */
queryControls?: ControlSetRow[];
/** Additional control rows for the chart options section */
chartOptionsControls?: ControlSetRow[];
/** Control overrides */
controlOverrides?: Record<string, Record<string, unknown>>;
/** Form data overrides function */
formDataOverrides?: (
formData: Record<string, unknown>,
) => Record<string, unknown>;
}
/**
* Generate a complete ControlPanelConfig from glyph arguments
*
* This is the core function that converts semantic argument definitions
* into Superset's control panel format.
*/
export function generateControlPanel(
glyphArguments: GlyphArguments,
options: ControlPanelOptions = {},
): ControlPanelConfig {
const queryControls: ControlSetRow[] = [];
const chartOptionsControls: ControlSetRow[] = [];
// Process each argument
for (const [paramName, argOrConfig] of glyphArguments) {
const argClass = getArgClass(argOrConfig);
const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig);
// Data arguments go in Query section
if (isMetricArg(argClass)) {
queryControls.push(['metric']);
continue;
}
if (isDimensionArg(argClass)) {
queryControls.push(['groupby']);
continue;
}
if (isTemporalArg(argClass)) {
queryControls.push(['x_axis'], ['time_grain_sqla']);
continue;
}
// Style/visual arguments go in Chart Options section
const controlConfig = getControlConfig(argClass, paramName);
// Add visibility if specified
if (visibility) {
controlConfig.visibility = visibility;
controlConfig.resetOnHide = resetOnHide ?? false;
}
chartOptionsControls.push([
{
name: paramName,
config: controlConfig,
},
]);
}
// Add adhoc_filters to query section
queryControls.push(['adhoc_filters']);
// Merge with additional controls from options
const finalQueryControls = [
...queryControls,
...(options.queryControls || []),
];
const finalChartOptionsControls = [
...chartOptionsControls,
...(options.chartOptionsControls || []),
];
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: finalQueryControls,
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: finalChartOptionsControls,
},
],
};
if (options.controlOverrides) {
config.controlOverrides = options.controlOverrides;
}
if (options.formDataOverrides) {
// Type assertion needed because SqlaFormData is more specific than Record<string, unknown>
config.formDataOverrides =
options.formDataOverrides as ControlPanelConfig['formDataOverrides'];
}
return config;
}
/**
* Options for transformProps generation
*/
export interface TransformPropsOptions<TResult> {
/** Custom transformation function that receives extracted values */
transform?: (
values: Record<string, unknown>,
chartProps: ChartProps,
) => TResult;
/** Additional props to pass through from chartProps */
passthrough?: (keyof ChartProps)[];
}
/**
* Generate a transformProps function from glyph arguments
*
* This extracts values from formData based on argument definitions,
* applying type conversions as needed (e.g., RGBA to hex for colors).
*/
export function generateTransformProps<TResult = Record<string, unknown>>(
glyphArguments: GlyphArguments,
options: TransformPropsOptions<TResult> = {},
): (chartProps: ChartProps) => TResult {
return (chartProps: ChartProps) => {
const { formData, width, height, queriesData } = chartProps;
const values: Record<string, unknown> = {
width,
height,
queriesData,
};
// Add passthrough props
if (options.passthrough) {
for (const key of options.passthrough) {
values[key] = chartProps[key];
}
}
// Extract values from formData based on argument definitions
for (const [paramName, argOrConfig] of glyphArguments) {
const argClass = getArgClass(argOrConfig);
// Skip data arguments (metric, dimension, temporal) - these are handled differently
if (
isMetricArg(argClass) ||
isDimensionArg(argClass) ||
isTemporalArg(argClass)
) {
continue;
}
// Get value from formData, using default if not present
let value = formData[paramName];
// Color control: convert RGBA object to hex string
if (isColorArg(argClass)) {
const colorClass = argClass as typeof Color;
// eslint-disable-next-line theme-colors/no-literal-colors
const defaultRgba = hexToRgba(colorClass.default ?? '#000000');
const colorValue = value ?? defaultRgba;
if (
typeof colorValue === 'object' &&
colorValue !== null &&
'r' in colorValue
) {
value = rgbaToHex(colorValue as RgbaColor);
} else if (typeof colorValue === 'string') {
value = colorValue;
} else {
// eslint-disable-next-line theme-colors/no-literal-colors
value = colorClass.default ?? '#000000';
}
}
// Select control: use default if no value
else if (isSelectArg(argClass)) {
const selectClass = argClass as typeof Select;
value = value ?? selectClass.default;
}
// Checkbox control: use default if no value
else if (isCheckboxArg(argClass)) {
const checkboxClass = argClass as typeof Checkbox;
value = value ?? checkboxClass.default ?? false;
}
// Int control: use default if no value
else if (isIntArg(argClass)) {
const intClass = argClass as typeof Int;
value = value ?? intClass.default ?? 0;
}
// Text control: use default if no value
else {
const textClass = argClass as typeof Text;
value = value ?? textClass.default ?? '';
}
values[paramName] = value;
}
// Apply custom transformation if provided
if (options.transform) {
return options.transform(values, chartProps);
}
return values as TResult;
};
}
/**
* Combined result of creating a glyph plugin
*/
export interface GlyphPluginDef<TProps> {
controlPanel: ControlPanelConfig;
transformProps: (chartProps: ChartProps) => TProps;
}
/**
* Create both controlPanel and transformProps from a single argument definition
*
* This is the main entry point for the single-file viz pattern.
*/
export function createGlyphPlugin<TProps = Record<string, unknown>>(
glyphArguments: GlyphArguments,
controlPanelOptions: ControlPanelOptions = {},
transformPropsOptions: TransformPropsOptions<TProps> = {},
): GlyphPluginDef<TProps> {
return {
controlPanel: generateControlPanel(glyphArguments, controlPanelOptions),
transformProps: generateTransformProps(
glyphArguments,
transformPropsOptions,
),
};
}

View File

@@ -1,71 +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.
*/
/**
* 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 './generators';
export * from './defineChart';
export * from './presets';
export * from './crossFilter';

View File

@@ -1,406 +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.
*/
/**
* 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({
label: t('Show Legend'),
description: t('Whether to display the chart legend'),
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('Legend Type'),
description: t('Type of legend display'),
options: LEGEND_TYPE_OPTIONS,
default: 'scroll',
});
/**
* Legend orientation selector
* Position the legend relative to the chart
*/
export const LegendOrientation = Select.with({
label: t('Legend Orientation'),
description: t('Position of the legend'),
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,
});

View File

@@ -1,306 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { ControlPanelConfig } from '@superset-ui/chart-controls';
import type { ChartProps } from '@superset-ui/core';
/**
* 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',
}
/**
* Base argument class interface
*/
export interface ArgumentClass {
label: string | null;
description: string | null;
columnType?: ColumnType;
controlType?: string;
}
/**
* 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)
>;
/**
* Extended control configuration with visibility
*/
export interface ControlConfig {
name: string;
config: Record<string, unknown>;
visibility?: VisibilityFn;
resetOnHide?: boolean;
}
/**
* Glyph chart definition
*/
export interface GlyphChartDef<TArgs extends Record<string, ArgumentClass>> {
arguments: TArgs;
sections?: {
query?: ControlConfig[][];
chartOptions?: ControlConfig[][];
};
}
/**
* Result of createGlyphPlugin
*/
export interface GlyphPluginResult<TFormData> {
controlPanel: ControlPanelConfig;
transformProps: (chartProps: ChartProps) => TFormData;
}
/**
* Type helper to extract form data types from argument definitions
*/
export type ArgumentsToFormData<TArgs extends Record<string, ArgumentClass>> = {
[K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown;
};

View File

@@ -1,475 +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 {
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);
});
});

View File

@@ -1,212 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { 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({});
});
});

View File

@@ -1,563 +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 {
Behavior,
ChartLabel,
ChartMetadata,
ChartPlugin,
} from '@superset-ui/core';
import {
Checkbox,
defineChart,
Dimension,
evaluateGlyphCondition,
getArgVisibleWhen,
Metric,
resolveArgClass,
Select,
Text,
} from '@superset-ui/glyph-core';
// 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 Behavior.InteractiveChart when omitted', () => {
const Plugin = defineChart({
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
arguments: {},
transform: () => ({}),
render: () => null as unknown as React.ReactElement,
});
const { metadata } = instantiate(Plugin);
expect(metadata.behaviors).toContain(Behavior.InteractiveChart);
});
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',
});
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 });
});
});

View File

@@ -1,401 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { ChartProps } from '@superset-ui/core';
import {
Checkbox,
Color,
createGlyphPlugin,
Dimension,
generateControlPanel,
generateTransformProps,
getControlConfig,
Int,
Metric,
Select,
Temporal,
Text,
} from '@superset-ui/glyph-core';
import type { GlyphArguments } from '@superset-ui/glyph-core/generators';
describe('getControlConfig - per argument type', () => {
test('Select → SelectControl with options and clearable=false', () => {
const S = Select.with({
label: 'Choose',
default: 'a',
options: [{ label: 'A', value: 'a' }],
});
const cfg = getControlConfig(S, 'myParam');
expect(cfg.type).toBe('SelectControl');
expect(cfg.label).toBe('Choose');
expect(cfg.default).toBe('a');
expect(cfg.options).toEqual([{ label: 'A', value: 'a' }]);
expect(cfg.clearable).toBe(false);
expect(cfg.renderTrigger).toBe(true);
});
test('Checkbox → CheckboxControl with default', () => {
const C = Checkbox.with({ label: 'Show', default: true });
const cfg = getControlConfig(C, 'show');
expect(cfg.type).toBe('CheckboxControl');
expect(cfg.label).toBe('Show');
expect(cfg.default).toBe(true);
expect(cfg.renderTrigger).toBe(true);
});
test('Int → SliderControl with min/max/step', () => {
const I = Int.with({ label: 'Limit', default: 50, min: 0, max: 1000, step: 5 });
const cfg = getControlConfig(I, 'limit');
expect(cfg.type).toBe('SliderControl');
expect(cfg.label).toBe('Limit');
expect(cfg.default).toBe(50);
expect(cfg.min).toBe(0);
expect(cfg.max).toBe(1000);
expect(cfg.step).toBe(5);
});
test('Color (hex) → ColorPickerControl with RGBA default', () => {
const C = Color.with({ label: 'Fill', default: '#ff0000' });
const cfg = getControlConfig(C, 'fill');
expect(cfg.type).toBe('ColorPickerControl');
expect(cfg.label).toBe('Fill');
expect(cfg.default).toEqual({ r: 255, g: 0, b: 0, a: 1 });
});
test('Text → TextControl with placeholder', () => {
const T = Text.with({
label: 'Title',
default: 'Untitled',
placeholder: 'Enter…',
});
const cfg = getControlConfig(T, 'title');
expect(cfg.type).toBe('TextControl');
expect(cfg.label).toBe('Title');
expect(cfg.default).toBe('Untitled');
expect(cfg.placeholder).toBe('Enter…');
});
test('falls back to paramName when label is unset', () => {
// Use the raw Text class (label: null on Argument)
class Bare extends Text {
static override label = null;
}
const cfg = getControlConfig(Bare, 'fallback_name');
expect(cfg.label).toBe('fallback_name');
});
});
describe('generateControlPanel', () => {
test('produces Query and Chart Options sections', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
]);
const cp = generateControlPanel(args);
const labels = cp.controlPanelSections.map(s =>
s && 'label' in s ? s.label : undefined,
);
expect(labels).toContain('Query');
expect(labels).toContain('Chart Options');
});
test('Metric args produce a [metric] row in Query', () => {
const args: GlyphArguments = new Map([['m', Metric]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
expect(querySection).toBeDefined();
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['metric']);
});
test('Dimension args produce a [groupby] row in Query', () => {
const args: GlyphArguments = new Map([['d', Dimension]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['groupby']);
});
test('Temporal args produce [x_axis] and [time_grain_sqla] rows in Query', () => {
const args: GlyphArguments = new Map([['t', Temporal]]);
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['x_axis']);
expect(rows).toContainEqual(['time_grain_sqla']);
});
test('adhoc_filters is always added to Query', () => {
const args: GlyphArguments = new Map();
const cp = generateControlPanel(args);
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toContainEqual(['adhoc_filters']);
});
test('Non-data args become controls in Chart Options', () => {
const args: GlyphArguments = new Map([
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
]);
const cp = generateControlPanel(args);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const rows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
expect(rows).toHaveLength(1);
expect(rows[0]).toEqual([
{
name: 'showLegend',
config: expect.objectContaining({ type: 'CheckboxControl' }),
},
]);
});
test('GlyphArgConfig with visibility wires onto control', () => {
const visibility = jest.fn(() => true);
const args: GlyphArguments = new Map([
[
'subtitleSize',
{
arg: Select.with({
label: 'Size',
default: 'm',
options: [{ label: 'M', value: 'm' }],
}),
visibility,
resetOnHide: true,
},
],
]);
const cp = generateControlPanel(args);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const row = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows[0];
const item = (row as Array<{ config: Record<string, unknown> }>)[0];
expect(item.config.visibility).toBe(visibility);
expect(item.config.resetOnHide).toBe(true);
});
test('controlOverrides and formDataOverrides options pass through', () => {
const fdo = (fd: Record<string, unknown>) => ({ ...fd, x: 1 });
const cp = generateControlPanel(new Map(), {
controlOverrides: { metric: { label: 'M' } },
formDataOverrides: fdo,
});
expect(cp.controlOverrides).toEqual({ metric: { label: 'M' } });
expect(cp.formDataOverrides).toBe(fdo);
});
test('extra queryControls and chartOptionsControls are appended', () => {
const args: GlyphArguments = new Map();
const cp = generateControlPanel(args, {
queryControls: [['custom_filter']] as never,
chartOptionsControls: [['custom_chart_opt']] as never,
});
const querySection = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Query',
);
const queryRows = (querySection as { controlSetRows: unknown[][] })
.controlSetRows;
expect(queryRows).toContainEqual(['custom_filter']);
const chartOpts = cp.controlPanelSections.find(
s => s && 'label' in s && s.label === 'Chart Options',
);
const optRows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
expect(optRows).toContainEqual(['custom_chart_opt']);
});
});
describe('generateTransformProps', () => {
function makeChartProps(formData: Record<string, unknown>): ChartProps {
return {
width: 400,
height: 300,
queriesData: [{ data: [] }],
formData,
} as unknown as ChartProps;
}
test('returns width/height/queriesData passthrough', () => {
const transform = generateTransformProps(new Map());
const out = transform(makeChartProps({}));
expect(out).toMatchObject({ width: 400, height: 300 });
});
test('extracts Select value from formData', () => {
const args: GlyphArguments = new Map([
[
'size',
Select.with({
label: 'Size',
default: 'm',
options: [
{ label: 'S', value: 's' },
{ label: 'M', value: 'm' },
],
}),
],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ size: 's' }));
expect((out as { size: unknown }).size).toBe('s');
});
test('Select falls back to default when value missing', () => {
const args: GlyphArguments = new Map([
['size', Select.with({ label: 'Size', default: 'm', options: [] })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { size: unknown }).size).toBe('m');
});
test('Checkbox uses formData value when present', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: false })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ flag: true }));
expect((out as { flag: unknown }).flag).toBe(true);
});
test('Checkbox falls back to default when value missing', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: true })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { flag: unknown }).flag).toBe(true);
});
test('Color: RGBA in formData → hex string', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#000000' })],
]);
const transform = generateTransformProps(args);
const out = transform(
makeChartProps({ fill: { r: 255, g: 0, b: 0, a: 1 } }),
);
expect((out as { fill: unknown }).fill).toBe('#ff0000');
});
test('Color: string value in formData passes through', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#000000' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({ fill: '#abcdef' }));
expect((out as { fill: unknown }).fill).toBe('#abcdef');
});
test('Color: falls back to class default when value missing', () => {
const args: GlyphArguments = new Map([
['fill', Color.with({ label: 'Fill', default: '#112233' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
// default is hex string; transform converts the RGBA-formatted default back to hex
expect((out as { fill: unknown }).fill).toBe('#112233');
});
test('Int uses default when value missing', () => {
const args: GlyphArguments = new Map([
['n', Int.with({ label: 'N', default: 7, min: 0, max: 100 })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { n: unknown }).n).toBe(7);
});
test('Text uses default when value missing', () => {
const args: GlyphArguments = new Map([
['s', Text.with({ label: 'S', default: 'hi' })],
]);
const transform = generateTransformProps(args);
const out = transform(makeChartProps({}));
expect((out as { s: unknown }).s).toBe('hi');
});
test('Metric/Dimension/Temporal args are NOT extracted (handled elsewhere)', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['groupby', Dimension],
['t', Temporal],
]);
const transform = generateTransformProps(args);
const out = transform(
makeChartProps({ metric: 'count', groupby: 'a', t: 'date' }),
);
expect((out as Record<string, unknown>).metric).toBeUndefined();
expect((out as Record<string, unknown>).groupby).toBeUndefined();
expect((out as Record<string, unknown>).t).toBeUndefined();
});
test('passthrough option copies named ChartProps fields onto the result', () => {
const args: GlyphArguments = new Map();
const transform = generateTransformProps(args, {
passthrough: ['formData'],
});
const out = transform(makeChartProps({ marker: 1 })) as Record<
string,
unknown
>;
expect(out.formData).toEqual({ marker: 1 });
});
test('custom transform option receives extracted values and chartProps', () => {
const args: GlyphArguments = new Map([
['flag', Checkbox.with({ label: 'F', default: false })],
]);
const transform = generateTransformProps(args, {
transform: (values, chartProps) => ({
values,
gotChartProps: !!chartProps,
}),
});
const out = transform(makeChartProps({ flag: true })) as {
values: { flag: boolean };
gotChartProps: boolean;
};
expect(out.values.flag).toBe(true);
expect(out.gotChartProps).toBe(true);
});
});
describe('createGlyphPlugin', () => {
test('returns both controlPanel and transformProps', () => {
const args: GlyphArguments = new Map([
['metric', Metric],
['show', Checkbox.with({ label: 'S', default: true })],
]);
const plugin = createGlyphPlugin(args);
expect(plugin.controlPanel).toBeDefined();
expect(plugin.controlPanel.controlPanelSections.length).toBe(2);
expect(typeof plugin.transformProps).toBe('function');
});
});

View File

@@ -1,203 +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 {
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,
);
});
});

View File

@@ -1,21 +0,0 @@
{
"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" }
]
}

View File

@@ -32,7 +32,6 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
},

View File

@@ -0,0 +1,205 @@
/**
* 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;

View File

@@ -0,0 +1,58 @@
/**
* 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,
});
}
}

View File

@@ -1,241 +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 { 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}
/>
),
});

View File

@@ -0,0 +1,62 @@
/**
* 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,
};
}

View File

@@ -20,7 +20,6 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: any;
const value: string;
export default value;
}
declare module '*.jpg' {
const value: any;
const value: string;
export default value;
}

View File

@@ -36,7 +36,6 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@superset-ui/glyph-core": "*"
"@apache-superset/core": "*"
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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;

View File

@@ -0,0 +1,57 @@
/**
* 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,
});
}
}

View File

@@ -1,120 +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 { 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}
/>
),
});

View File

@@ -0,0 +1,33 @@
/**
* 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,
};
}

View File

@@ -20,7 +20,6 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: any;
const value: string;
export default value;
}
declare module '*.jpg' {
const value: any;
const value: string;
export default value;
}

View File

@@ -34,7 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"react": "^18.2.0"
}
}

View File

@@ -0,0 +1,99 @@
/**
* 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;

View File

@@ -0,0 +1,65 @@
/**
* 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';

View File

@@ -1,170 +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 {
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}
/>
),
});

View File

@@ -0,0 +1,63 @@
/**
* 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,
};
}

View File

@@ -20,7 +20,6 @@
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" },
{ "path": "../../packages/superset-ui-glyph-core" }
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
declare module '*.png' {
const value: any;
const value: string;
export default value;
}
declare module '*.jpg' {
const value: any;
const value: string;
export default value;
}

View File

@@ -30,7 +30,6 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@superset-ui/glyph-core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
},

View File

@@ -0,0 +1,105 @@
/**
* 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;

View File

@@ -0,0 +1,51 @@
/**
* 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,
});
}
}

View File

@@ -1,142 +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 { 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}
/>
),
});

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