Compare commits

..

239 Commits

Author SHA1 Message Date
Claude Code
d544bff071 fix(chart): keep query-context updates bound to the chart's datasource
On the query-context-only update path UpdateChartCommand intentionally
skips the ownership check so report and alert workers can refresh a
chart's cached payload. Validate that the submitted query context still
targets the chart's own datasource (id and type) before saving, so a
cached payload cannot be repointed at an unrelated datasource. Payloads
without a parseable datasource fall back to the chart's datasource at
execution time and are left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:29:39 -07:00
Dylan Cavalcante
f79a88c685 test(core): add unit tests for split function (#40819)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:12:35 -07:00
dependabot[bot]
b1d965932d chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.60.0 to 8.60.1 in /superset-websocket (#40888)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:46:38 -07:00
dependabot[bot]
7d046340dc chore(deps): bump ag-grid-react from 35.3.0 to 35.3.1 in /superset-frontend/packages/superset-ui-core (#40924)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:46:24 -07:00
dependabot[bot]
aa872cd0a1 chore(deps): bump dompurify from 3.4.9 to 3.4.8 in /superset-frontend/packages/superset-ui-core (#40938)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:45:33 -07:00
dependabot[bot]
b2c5a1ecb3 chore(deps): bump jsonpath-ng from 1.7.0 to 1.8.0 (#40940)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:45:21 -07:00
dependabot[bot]
6cd9bdee0b chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.3 to 0.10.13 in /superset-frontend (#40925)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:44:40 -07:00
dependabot[bot]
a8a1d9c17d chore(deps): bump morgan from 1.10.1 to 1.11.0 in /superset-websocket/utils/client-ws-app (#40921)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:43:33 -07:00
dependabot[bot]
97058d2cf0 chore(deps): bump fuse.js from 7.3.0 to 7.4.1 in /superset-frontend (#40922)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:43:19 -07:00
dependabot[bot]
ef57409209 chore(deps): bump ag-grid-community from 35.3.0 to 35.3.1 in /superset-frontend/packages/superset-ui-core (#40923)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:43:06 -07:00
dependabot[bot]
5f06e66cf1 chore(deps): bump @deck.gl/mapbox from 9.3.2 to 9.3.3 in /superset-frontend (#40927)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:42:22 -07:00
dependabot[bot]
11af932099 chore(deps): bump dompurify from 3.4.7 to 3.4.8 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40937)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:42:06 -07:00
dependabot[bot]
c9c05d8d0a chore(deps-dev): update thrift requirement from <1.0.0,>=0.14.1 to >=0.23.0,<1.0.0 (#40942)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:36:51 -07:00
dependabot[bot]
0f59705806 chore(deps): bump wtforms from 3.2.1 to 3.2.2 (#40943)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:36:26 -07:00
dependabot[bot]
320965612d chore(deps-dev): update clickhouse-connect requirement from <2.0,>=0.13.0 to >=1.1.1,<2.0 (#40944)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:36:03 -07:00
dependabot[bot]
c3df60c12b chore(deps): bump selenium from 4.32.0 to 4.44.0 (#40945)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:34:01 -07:00
dependabot[bot]
4f69949c10 chore(deps-dev): bump eslint-plugin-storybook from 10.4.1 to 10.4.2 in /superset-frontend (#40949)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:31:47 -07:00
bogdanmoale
3380496e9f feat(i18n): add Romanian (ro) translations (#36712)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-10 12:25:42 -07:00
Michael S. Molina
248ccadecd fix(extensions): load extensions async to avoid blocking initial page render (#40915)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:23:38 -03:00
Joe Li
cc5a3ddd05 test(dashboard-filter): RTL coverage for horizontal filter bar (#40782)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:53:56 -07:00
Joe Li
f27424d72e fix(embedded): show filter bar controls on embedded dashboards with tabs (#39417)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-10 09:53:10 -07:00
Evan Rusackas
5a0e3f15ca feat(embedded): add guest token revocation support (#40671)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-10 09:17:30 -07:00
Đỗ Trọng Hải
3d1253c992 build(major,dev-deps): upgrade Storybook from v8 to v10 (#40713)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-06-10 23:05:18 +07:00
dependabot[bot]
2b58411391 chore(deps): bump ag-grid-react from 35.3.0 to 35.3.1 in /superset-frontend (#40928)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:53:40 +07:00
Evan Rusackas
08b8bdecbd fix(charts): tighten chart schema input validation (query_context JSON, prophet/rolling bounds) (#40634)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-10 08:17:12 -07:00
Evan Rusackas
0a1e51f542 fix(schemas): tighten guest dataset fields, external_url protocols, ssh creds, prophet bounds (#40640)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 18:30:30 -07:00
Joe Li
1bfdb19e88 test(dashboard): RTL coverage for native filter modal and sidebar (#40778)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 16:26:04 -07:00
Elizabeth Thompson
c0e78f39d7 fix: replace deprecated appbuilder.app with current_app (#40876)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:01:43 -07:00
Evan Rusackas
d51753dfdc chore(lint): convert reactify.tsx to function component (#39458)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:18:03 -07:00
dependabot[bot]
543ad04ca0 chore(deps): bump pyarrow from 20.0.0 to 24.0.0 (#39756)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:51:33 -07:00
Evan Rusackas
00e3682aaf fix(dashboard): URL-encode native_filters in permalink redirect (#40660)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 11:37:08 -07:00
Evan Rusackas
004101a752 fix(rls): apply standard datasource access checks in RLS rule commands (#40650)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 11:24:12 -07:00
Evan Rusackas
568f34d6d8 fix(mcp): enforce audience, algorithm, issuer binding, and token scopes (strict mode) (#40653)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 11:08:20 -07:00
Evan Rusackas
a0cf798409 fix(embedded): add Sec-Fetch-Dest defense-in-depth check on the embedded view (#40667)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 11:08:08 -07:00
dependabot[bot]
88ea96d417 chore(deps-dev): bump typescript-eslint from 8.60.0 to 8.60.1 in /docs (#40891)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 11:07:41 -07:00
dependabot[bot]
c88438ad35 chore(deps-dev): bump typescript-eslint from 8.60.0 to 8.60.1 in /superset-websocket (#40887)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 11:07:04 -07:00
Evan Rusackas
76f334f252 fix(i18n): stop fuzzy-matching new strings so adding one doesn't fail babel-extract (#40909)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:40:48 +07:00
Onur Taşhan
ab0fa5c3c8 test(dashboard): add regression tests for certification field clearing (#40758)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 10:37:14 -07:00
dependabot[bot]
9b4aaaa080 chore(deps-dev): bump @typescript-eslint/parser from 8.60.0 to 8.60.1 in /superset-websocket (#40889)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:35:34 -07:00
dependabot[bot]
eeaa213475 chore(deps): update react requirement from ^19.2.6 to ^19.2.7 in /superset-frontend/plugins/legacy-plugin-chart-chord (#40890)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:35:20 -07:00
dependabot[bot]
2d1b17d1ca chore(deps-dev): bump @typescript-eslint/parser from 8.60.0 to 8.60.1 in /docs (#40893)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:31:59 -07:00
dependabot[bot]
ff4783f1e4 chore(deps): bump github/codeql-action from 4.36.0 to 4.36.1 (#40894)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:30:30 -07:00
dependabot[bot]
f9ba11961a chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.60.0 to 8.60.1 in /superset-frontend (#40895)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:30:16 -07:00
Evan Rusackas
8117488fd8 feat(websocket): add configurable per-channel and total connection limits (#40856)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 10:30:07 -07:00
dependabot[bot]
336384bc67 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40896)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:29:48 -07:00
Evan Rusackas
065578e48a fix(commands,api): enforce command validation, sanitize export filename/token, set cache TTLs (#40655)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 10:29:46 -07:00
dependabot[bot]
3949089438 chore(deps-dev): bump oxlint from 1.67.0 to 1.68.0 in /superset-frontend (#40898)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:29:35 -07:00
dependabot[bot]
efa88b9b7f chore(deps-dev): bump eslint-import-resolver-typescript from 4.4.4 to 4.4.5 in /superset-frontend (#40899)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:29:20 -07:00
dependabot[bot]
f51736437d chore(deps): bump shell-quote from 1.8.3 to 1.8.4 in /docs (#40910)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 10:28:51 -07:00
EMMANUELA OPURUM
6311e2c315 fix: use pd.to_numeric in df_metrics_to_num to handle string-encoded numerics from ClickHouse (#40190)
Co-authored-by: Emmanuela Opurum <youremail@example.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-06-09 10:28:34 -07:00
dependabot[bot]
7a3b8f49c7 chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#40892)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-06-10 00:25:41 +07:00
Aleksey Karpov
17fb7a7c75 chore(helm): bump app version to 6.1.0 (#40862) 2026-06-09 10:17:55 -07:00
Evan Rusackas
bf9ad4d2ba fix: set charset via content_type to avoid malformed Content-Type headers (#40658)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 10:17:44 -07:00
Evan Rusackas
6681ab571d perf(websocket): yield to the event loop while processing large event batches (#40866)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:59:03 -07:00
dependabot[bot]
58d29e0779 chore(deps-dev): bump concurrently from 10.0.0 to 10.0.3 in /superset-frontend (#40897)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 23:56:31 +07:00
Evan Rusackas
0133ebc9f2 feat(mcp): log successful JWT authentication events (#40864)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:34:52 -07:00
Evan Rusackas
b64dd4af4a fix(mcp): handle JWKS fetch network errors during token verification (#40869)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:34:33 -07:00
Evan Rusackas
95d46073cb feat(websocket): add backpressure handling for slow consumers (#40857)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:33:59 -07:00
Evan Rusackas
7b1e1e5668 fix(charts): route CSV result format through the escaping CSV writer (#40859)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:33:46 -07:00
Evan Rusackas
62084f4015 feat(websocket): improve operational logging and crash safety (#40868)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:33:13 -07:00
Evan Rusackas
f70cd8b5b8 feat(embedded-sdk): add fetchGuestToken timeout and clean up refresh timer (#40870)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:33:01 -07:00
Evan Rusackas
a32b7b1523 fix(migrations): replay user_favorite_tag migration for 3.0.x upgraders (#38157)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-06-09 09:30:09 -07:00
Evan Rusackas
9105adc67b fix(mcp): return a generic message when a request is unauthenticated (#40861)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 09:19:15 -07:00
Sebastian Mohr
443fd7bcee fix(assets): Support uploading tags using the assets import endpoint (#38343)
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
2026-06-09 10:13:28 -04:00
Kilobyte9304
3259a4a781 fix(translation): update german translation for slider range (#40902) 2026-06-09 09:07:05 -04:00
Alexandru Soare
56c856e802 fix(explore): require Update Chart for Matrixify dimension changes (#40851) 2026-06-09 15:09:41 +03:00
Daniel Vaz Gaspar
2f71771b56 fix(sqllab): prevent corrupted query state from blocking SQL Lab access (#40580)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-06-09 10:51:45 +01:00
Mehmet Salih Yavuz
d7ddf2023d fix(theme): SDK theme config overrides dashboard-level theme in embedded mode (#40763) 2026-06-09 12:01:57 +03:00
Evan Rusackas
c58408d76c fix(revert 40875): "ci: authenticate Docker Hub pulls for service containers" failed (#40879) 2026-06-09 11:17:59 +07:00
Evan Rusackas
1188cfef1d ci: make Docker-build npm ci resilient to transient network blips (#40874)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 08:58:01 +07:00
Evan Rusackas
fb0e7fecaf ci: authenticate Docker Hub pulls for service containers (#40875)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-09 08:57:31 +07:00
Evan Rusackas
3afbb48188 fix(uploads,dao): add zip-safety check to columnar reader and cap DAO page size (#40637)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 17:07:57 -07:00
Evan Rusackas
837f41986d fix: reject default guest/async JWT secrets at startup (#40649)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 16:53:37 -07:00
Evan Rusackas
8eda626466 fix: raise random_key entropy and add expiry to async query tokens (#40638)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 16:24:06 -07:00
Evan Rusackas
fe9818226d fix(viz): gate stacktrace behind SHOW_STACKTRACE and allowlist resample method (#40636)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 16:09:59 -07:00
Joe Li
1e8438a478 test(dashboard): migrate favorite toggle Cypress spec to RTL (#40872)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 16:03:59 -07:00
dependabot[bot]
8fdabc44f5 chore(deps): update react-draggable requirement from ^4.5.0 to ^4.6.0 in /superset-frontend/packages/superset-ui-core (#40841)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:56:20 -07:00
Evan Rusackas
e9e9245112 test(mixed-chart): dashboard filters should reach both Mixed chart queries (#29519) (#40818)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 15:55:41 -07:00
Evan Rusackas
580be2cf32 fix(extensions-cli): constrain backend include patterns to the backend directory (#40593)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 15:42:06 -07:00
Evan Rusackas
911bb9dcda fix: harden ZIP safety checks (total-size cap, zero-division guard) and extension path matching (#40664)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 14:14:53 -07:00
Evan Rusackas
507cf93687 test(dashboard): API-created dashboards should link charts from position_json (#32966) (#40816)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 10:51:25 -07:00
dependabot[bot]
ba6e9cc90f chore(deps-dev): bump eslint from 10.4.0 to 10.4.1 in /superset-websocket (#40840)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:29:08 -07:00
dependabot[bot]
228ac0d568 chore(deps): bump baseline-browser-mapping from 2.10.32 to 2.10.33 in /docs (#40842)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:27:31 -07:00
dependabot[bot]
c6ecaf9642 chore(deps): bump js-yaml from 4.1.1 to 4.2.0 in /docs (#40843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:27:16 -07:00
dependabot[bot]
534d2191ff chore(deps): bump react-draggable from 4.5.0 to 4.6.0 in /superset-frontend (#40844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:27:03 -07:00
dependabot[bot]
709fd52b0b chore(deps-dev): bump tsx from 4.22.3 to 4.22.4 in /superset-frontend (#40845)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:26:29 -07:00
dependabot[bot]
c5d795c1f1 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40847)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:25:55 -07:00
dependabot[bot]
983f2818b0 chore(deps-dev): bump @swc/plugin-emotion from 14.10.0 to 14.12.0 in /superset-frontend (#40848)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:24:52 -07:00
dependabot[bot]
b4eda37fbf chore(deps-dev): bump baseline-browser-mapping from 2.10.32 to 2.10.33 in /superset-frontend (#40849)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 10:23:53 -07:00
Evan Rusackas
a5fe47ee71 docs(footer): render social icons as uniform white (#40854)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-08 09:30:58 -07:00
Onur Taşhan
dc423b22b3 feat(embedded): support themeMode URL param for initial theme (#40760)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:28:09 -07:00
Alexandru Soare
7c7ab88a60 feat(ListView): expose expandable prop (#40765) 2026-06-08 15:23:15 +03:00
jesperct
21189ae130 fix(dashboard): update browser tab title when dashboard is renamed (#40730)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-06-08 10:42:59 +02:00
Evan Rusackas
06f95f5362 refactor(explore): migrate Explore Controls from react-dnd to @dnd-kit (#37880)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-07 20:00:03 -07:00
jesperct
5da63d716b fix(toasts): stop the toast overlay from covering controls behind it (#40805)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-06-07 04:10:43 -07:00
dependabot[bot]
9bb700ff0d chore(deps-dev): bump concurrently from 9.2.1 to 10.0.0 in /superset-frontend (#40798)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-06-07 17:02:09 +07:00
dependabot[bot]
c0a12f4cfb chore(deps-dev): update sqlalchemy-kusto requirement from <4,>=3.0.0 to >=3.1.2,<4 (#40828)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-07 17:01:25 +07:00
dependabot[bot]
138e405cb6 chore(deps): bump xlsxwriter from 3.0.9 to 3.2.9 (#40825)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:36:03 +07:00
dependabot[bot]
849f297e9d chore(deps-dev): update sqlalchemy-vertica-python requirement from <0.7,>=0.5.9 to >=0.6.3,<0.7 (#40826)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-07 13:16:27 +07:00
dependabot[bot]
9da4536354 chore(deps-dev): update oceanbase-py requirement from >=0.0.1 to >=0.0.1.2 (#40824)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-07 12:03:13 +07:00
dependabot[bot]
2463eb65b1 chore(deps-dev): bump python-ldap from 3.4.5 to 3.4.7 (#40827)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:58:49 +07:00
dependabot[bot]
d3f07a7ba5 chore(deps-dev): update playwright requirement from <2,>=1.37.0 to >=1.60.0,<2 (#40829)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-07 11:57:10 +07:00
dependabot[bot]
6348aa1917 chore(deps-dev): update taos-ws-py requirement from >=0.3.8 to >=0.6.9 (#40831)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-07 11:55:55 +07:00
Amin Ghadersohi
ef7379c47e chore(mcp): remove low-value list/info tools that fail agent-native policy (#40690) 2026-06-06 14:57:41 -04:00
Amin Ghadersohi
84aaaaa6b0 fix(mcp): filter sensitive database columns from list_databases loaded-metadata (#40771) 2026-06-06 14:57:21 -04:00
Evan Rusackas
b85a2cdab1 fix: ODPS (MaxCompute) data source table preview failed (#38174)
Co-authored-by: zhutong6688 <zhutong66@163.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 17:57:44 -07:00
Evan Rusackas
381b99ae84 fix(csv): respect CSV_EXPORT config for decimal separator and delimiter (#38170)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 17:57:21 -07:00
Evan Rusackas
6b0d747939 fix: cache warmup using WebDriver for reliable authentication (#38449)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 16:36:30 -07:00
Evan Rusackas
151df43d9d fix(docker): prevent static asset 404s by waiting for webpack dev server (#38161)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 15:19:50 -07:00
dependabot[bot]
3d7021fdf9 chore(deps): bump hot-shots from 14.3.1 to 15.0.0 in /superset-websocket (#40789)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:48:37 -07:00
dependabot[bot]
2babb48081 chore(deps): bump ioredis from 5.10.1 to 5.11.0 in /superset-websocket (#40734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:06:56 -07:00
dependabot[bot]
4715cfd372 chore(deps-dev): bump eslint-plugin-prettier from 5.5.5 to 5.5.6 in /docs (#40791)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:06:51 -07:00
Evan Rusackas
5a6306983e docs: add social media links to website footer and README (#38108)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 14:06:43 -07:00
dependabot[bot]
7f452e4096 chore(deps): bump @ant-design/icons from 6.2.3 to 6.2.5 in /docs (#40792)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 14:06:30 -07:00
Evan Rusackas
7eaaffde89 ci: cache npm downloads in the translations workflow (#40779)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-05 13:22:20 -07:00
Evan Rusackas
0984839788 ci: required-check anchors for cypress-matrix and playwright-tests (unblock docs-only PRs) (#40780)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-05 13:17:41 -07:00
Rabuma A. Bekele
863e93539a fix(dashboard): clean up JSON formatting and contribution suffix in V… (#40683) 2026-06-05 11:44:03 -07:00
Evan Rusackas
81bc3088e2 fix(dashboard): prevent stale favorite status errors after navigation (#38156)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 11:42:54 -07:00
Evan Rusackas
19d01521bf fix(dashboard): replace chartsInScope references at import time (#38171)
Co-authored-by: Rémy Dubois <remy.dubois@komodohealth.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-05 11:42:24 -07:00
Evan Rusackas
1623ceda73 fix(result_set): preserve JSON/JSONB data as objects instead of strings (#38172)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:41:40 -07:00
yousoph
e956f82224 fix(dashboard): prevent divider display controls from reverting on second save (#40696)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:36:55 -07:00
dependabot[bot]
2aca35cb68 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40793)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 11:32:41 -07:00
dependabot[bot]
44777cc110 chore(deps): bump @ant-design/icons from 6.2.3 to 6.2.5 in /superset-frontend (#40794)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 11:32:28 -07:00
dependabot[bot]
20024ce3af chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 0.10.2 to 0.10.4 in /superset-frontend (#40796)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 11:32:13 -07:00
dependabot[bot]
b069b6caf6 chore(deps-dev): bump terser-webpack-plugin from 5.6.0 to 5.6.1 in /superset-frontend (#40797)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 11:31:59 -07:00
dependabot[bot]
70ee6e21eb chore(deps-dev): bump @babel/core from 7.29.0 to 7.29.7 in /superset-frontend (#40800)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 11:31:44 -07:00
Evan Rusackas
550c80f640 chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-06-05 10:58:44 -07:00
innovark
108e40cbb6 feat(duration-format): replace pretty-ms with native Intl.DurationFormat for localized duration formatting (#39330) 2026-06-05 10:33:17 -07:00
jesperct
8119204857 fix(dashboard): sort Dynamic Group By display values alphabetically (#40220) 2026-06-05 10:32:54 -07:00
dependabot[bot]
645aa3b1df chore(deps-dev): bump eslint-plugin-prettier from 5.5.5 to 5.5.6 in /superset-frontend (#40795)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 10:18:56 -07:00
Evan Rusackas
55bb75efe6 fix(dashboard): prevent filter dropdown button from disappearing during layout recalculations (#38193)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-06-05 10:09:50 -07:00
Richard Fogaca Nienkotter
601f9c2b8c fix(embedded): add guest token to streaming exports (#40712)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-06-05 13:27:06 -03:00
madhushreeag
fa42b13eb8 fix(dataset): preserve numeric column types when pydruid infers STRING from first-row value (#40677)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-06-05 09:25:57 -07:00
Amin Ghadersohi
aa4092ba68 fix(mcp): add select_columns lean defaults to get_dashboard_info, get_chart_info, get_dataset_info (#40473)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Richard Fogaça <richardfogaca@gmail.com>
2026-06-05 11:10:13 -03:00
dependabot[bot]
45a616439b chore(deps): update dayjs requirement from ^1.11.20 to ^1.11.21 in /superset-frontend/packages/superset-ui-core (#40736)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:59:17 -07:00
dependabot[bot]
98c096df05 chore(deps): bump @babel/runtime from 7.29.2 to 7.29.7 in /superset-frontend (#40753)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 20:59:08 -07:00
Elizabeth Thompson
42367afb25 fix(reports): add per-tile animation wait to prevent partial ECharts renders in tiled screenshots (#40694)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:43:34 -07:00
Evan Rusackas
875673f670 fix(asyncEvent): use Map for job listener/retry registries (#40747)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 14:16:44 -07:00
Evan Rusackas
79c74af2e9 ci: cache npm downloads in frontend-heavy workflows (#40744)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 13:34:35 -07:00
Vitor Avila
7406098708 fix(dashboard-filter): Consider dashboard filters to charts not declared in the dashboard position (#40774) 2026-06-04 16:43:38 -03:00
dependabot[bot]
ccce0cab18 chore(deps): bump content-disposition from 2.0.0 to 2.0.1 in /superset-frontend (#40750)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:32:09 -07:00
dependabot[bot]
94c1a1b1f2 chore(deps-dev): bump @babel/runtime-corejs3 from 7.29.2 to 7.29.7 in /superset-frontend (#40751)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:31:59 -07:00
dependabot[bot]
04939c94cc chore(deps-dev): bump @babel/node from 7.29.0 to 7.29.7 in /superset-frontend (#40752)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:31:49 -07:00
dependabot[bot]
937eff6d52 chore(deps-dev): bump oxlint from 1.66.0 to 1.67.0 in /superset-frontend (#40755)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:31:23 -07:00
dependabot[bot]
f5f4a41598 chore(deps-dev): bump @babel/register from 7.29.3 to 7.29.7 in /superset-frontend (#40757)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:30:55 -07:00
Evan Rusackas
639866625d fix(echarts): Show full labels in bar chart tooltips (#34759)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-04 12:29:48 -07:00
Evan Rusackas
7d323dc0ae fix(filters): Enable decimal values in Range filter slider (#34742)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 12:29:33 -07:00
Evan Rusackas
0d1b702ce8 feat(extensions): static supply-chain controls — denylist + version policy (#40668)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 12:29:03 -07:00
dependabot[bot]
ddeec68c88 chore(deps): bump dompurify from 3.4.5 to 3.4.7 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:28:13 -07:00
dependabot[bot]
0ad09d5cd0 chore(deps): bump dompurify from 3.4.8 to 3.4.7 in /superset-frontend/packages/superset-ui-core (#40737)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:27:26 -07:00
dependabot[bot]
6662529306 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40739)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:27:18 -07:00
dependabot[bot]
09cd2c26cd chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40740)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:27:05 -07:00
dependabot[bot]
cbd731e661 chore(deps-dev): bump webpack from 5.107.1 to 5.107.2 in /superset-frontend (#40741)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:26:54 -07:00
dependabot[bot]
3f94c9db2d chore(deps): bump query-string from 9.3.1 to 9.4.0 in /superset-frontend (#40742)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 12:26:46 -07:00
Evan Rusackas
80a3df3550 ci: run full Python-version matrix on push, current-only on PRs (#40722)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 21:17:29 +02:00
Evan Rusackas
6f97d9817e fix(database): preserve engine_information when creating database connection (#38107)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-04 12:04:59 -07:00
Amin Ghadersohi
7d69f76127 fix(mcp): API key authentication for MCP — transport, validation, and RBAC (#39604) 2026-06-04 15:04:43 -04:00
Evan Rusackas
9a31362fa5 fix(reports): stamp email subject date at send time, not import time (#40693)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:03:28 -07:00
Joe Li
cd5bdf11ac fix(playwright): de-flake list-view delete and bulk-export specs (#39980)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 11:41:36 -07:00
Evan Rusackas
75d94ff466 fix(SafeMarkdown): block script-executing link protocols regardless of EscapeMarkdownHtml (#40622)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 11:13:31 -07:00
Evan Rusackas
c505c70c52 fix(databases): do not render existing encrypted field value in edit mode (#40628)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 10:15:01 -07:00
Evan Rusackas
23d18743bd fix(deck.gl): strip all JS-executed form_data keys when JavaScript controls are disabled (#40602)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 10:14:33 -07:00
Evan Rusackas
ddb09f468d fix(plugin-chart-ag-grid-table): enforce numeric bounds for range (BETWEEN) filters (#40607)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Shaitan <105581038+sha174n@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 10:14:21 -07:00
Evan Rusackas
8dcc7e7eec ci: stable required-check anchors for skippable matrix test jobs (#40772)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 09:50:06 -07:00
Evan Rusackas
ff5e43c8a0 ci: add timeout-minutes to compute-heavy workflow jobs (#40743)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 09:47:55 -07:00
Evan Rusackas
bdb081329f feat(websocket): validate WebSocket upgrade Origin against an allowlist (#40625)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 09:43:16 -07:00
Evan Rusackas
aa547da960 fix: remove registration_hash in the registrations API (#40643)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 09:43:03 -07:00
Evan Rusackas
966c243db6 ci: drop removed Cypress shards from required status checks (#40770)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-04 18:23:47 +02:00
Evan Rusackas
696705794b ci: gate docker image builds at the job level (#40723)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 15:39:01 -07:00
Shaitan
41572dbf9d fix(chart): restrict owner lookup to users with write access (#39304)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:00:31 +01:00
Evan Rusackas
5ba60d51fd ci: gate CodeQL analysis at the job level for docs-only PRs (#40724) 2026-06-03 23:49:59 +02:00
Evan Rusackas
cf5307d0c6 ci: reduce Cypress parallelism from 6 shards to 2 (#40717)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 23:48:46 +02:00
Evan Rusackas
9d1bc6b2cc fix(i18n): don't flag intentional string deletions as translation regressions (#40716)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:47:31 -07:00
Shaitan
6a125bf774 fix(jinja): expose dialect-escaped companion value on get_filters() (#40531) 2026-06-03 21:53:12 +01:00
Shaitan
43fde2fb07 fix(charts): enforce DISALLOWED_SQL_FUNCTIONS and DISALLOWED_SQL_TABLES at chart-data execution (#40567)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 21:52:48 +01:00
dependabot[bot]
2be2246a00 chore(deps-dev): bump gevent from 24.2.1 to 26.4.0 (#40378)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
2026-06-03 12:58:17 -07:00
Evan Rusackas
80a5f6b787 fix(calendar): Fix day offset in Calendar Heatmap visualization (#34564)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Joe Li <joe@preset.io>
2026-06-03 12:46:12 -07:00
Evan Rusackas
c373da1bb9 ci: add cancel-in-progress concurrency to PR helper workflows (#40725)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 12:44:36 -07:00
Evan Rusackas
80ea36c852 fix(db_engine_specs): escape schema name in regex; document safe filter pattern (#40642)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 11:56:51 -07:00
Evan Rusackas
6ea4e22785 refactor(nvd3): extract testable generateAnnotationTooltipContent helper (#40620)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:56:07 -07:00
Evan Rusackas
fcb1e299ac fix(nvd3): sanitize generateMultiLineTooltipContent output (#40612)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 11:55:55 -07:00
Amin Ghadersohi
f4dfb7f026 fix(mcp): fall back to form_data spatial query for Deck.gl charts (#40339) 2026-06-03 13:30:52 -04:00
Amin Ghadersohi
001834470b fix(mcp): escape LIKE wildcards in MCP list tool search filters (#40682) 2026-06-03 13:30:05 -04:00
Evan Rusackas
e5c7200551 ci: gate expensive test workflows at the job level (#40718)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 10:04:10 -07:00
Evan Rusackas
cb2a56d16e chore: guard recursive merge keys and invoke subprocess without a shell (#40558)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 22:18:05 +07:00
Burhanuddin Mundrawala
e5ff6de790 chore: correct typos in config.py and models_test.py comments (#40706) 2026-06-03 21:58:29 +07:00
faisal2901
accc94da51 fix(users): show 0 for null login_count and fail_login_count (#40281) 2026-06-03 10:14:46 -04:00
Evan Rusackas
c914df5a67 ci: harden CI against Docker Hub registry flakes (retries + auth) (#40700)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:53:24 +07:00
Shaitan
e3ba85b1a5 fix(redirect): normalize browser-stripped whitespace before protocol-relative check (#40566)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:56:10 +01:00
Shaitan
b8a2f925ee fix(views): enforce per-chart access check in legacy form_data endpoint (#40497)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:56:03 +01:00
Shaitan
77c2bed5f7 fix(dashboards): narrow datasets payload to callers with read access (#40396)
Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com>
2026-06-03 12:55:57 +01:00
Shaitan
56fd991efd fix(dataset): unify validation for stored and adhoc SQL expressions (#40392)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:55:50 +01:00
Shaitan
61b32d1b7d fix(chart): standardize dashboard validation across chart create/update (#40336)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:44 +01:00
Shaitan
3191b0fdcd fix: apply dashboard access check in related_objects endpoints (#40333)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:38 +01:00
Shaitan
cf08a5ebf7 feat(docker): add environment-based debugger control (#40327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Jay Masiwal <masiwaljay.02@gmail.com>
Co-authored-by: JUST.in DO IT <justin.park@airbnb.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: chaselynisabella <chaselynisabella@gmail.com>
2026-06-03 12:55:31 +01:00
Shaitan
f7f50a7977 fix(sqllab): quote CTAS target identifiers and validate tmp_table_name format (#40245)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:25 +01:00
Shaitan
725f5ed2a9 fix(api): enforce per-object ownership validation in chart, dataset, and report commands (#39303)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:15 +01:00
Shaitan
faa76f6741 fix(embedding): add optional dataset allowlist to guest tokens (#39302)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:09 +01:00
Shaitan
8e4a460cc7 fix(charts): apply DISALLOWED_SQL_FUNCTIONS gate to adhoc expressions (#40568)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:52:22 +01:00
Evan Rusackas
b9dc9d722e fix(export): sanitize user-supplied CSV export filename (charts + SQL Lab) (#40632)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-03 00:14:48 -07:00
Evan Rusackas
fa41769a08 fix(embedded): enforce configured allowed domains for postMessage origin (#40629)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 22:58:30 -07:00
Evan Rusackas
df21fe6571 chore(mcp): return a generic error from the webdriver pool-stats endpoint (#40559)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 21:51:32 -07:00
Evan Rusackas
12bef03f4a fix(jinja): apply consistent escaping to url_param values from request args (#40633)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 21:23:48 -07:00
Evan Rusackas
0b9764aed5 fix(mcp): honor AUTH_ROLE_ADMIN and warn on permission-less protected tools (#40659)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 21:20:11 -07:00
Evan Rusackas
ac522ded1c fix(ssh-tunnel): validate server_address format (SSRF defense-in-depth) (#40665)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 21:19:24 -07:00
Evan Rusackas
c54990c861 fix(plugin-chart-ag-grid-table): validate filter values/operators in state converter (#40623)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 18:44:29 -07:00
Evan Rusackas
3bbb35e8a3 ci(bashlib): drop the dead bc-based NONCE (perf + reliability) (#40691)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:49:49 -07:00
Shaitan
a2a369cb5c fix(charts): sanitize tooltip HTML across nvd3, rose and partition plugins (#40502)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-06-02 16:45:38 -07:00
Evan Rusackas
9af6746dbe fix(models): HTML-escape data-controlled values in dashboard_link and Slice.icons (#40639)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 16:15:11 -07:00
Evan Rusackas
6abee0289b fix(reports): guard SUCCESS-state report execution against duplicate sends and stuck WORKING state (#40657)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 15:09:14 -07:00
Evan Rusackas
8c62f533d7 fix(core): restrict allowed CSS properties in sanitized HTML (#40627)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 14:24:00 -07:00
Oleg Ovcharuk
17d1a45bc9 feat(ydb): switch to native YDB sqlglot dialect (#40170) 2026-06-02 17:13:41 -04:00
Shaitan
6eaee211aa fix(sqllab): require dataset match for raw query access (#40409)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:50:27 +01:00
Evan Rusackas
3e589436fa fix(reports): sanitize error text in email notification template (#40641)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 13:40:10 -07:00
Evan Rusackas
a9df2c7e5e fix(mcp): address post-approval review feedback on auth logging PR #40646 (#40684)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:39:23 -07:00
Evan Rusackas
8508af3201 chore(key_value): prune expired entries from the key-value store (#40663)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
2026-06-02 12:36:32 -07:00
Evan Rusackas
49f3dbba73 fix(dashboard): address post-approval review feedback on #40528 (#40685)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:16:15 -07:00
Amin Ghadersohi
616c243278 fix(deps): revert joserfc JWT error migration — fastmcp still uses authlib (#40688) 2026-06-02 12:02:17 -07:00
Evan Rusackas
00dd31494d fix: sanitize URL sinks and trim sensitive log fields (#40546)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 11:52:02 -07:00
Evan Rusackas
b97d3ef520 fix(api,sql): use json_response in Api.query and log dialect fallback (#40644)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 11:48:46 -07:00
Evan Rusackas
4d2b10d916 chore(excel): strip document metadata from Excel exports (#40661)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 11:48:36 -07:00
SBIN2010
86fa5bb46f feat(table v2): agGridTableChart add row numer column (#39284)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-06-02 11:37:26 -07:00
Evan Rusackas
19c2b67d09 fix(websocket): validate last_id query param format (#40626)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 11:36:33 -07:00
Jean Massucatto
d2d46169bf fix(explore): tighten popover title-to-tabs spacing to 12px (#40410) 2026-06-02 11:30:27 -07:00
Jean Massucatto
1b8099811b fix(chart-list): sort by changed_on instead of last_saved_at (#39984) 2026-06-02 10:58:23 -07:00
Evan Rusackas
242c27a974 test(presto): 401 Unauthorized must surface as CONNECTION_ACCESS_DENIED_ERROR (#33554) (#40618)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:49:58 -07:00
Evan Rusackas
24422c8311 test(histogram): metric filters require aggregation in buildQuery (#30330) (#40617)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:49:43 -07:00
Evan Rusackas
1632b235ae fix(sqllab): surface stacktrace in SQL Lab error responses (#28248) (#40585)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:41:39 -07:00
Evan Rusackas
093b43c7a5 fix(exports,email,logs): csv formula escaping, subject CRLF stripping, UTC log pruning (#40645)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 18:32:36 +01:00
Durgaprasad M L
4996d7c277 fix: avoid warning spam when default spinner SVG is missing (#40481)
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
2026-06-02 10:26:37 -07:00
Evan Rusackas
d26a7aac3d fix(dashboard): hide Edit button in embedded dashboards (#40687)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 10:17:10 -07:00
jesperct
699e741c69 fix(time-comparison): shift offset filter when X-axis is adhoc Custom SQL (#40586) 2026-06-02 09:52:42 -07:00
Jean Massucatto
fc0245bdb0 fix(charts): show non-filterable columns in metric section for table … (#39524) 2026-06-02 18:31:42 +02:00
Jean Massucatto
7275116f4c fix(world-map): preserve bubbles and exclude only null metrics from color scale (#39926) 2026-06-02 18:05:49 +02:00
Richard Fogaca Nienkotter
88abd41c8b fix(sql-lab): prevent crash when host shell lacks useAppDispatch export (#40591)
Co-authored-by: yousoph <sophieyou12@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:52:01 -03:00
Jean Massucatto
ddb647cd3a fix(dashboard): clear undo history (#40569) 2026-06-02 17:47:27 +02:00
Evan Rusackas
aba6ea536c fix(dashboard): prevent "undefined undefined" owner names in properties modal (#40528)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 08:31:37 -07:00
Evan Rusackas
ca8855dc03 fix(mcp): generic auth errors, required token expiry, and safer auth logging (#40646)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 08:31:05 -07:00
Evan Rusackas
052e567f77 fix: guard dynamic dispatch and bound a regex quantifier (#40547)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-02 08:09:22 -07:00
748 changed files with 46088 additions and 25140 deletions

View File

@@ -77,23 +77,17 @@ github:
# combination here.
contexts:
- lint-check
- cypress-matrix (0, chrome)
- cypress-matrix (1, chrome)
- cypress-matrix (2, chrome)
- cypress-matrix (3, chrome)
- cypress-matrix (4, chrome)
- cypress-matrix (5, chrome)
- cypress-matrix-required
- dependency-review
- frontend-build
- playwright-tests (chromium)
- playwright-tests-required
- pre-commit (current)
- pre-commit (previous)
- test-mysql
- test-postgres (current)
- test-postgres-required
- test-postgres-hive
- test-postgres-presto
- test-sqlite
- unit-tests (current)
- unit-tests-required
required_pull_request_reviews:
dismiss_stale_reviews: false

View File

@@ -14,12 +14,6 @@ updates:
- package-ecosystem: "npm"
ignore:
# TODO: remove below entries until React >= 18.0.0
- dependency-name: "storybook"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@storybook*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "eslint-plugin-storybook"
- dependency-name: "react-error-boundary"
- dependency-name: "@rjsf/*"
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
@@ -42,14 +36,6 @@ updates:
# and confirm the issue https://github.com/apache/superset/issues/39600 is fixed
- dependency-name: "react-checkbox-tree"
update-types: ["version-update:semver-major"]
groups:
storybook:
applies-to: version-updates
patterns:
- "@storybook*"
- "storybook"
update-types:
- "patch"
directory: "/superset-frontend/"
schedule:
interval: "daily"
@@ -90,21 +76,7 @@ updates:
- package-ecosystem: "npm"
directory: "/docs/"
ignore:
# TODO: remove below entries until React >= 18.0.0 in superset-frontend
- dependency-name: "storybook"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@storybook*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "eslint-plugin-storybook"
- dependency-name: "react-error-boundary"
groups:
storybook:
applies-to: version-updates
patterns:
- "@storybook*"
- "storybook"
update-types:
- "patch"
schedule:
interval: "daily"
open-pull-requests-limit: 10

View File

@@ -20,10 +20,6 @@ set -e
GITHUB_WORKSPACE=${GITHUB_WORKSPACE:-.}
ASSETS_MANIFEST="$GITHUB_WORKSPACE/superset/static/assets/manifest.json"
# Rounded job start time, used to create a unique Cypress build id for
# parallelization so we can manually rerun a job after 20 minutes
NONCE=$(echo "$(date "+%Y%m%d%H%M") - ($(date +%M)%20)" | bc)
# Echo only when not in parallel mode
say() {
if [[ $(echo "$INPUT_PARALLEL" | tr '[:lower:]' '[:upper:]') != 'TRUE' ]]; then

View File

@@ -30,9 +30,8 @@ jobs:
pull-requests: write
checks: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: true
ref: master

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
@@ -38,6 +38,19 @@ jobs:
if: steps.check.outputs.python
uses: ./.github/actions/setup-backend/
# Authenticate the Docker daemon so the python:slim pull in
# uv-pip-compile.sh uses our (much higher) authenticated rate limit
# instead of the shared-runner anonymous one. Best-effort: on fork PRs the
# secrets are unavailable, so this no-ops and the pull falls back to
# anonymous (covered by the retry loop in the script).
- name: Login to Docker Hub
if: steps.check.outputs.python
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run uv
if: steps.check.outputs.python
run: ./scripts/uv-pip-compile.sh

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check and notify

View File

@@ -75,14 +75,14 @@ jobs:
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
fetch-depth: 1
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
fetch-depth: 1
- name: Run Claude PR Action
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
- name: Run Claude PR Action
uses: anthropics/claude-code-action@5fb899572b81d2bb648d4d187173a2f423a9677c # beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"

View File

@@ -15,9 +15,35 @@ concurrency:
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
analyze:
name: Analyze
needs: changes
# Skip on PRs that touch neither code group (e.g. docs-only) so the
# analysis runners don't spin up. push/schedule runs always proceed:
# the change-detector returns "all changed" for non-PR events.
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
actions: read
contents: read
@@ -31,19 +57,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,7 +74,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: "Dependency Review"
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

View File

@@ -18,9 +18,30 @@ concurrency:
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
frontend: ${{ steps.check.outputs.frontend }}
docker: ${{ steps.check.outputs.docker }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
setup_matrix:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
matrix_config: ${{ steps.set_matrix.outputs.matrix_config }}
steps:
@@ -32,8 +53,13 @@ jobs:
docker-build:
name: docker-build
needs: setup_matrix
needs: [setup_matrix, changes]
if: >-
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.frontend == 'true' ||
needs.changes.outputs.docker == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
matrix:
build_preset: ${{fromJson(needs.setup_matrix.outputs.matrix_config)}}
@@ -44,20 +70,12 @@ jobs:
IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Docker Environment
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
uses: ./.github/actions/setup-docker
with:
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
@@ -65,11 +83,9 @@ jobs:
build: "true"
- name: Setup supersetbot
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
uses: ./.github/actions/setup-supersetbot/
- name: Build Docker Image
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -95,7 +111,7 @@ jobs:
# in the context of push (using multi-platform build), we need to pull the image locally
- name: Docker pull
if: github.event_name == 'push' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker)
if: github.event_name == 'push'
run: |
for i in 1 2 3; do
docker pull $IMAGE_TAG && break
@@ -103,7 +119,6 @@ jobs:
done
- name: Print docker stats
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
run: |
echo "SHA: ${{ github.sha }}"
echo "IMAGE: $IMAGE_TAG"
@@ -111,7 +126,7 @@ jobs:
docker history $IMAGE_TAG
- name: docker-compose sanity check
if: (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'dev'
if: matrix.build_preset == 'dev'
shell: bash
env:
BUILD_PRESET: ${{ matrix.build_preset }}
@@ -124,20 +139,16 @@ jobs:
docker-compose-image-tag:
# Run this job only on pushes to master (not for PRs)
# goal is to check that building the latest image works, not required for all PR pushes
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
needs: changes
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.changes.outputs.docker == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Docker Environment
if: steps.check.outputs.docker
uses: ./.github/actions/setup-docker
with:
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
@@ -145,7 +156,6 @@ jobs:
build: "false"
install-docker-compose: "true"
- name: docker-compose sanity check
if: steps.check.outputs.docker
shell: bash
run: |
docker compose -f docker-compose-image-tag.yml up superset-init --exit-code-from superset-init

View File

@@ -33,13 +33,13 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
node-version-file: "./superset-embedded-sdk/.nvmrc"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run ci:release
env:

View File

@@ -21,13 +21,13 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
node-version-file: "./superset-embedded-sdk/.nvmrc"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm test
- run: npm run build

View File

@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive

View File

@@ -12,8 +12,12 @@ on:
permissions:
contents: read
jobs:
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
validate-all-ghas:
runs-on: ubuntu-24.04
permissions:
@@ -23,14 +27,14 @@ jobs:
security-events: write
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
node-version: "20"
- name: Install Dependencies
run: npm install -g @action-validator/core @action-validator/cli --save-dev

View File

@@ -15,9 +15,8 @@ jobs:
pull-requests: write
issues: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

View File

@@ -2,6 +2,11 @@ name: "Pull Request Labeler"
on:
- pull_request_target
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
labeler:
permissions:

View File

@@ -11,29 +11,29 @@ jobs:
contents: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
submodules: recursive
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for latest tag
id: latest-tag
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
- name: Check for latest tag
id: latest-tag
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run latest-tag
uses: ./.github/actions/latest-tag
if: steps.latest-tag.outputs.SKIP_TAG != 'true'
with:
description: Superset latest release
tag-name: latest
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Run latest-tag
uses: ./.github/actions/latest-tag
if: steps.latest-tag.outputs.SKIP_TAG != 'true'
with:
description: Superset latest release
tag-name: latest
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -18,14 +18,14 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'temurin'
java-version: '11'
distribution: "temurin"
java-version: "11"
- name: Run license check
run: ./scripts/check_license.sh

View File

@@ -8,6 +8,11 @@ on:
# Possible values: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
types: [opened, edited, reopened, synchronize]
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
lint-check:
runs-on: ubuntu-24.04
@@ -16,7 +21,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
@@ -26,6 +31,5 @@ jobs:
on-failed-regex-fail-action: true
on-failed-regex-request-changes: false
on-failed-regex-create-review: false
on-failed-regex-comment:
"Please format your PR title to match: `%regex%`!"
on-failed-regex-comment: "Please format your PR title to match: `%regex%`!"
repo-token: "${{ github.token }}"

View File

@@ -19,12 +19,16 @@ concurrency:
jobs:
pre-commit:
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
matrix:
python-version: ["current", "previous", "next"]
# Run the full version spread on push (master/release) and nightly,
# but only the current version on PRs — lint/format/type results
# rarely differ across patch versions, so 3x per PR is wasteful.
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
@@ -44,7 +48,9 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '20'
node-version: "20"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install Frontend Dependencies
run: |
@@ -68,7 +74,7 @@ jobs:
id: changed_files
uses: ./.github/actions/file-changes-action
with:
output: ' '
output: " "
- name: pre-commit
env:

View File

@@ -33,7 +33,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# pulls all commits (needed for lerna / semantic release to correctly version)
@@ -52,7 +52,7 @@ jobs:
if: env.HAS_TAGS
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
- name: Cache npm
if: env.HAS_TAGS

View File

@@ -10,11 +10,11 @@ on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to sync'
description: "PR number to sync"
required: true
type: number
sha:
description: 'Specific SHA to deploy (optional, defaults to latest)'
description: "Specific SHA to deploy (optional, defaults to latest)"
required: false
type: string
@@ -152,7 +152,7 @@ jobs:
- name: Checkout PR code (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false

View File

@@ -41,7 +41,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive

View File

@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
persist-credentials: false
@@ -68,13 +68,13 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './docs/.nvmrc'
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'zulu'
java-version: '21'
distribution: "zulu"
java-version: "21"
- name: Install Graphviz
run: sudo apt-get install -y graphviz
- name: Compute Entity Relationship diagram (ERD)

View File

@@ -28,12 +28,12 @@ jobs:
name: Link Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
with:
paths: "**/*.md, **/*.mdx"
@@ -73,14 +73,14 @@ jobs:
working-directory: docs
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './docs/.nvmrc'
node-version-file: "./docs/.nvmrc"
- name: yarn install
run: |
yarn install --check-cache
@@ -112,7 +112,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
@@ -120,7 +120,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './docs/.nvmrc'
node-version-file: "./docs/.nvmrc"
- name: yarn install
run: |
yarn install --check-cache
@@ -131,7 +131,7 @@ jobs:
run_id: ${{ github.event.workflow_run.id }}
name: database-diagnostics
path: docs/src/data/
if_no_artifact_found: 'warning'
if_no_artifact_found: "warning"
- name: Use fresh diagnostics
run: |
if [ -f "src/data/databases-diagnostics.json" ]; then

View File

@@ -10,26 +10,49 @@ on:
workflow_dispatch:
inputs:
use_dashboard:
description: 'Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work.'
description: "Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work."
required: false
default: 'false'
default: "false"
ref:
description: 'The branch or tag to checkout'
description: "The branch or tag to checkout"
required: false
default: ''
default: ""
pr_id:
description: 'The pull request ID to checkout'
description: "The pull request ID to checkout"
required: false
default: ''
default: ""
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
cypress-matrix:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
# Somehow one test flakes on 24.04 for unknown reasons, this is the only GHA left on 22.04
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: read
pull-requests: read
@@ -40,9 +63,14 @@ jobs:
# https://github.com/cypress-io/github-action/issues/48
fail-fast: false
matrix:
parallel_id: [0, 1, 2, 3, 4, 5]
parallel_id: [0, 1]
browser: ["chrome"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
# The /app/prefix variant (push events only) is smoke-tested on a single
# shard rather than the full matrix, so exclude it from the other shards.
exclude:
- parallel_id: 1
app_root: "/app/prefix"
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -69,71 +97,60 @@ jobs:
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python || steps.check.outputs.frontend
- name: Setup postgres
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Import test data
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Build javascript packages
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-instrumented-assets
- name: Install cypress
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: cypress-install
- name: Run Cypress
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
env:
CYPRESS_BROWSER: ${{ matrix.browser }}
PARALLEL_ID: ${{ matrix.parallel_id }}
PARALLELISM: 6
PARALLELISM: 2
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
NODE_OPTIONS: "--max-old-space-size=4096"
with:
@@ -154,7 +171,10 @@ jobs:
name: cypress-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}-${{ matrix.parallel_id }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
playwright-tests:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: read
pull-requests: read
@@ -187,71 +207,59 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python || steps.check.outputs.frontend
- name: Setup postgres
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Import test data
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: playwright_testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Build javascript packages
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-instrumented-assets
- name: Build embedded SDK
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-embedded-sdk
- name: Install Playwright
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: playwright-install
- name: Run Playwright (Required Tests)
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
@@ -273,3 +281,63 @@ jobs:
${{ github.workspace }}/superset-frontend/playwright-results/
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
# Stable required-status-check anchors. cypress-matrix and playwright-tests
# are matrix jobs gated on change detection (python || frontend). On a PR
# that touches neither — e.g. a docs-only PR — they are skipped at the job
# level, which happens before matrix expansion, so the per-combination
# contexts (`cypress-matrix (0, chrome)`, `playwright-tests (chromium)`) are
# never produced and branch protection waits on them forever. These
# always-running jobs report a single stable context that passes when the
# underlying matrix job succeeded or was skipped, and fails only on a real
# failure. Require these in .asf.yaml instead of the matrix-expanded names.
#
# A matrix job reads as "skipped" in two distinct cases, and only the first
# is a legitimate pass: (a) change detection succeeded and gated the job off
# (docs-only PR); (b) the `changes` job itself failed or was cancelled, in
# which case GHA skips its dependents too. Accepting (b) would let a broken
# change-detector report a false green, so each anchor first requires
# `changes` to have succeeded before honouring a skip.
cypress-matrix-required:
needs: [changes, cypress-matrix]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check cypress-matrix result
env:
CHANGES: ${{ needs.changes.result }}
RESULT: ${{ needs.cypress-matrix.result }}
run: |
if [ "$CHANGES" != "success" ]; then
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
exit 1
fi
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "cypress-matrix did not pass (result: $RESULT)"
exit 1
fi
echo "cypress-matrix result: $RESULT (changes: $CHANGES)"
playwright-tests-required:
needs: [changes, playwright-tests]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check playwright-tests result
env:
CHANGES: ${{ needs.changes.result }}
RESULT: ${{ needs.playwright-tests.result }}
run: |
if [ "$CHANGES" != "success" ]; then
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
exit 1
fi
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "playwright-tests did not pass (result: $RESULT)"
exit 1
fi
echo "playwright-tests result: $RESULT (changes: $CHANGES)"

View File

@@ -20,15 +20,18 @@ concurrency:
jobs:
test-superset-extensions-cli-package:
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
matrix:
python-version: ["previous", "current", "next"]
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
defaults:
run:
working-directory: superset-extensions-cli
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive

View File

@@ -22,11 +22,12 @@ permissions:
jobs:
frontend-build:
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
should-run: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
fetch-depth: 0
@@ -74,6 +75,7 @@ jobs:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
fail-fast: false
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
@@ -103,11 +105,12 @@ jobs:
needs: [sharded-jest-tests]
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
id-token: write
steps:
- name: Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
fetch-depth: 0
@@ -144,6 +147,7 @@ jobs:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
@@ -168,6 +172,7 @@ jobs:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Download Docker Image Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
@@ -187,6 +192,7 @@ jobs:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- name: Download Docker Image Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
@@ -33,7 +33,7 @@ jobs:
- name: Setup Python
uses: ./.github/actions/setup-backend/
with:
install-superset: 'false'
install-superset: "false"
- name: Set up chart-testing
uses: ./.github/actions/chart-testing-action

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true

View File

@@ -10,23 +10,46 @@ on:
workflow_dispatch:
inputs:
ref:
description: 'The branch or tag to checkout'
description: "The branch or tag to checkout"
required: false
default: ''
default: ""
pr_id:
description: 'The pull request ID to checkout'
description: "The pull request ID to checkout"
required: false
default: ''
default: ""
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
# NOTE: Required Playwright tests are in superset-e2e.yml (E2E / playwright-tests)
# This workflow contains only experimental tests that run in shadow mode
playwright-tests-experimental:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 30
continue-on-error: true
permissions:
contents: read
@@ -60,78 +83,65 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python || steps.check.outputs.frontend
- name: Setup postgres
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Import test data
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: playwright_testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Build javascript packages
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-instrumented-assets
- name: Build embedded SDK
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-embedded-sdk
- name: Install Playwright
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: playwright-install
- name: Run Playwright (Experimental Tests)
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: playwright-run "${{ matrix.app_root }}" experimental/
- name: Run Playwright (Embedded Tests)
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"

View File

@@ -14,8 +14,30 @@ concurrency:
cancel-in-progress: true
jobs:
test-mysql:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
test-mysql:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
id-token: write
env:
@@ -27,6 +49,8 @@ jobs:
services:
mysql:
image: mysql:8.0
# Authenticated pulls use our higher Docker Hub rate limit. Empty on
# fork PRs (secrets unavailable) -> runner falls back to anonymous.
env:
MYSQL_ROOT_PASSWORD: root
ports:
@@ -43,30 +67,21 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python
- name: Setup MySQL
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: setup-mysql
- name: Start Celery worker
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: celery-worker
- name: Python integration tests (MySQL)
if: steps.check.outputs.python
run: |
./scripts/python_tests.sh
- name: Upload code coverage
@@ -77,7 +92,6 @@ jobs:
use_oidc: true
slug: apache/superset
- name: Generate database diagnostics for docs
if: steps.check.outputs.python
env:
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
SUPERSET__SQLALCHEMY_DATABASE_URI: |
@@ -100,19 +114,23 @@ jobs:
print(f'Generated diagnostics for {len(docs)} databases')
"
- name: Upload database diagnostics artifact
if: steps.check.outputs.python
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: database-diagnostics
path: databases-diagnostics.json
retention-days: 7
test-postgres:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
id-token: write
strategy:
matrix:
python-version: ["current", "previous", "next"]
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -134,33 +152,24 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python
with:
python-version: ${{ matrix.python-version }}
- name: Setup Postgres
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: |
setup-postgres
- name: Start Celery worker
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: celery-worker
- name: Python integration tests (PostgreSQL)
if: steps.check.outputs.python
run: |
./scripts/python_tests.sh
- name: Upload code coverage
@@ -172,7 +181,10 @@ jobs:
slug: apache/superset
test-sqlite:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
id-token: write
env:
@@ -190,32 +202,23 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python
- name: Install dependencies
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: |
# sqlite needs this working directory
mkdir ${{ github.workspace }}/.temp
- name: Start Celery worker
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: celery-worker
- name: Python integration tests (SQLite)
if: steps.check.outputs.python
run: |
./scripts/python_tests.sh
- name: Upload code coverage
@@ -225,3 +228,25 @@ jobs:
verbose: true
use_oidc: true
slug: apache/superset
# Stable required-status-check anchor for the matrix-based test-postgres job.
# It is gated on change detection, so on non-Python PRs it is skipped and
# never produces its `test-postgres (current)` context (a job-level skip
# happens before matrix expansion). This always-running job reports a single
# context branch protection can require: it passes when test-postgres
# succeeded or was skipped, and fails only on a real failure.
test-postgres-required:
needs: [changes, test-postgres]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check test-postgres result
env:
RESULT: ${{ needs.test-postgres.result }}
run: |
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "test-postgres did not pass (result: $RESULT)"
exit 1
fi
echo "test-postgres result: $RESULT"

View File

@@ -15,8 +15,30 @@ concurrency:
cancel-in-progress: true
jobs:
test-postgres-presto:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
test-postgres-presto:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
id-token: write
env:
@@ -50,32 +72,21 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python == 'true'
- name: Setup Postgres
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: |
echo "${{ steps.check.outputs.python }}"
setup-postgres
run: setup-postgres
- name: Start Celery worker
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: celery-worker
- name: Python unit tests (PostgreSQL)
if: steps.check.outputs.python
run: |
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
@@ -87,7 +98,10 @@ jobs:
slug: apache/superset
test-postgres-hive:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
id-token: write
env:
@@ -113,39 +127,27 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create csv upload directory
if: steps.check.outputs.python
run: sudo mkdir -p /tmp/.superset/uploads
- name: Give write access to the csv upload directory
if: steps.check.outputs.python
run: sudo chown -R $USER:$USER /tmp/.superset
- name: Start hadoop and hive
if: steps.check.outputs.python
run: docker compose -f scripts/databases/hive/docker-compose.yml up -d
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python
- name: Setup Postgres
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Start Celery worker
if: steps.check.outputs.python
uses: ./.github/actions/cached-dependencies
with:
run: celery-worker
- name: Python unit tests (PostgreSQL)
if: steps.check.outputs.python
run: |
pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'

View File

@@ -15,40 +15,56 @@ concurrency:
cancel-in-progress: true
jobs:
unit-tests:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
id-token: write
strategy:
matrix:
python-version: ["previous", "current", "next"]
env:
PYTHONPATH: ${{ github.workspace }}
contents: read
pull-requests: read
outputs:
python: ${{ steps.check.outputs.python }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
unit-tests:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
id-token: write
strategy:
matrix:
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python
with:
python-version: ${{ matrix.python-version }}
- name: Python unit tests
if: steps.check.outputs.python
env:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50
- name: Python 100% coverage unit tests
if: steps.check.outputs.python
env:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
@@ -62,3 +78,25 @@ jobs:
verbose: true
use_oidc: true
slug: apache/superset
# Stable required-status-check anchor. `unit-tests` is a matrix job gated on
# change detection, so on non-Python PRs it is skipped and never produces its
# `unit-tests (current)` context (a job-level skip happens before matrix
# expansion). This always-running job reports a single context that branch
# protection can require: it passes when unit-tests succeeded or was skipped,
# and fails only on a real failure.
unit-tests-required:
needs: [changes, unit-tests]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check unit-tests result
env:
RESULT: ${{ needs.unit-tests.result }}
run: |
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "unit-tests did not pass (result: $RESULT)"
exit 1
fi
echo "unit-tests result: $RESULT"

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive
@@ -40,7 +40,9 @@ jobs:
if: steps.check.outputs.frontend
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install dependencies
if: steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
@@ -59,7 +61,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
submodules: recursive

View File

@@ -22,9 +22,10 @@ concurrency:
jobs:
app-checks:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install dependencies

View File

@@ -9,7 +9,7 @@ on:
workflow_dispatch:
inputs:
comment_body:
description: 'Comment Body'
description: "Comment Body"
required: true
type: string
@@ -38,7 +38,7 @@ jobs:
});
- name: "Checkout ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

View File

@@ -16,11 +16,11 @@ on:
force-latest:
required: true
type: choice
default: 'false'
default: "false"
description: Whether to force a latest tag on the release
options:
- 'true'
- 'false'
- "true"
- "false"
permissions:
contents: read
@@ -49,12 +49,12 @@ jobs:
contents: write
strategy:
matrix:
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
build_preset:
["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
fetch-depth: 0
@@ -119,9 +119,8 @@ jobs:
contents: read
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -32,14 +32,14 @@ jobs:
name: Generate Reports
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-frontend/.nvmrc'
node-version-file: "./superset-frontend/.nvmrc"
- name: Install Dependencies
run: npm ci

View File

@@ -1,895 +0,0 @@
Chatbot extensions
Author: Enzo Martellucci
Team: Preset
Status: Draft | Under Review | Completed
Day: May, 2026
1. Introduction
This SIP proposes a new extension point that enables third-party chatbot integrations to be embedded directly into the Superset user interface through the existing extension framework.
The goal is to provide a stable, supported mechanism for chatbot providers to integrate with Superset without requiring direct access to internal application state, Redux stores, or implementation-specific frontend modules. Chatbot extensions should interact with Superset through the same extension-oriented principles already established for other extension surfaces, such as SQL Lab.
The proposal focuses on three core concerns:
• Defining how chatbot extensions are registered and rendered.
• Defining how chatbot extensions receive contextual information about the currently active application surface.
• Defining how administrators manage chatbot availability and select the active chatbot when multiple chatbot extensions are installed.
This SIP intentionally does not prescribe any specific chatbot implementation, user experience, LLM provider, or backend architecture.
1.1 Motivation
AI-powered assistants are becoming a common way for users to interact with analytical applications. Superset should provide a standardized extension mechanism that allows community-built chatbot integrations to participate in the platform without depending on internal frontend implementation details.
Today, chatbot integrations must either be embedded through custom application modifications or rely on unsupported access to internal application state. Both approaches create maintenance challenges and make integrations fragile when frontend architecture evolves.
This SIP introduces a stable extension contract that:
• Enables chatbot integrations to be distributed as standard Superset extensions.
• Preserves separation between host and extension responsibilities.
• Allows chatbot implementations to access contextual information about the current page and entity being viewed.
• Keeps authorization and permission enforcement aligned with existing Superset APIs.
• Remains compatible with future frontend architecture changes.
1.2 Goals
The goals of this SIP are:
Introduce a dedicated chatbot extension point within the Superset application shell.
Provide chatbot extensions with host-managed, permission-aligned page context.
Establish stable extension-facing APIs for dashboard, explore, dataset, and navigation context.
Support deployment-wide administration of chatbot availability and selection.
Maintain isolation between chatbot implementations and Superset internals.
Preserve compatibility with future extension capabilities and AI-related initiatives.
1.3 Out of Scope
The following capabilities are explicitly out of scope for this SIP.
Client Actions and Agentic UI Manipulation
This SIP defines how chatbot extensions are mounted and how they receive context from the host application.
It does not define how a chatbot performs actions within the user interface, such as:
• Modifying chart configuration.
• Updating dashboard layouts.
• Editing SQL queries.
• Triggering frontend workflows.
These capabilities are deferred to the proposed Client Actions SIP.
Chatbot User Experience
The chatbot user interface remains entirely owned by the extension.
This SIP does not prescribe:
• Visual design.
• Conversation experience.
• Streaming behavior.
• Message persistence.
• Prompting strategy.
• Accessibility implementation details.
• Branding or styling.
LLM and Backend Infrastructure
The following concerns remain extension-specific:
• Model providers.
• MCP implementations.
• Agent frameworks.
• Tool execution systems.
• Prompt orchestration.
• Backend services.
Superset acts only as the host application and context provider.
2. Requirements
2.1 Functional Requirements
Registration and Rendering
The platform must allow extensions to register chatbot providers through the standard extension system.
The host must:
• Support registration of chatbot extensions.
• Render a chatbot UI contributed by an extension.
• Maintain a single active chatbot instance at any given time.
• Make the chatbot available across supported application surfaces.
• Support fully custom chatbot user interfaces.
Context Sharing
The platform must provide chatbot extensions with contextual information about the user's current application state.
At minimum, the host must expose:
• Current page type (`home`, `dashboard`, `explore`, `sqllab`, `dataset`, `other`).
• Dashboard context.
• Explore/chart context.
• Dataset identity context.
• SQL Lab context.
• Navigation events.
The chatbot must be notified of relevant context changes without polling.
Examples include:
• Route changes.
• Dashboard changes.
• Chart changes.
• Dataset changes.
• Title changes.
• Filter changes.
Host-Owned Context
Context exposed to extensions must be computed by the host application.
Extensions must not be required to:
• Read Redux state.
• Access internal application modules.
• Depend on component-level implementation details.
• Reconstruct semantic context from frontend internals.
Instead, extensions consume stable namespace APIs provided by the host.
Conversation State
The conversation state remains entirely owned by the chatbot extension.
This includes:
• Message history.
• Tool execution state.
• Streaming buffers.
• Conversation persistence.
• Session management.
The host is responsible only for exposing contextual information.
2.2 Non-Functional Requirements
Security and Authorization
Context shared with chatbot extensions must remain aligned with Superset's existing authorization model.
The host must not expose:
• Entities the current user cannot access.
• Metadata outside the user's permission scope.
• Datasource-derived information unavailable through existing APIs.
Authorization remains enforced by backend APIs. The extension-facing APIs defined by this SIP operate on data that has already been scoped to the current user.
Stable Extension Contracts
Extension-facing APIs must remain independent of frontend implementation details.
Extensions should rely on documented namespace contracts rather than:
• Redux slices.
• Internal selectors.
• Component state.
• Routing implementation details.
This allows frontend architecture to evolve without breaking extensions.
Performance
The architecture must minimize impact on existing application performance.
In particular:
• Context APIs must avoid unnecessary application re-renders.
• Context change notifications must not rely on polling.
• Chatbot integrations should not introduce additional work for unrelated surfaces.
Fault Isolation
Failures within chatbot extensions must not affect the stability of the host application.
Errors originating from third-party chatbot implementations should be isolated to the chatbot mount boundary.
Extensibility
The architecture should support future:
• Application surfaces.
• AI-related capabilities.
• Extension APIs.
• Context providers.
without requiring redesign of the chatbot extension model.
Vendor Neutrality
The architecture must remain independent of any specific:
• LLM provider.
• AI platform.
• Agent framework.
• Backend implementation.
3. Administration
3.1 Overview
Administrators can manage chatbot availability and select the active chatbot when multiple chatbot extensions are installed.
Administration is exposed through the existing Extensions management interface.
For chatbot extensions, administrators can:
• Enable or disable individual chatbot extensions.
• Select the default chatbot when multiple chatbot providers are available.
Only one chatbot may be active at a time.
3.2 Default Chatbot Selection
Extensions that contribute a chatbot view participate in a deployment-wide chatbot selection process.
The host discovers available chatbot candidates from the chatbot contribution location and allows administrators to designate a single active chatbot.
When multiple chatbot extensions are installed:
Administrators select the preferred chatbot.
The host resolves the active chatbot using the configured selection.
Only the selected chatbot is rendered.
Changes are applied dynamically without requiring a page reload.
3.3 Scope of Administration
The administration model introduced by this SIP is deployment-wide.
Administrative settings answer the question:
"Which chatbot integrations are available within this Superset deployment?"
They do not answer:
"Which chatbot integrations does a specific user prefer to use?"
This distinction is intentional.
Deployment administrators determine which integrations are available across the environment, while user-specific preferences remain a separate concern.
3.4 Future User Preferences
Per-user chatbot preferences are considered an important future capability but are intentionally out of scope for this SIP.
This proposal does not introduce user-scoped extension availability.
Instead, future user preferences should be layered on top of deployment availability using the following model:
Effective Availability = Deployment Availability AND User Preference
The recommended persistence layer for future user preferences is the Extension Storage API, which provides user-scoped extension storage and aligns with the architecture established by SIP-127 (User Preferences).
This separation preserves a clear distinction between:
• Deployment configuration.
• User customization.
and avoids introducing multiple ownership models for extension availability.
Consequently, this SIP focuses exclusively on deployment-wide administration and active chatbot selection. 4. Proposed Extension Point
4.1 Overview
This SIP introduces a single extension point that allows chatbot providers to integrate directly into the Superset application shell.
Extension Point
Contribution Location
Registration API
Cardinality
Chatbot Bubble
superset.chatbot
views.registerView()
Singleton
The chatbot contribution point is application-wide and persists across supported Superset surfaces, including dashboards, Explore, SQL Lab, and dataset-related pages.
Unlike most contribution locations, which allow multiple contributions to be rendered simultaneously, the chatbot location is intentionally exclusive and renders a single active provider.
4.2 Chatbot Contribution Location
Contribution Area
The contribution location introduced by this SIP is:
superset.chatbot
The host provides a fixed mount point within the application shell and renders the active chatbot provider at that location.
The mount point persists across route changes, allowing chatbot conversations and UI state to remain available while users navigate between application surfaces.
The chatbot extension contributes a single React component representing the entire chatbot experience.
Manifest Support
The current contribution manifest schema is focused on SQL Lab contribution locations and does not provide an application-shell-level contribution scope.
To support chatbot integrations, the manifest schema must be extended with an application-level contribution scope capable of declaring:
{
"views": {
"app": [
{
"location": "superset.chatbot"
}
]
}
}
This is a schema-level change and requires updates to both:
• Manifest validation.
• Runtime registration infrastructure.
The runtime registration API alone is not sufficient because chatbot contributions must also be discoverable through extension manifests.
4.3 Singleton Rendering Model
The chatbot location is intentionally exclusive.
Only one chatbot may be active at a time.
This differs from other contribution locations that allow multiple views to be rendered simultaneously.
Motivation
Chatbot interactions are inherently conversational and user-focused.
Rendering multiple chatbot providers simultaneously would:
• Create competing user experiences.
• Introduce ambiguity regarding which chatbot should respond.
• Increase UI complexity.
• Reduce discoverability.
For these reasons, chatbot rendering is treated as a deployment-level selection rather than a multi-provider composition model.
Resolution Rules
The host applies the following behavior:
Installed Chatbots
Behavior
None
No chatbot is rendered
One
The chatbot is rendered automatically
Multiple
The administrator-selected chatbot is rendered
The singleton policy is implemented entirely by the host.
Extensions continue to register normally through the existing view registry.
4.4 Provider Isolation
A key architectural principle of this SIP is that extensions may discover registrations but may not invoke another extension's rendering logic.
Public View Discovery
The existing registry exposes:
getViews(location);
This API returns metadata describing registered views:
interface View {
id: string;
name: string;
description?: string;
icon?: string;
}
The returned descriptors are intentionally passive metadata.
They allow extensions and host components to:
• Discover available contributions.
• Display contribution information.
• Populate administration interfaces.
They do not allow rendering.
Why Providers Are Not Exposed
The view provider is executable rendering logic.
If providers were exposed through the public registry:
• Extensions could render another extension's UI.
• Extensions could bypass host lifecycle management.
• Extensions could circumvent fault-isolation boundaries.
• Rendering ownership would become ambiguous.
This would violate the separation between extension discovery and extension execution.
For this reason:
"Extensions may discover registered views, but only the host may render registered views."
Host-Managed Resolution
The host uses internal APIs to resolve the active chatbot provider.
These APIs are not exposed through the public extension surface.
Conceptually:
const provider = getViewProvider("superset.chatbot", selectedId);
The active chatbot is determined through a host-managed resolution policy:
const chatbot = getActiveChatbot(adminSelectedId, enabledMap);
This policy considers:
• Enabled state.
• Administrative selection.
• Runtime settings.
• Registration state.
before rendering any provider.
As a result, chatbot selection is implemented as a host-side rendering policy rather than a new registration primitive.
4.5 Chatbot Lifecycle
Host Responsibilities
The host is responsible for:
• Providing the chatbot mount point.
• Resolving the active chatbot provider.
• Loading chatbot extensions.
• Managing chatbot lifecycle integration.
• Handling activation and deactivation.
• Maintaining fault isolation boundaries.
• Preserving chatbot availability across route changes.
• Providing context APIs defined by this SIP.
The host also provides fixed positioning and layering behavior to ensure chatbot visibility remains consistent throughout the application.
Fault Isolation
Chatbot providers execute within a host-managed boundary.
Failures originating from a chatbot extension must not affect the rest of the application.
Examples include:
• Module Federation loading failures.
• Runtime exceptions.
• Provider initialization errors.
If a chatbot fails to load, the host logs the failure, surfaces an appropriate notification, and continues operating normally.
The application shell remains functional even when the chatbot provider is unavailable.
4.6 Extension Responsibilities
The registered chatbot component owns the complete chatbot experience.
The extension is responsible for:
User Interface
• Collapsed bubble UI.
• Expanded panel UI.
• Branding.
• Icons and badges.
• Layout.
• Responsiveness.
Interaction Model
• Open and close behavior.
• Keyboard shortcuts.
• Focus management.
• Accessibility behavior.
• Conversation navigation.
Conversation Runtime
• Message history.
• Streaming state.
• Tool execution.
• Persistence.
• Session management.
Backend Integration
• LLM communication.
• MCP integration.
• Agent orchestration.
• Tool invocation.
The host does not manage any chatbot-specific runtime state.
4.7 Registration Example
Chatbot extensions register a single provider through the existing view registration API.
import { views, type ExtensionContext } from '@apache-superset/core';
import { ChatbotApp } from './ChatbotApp';
export function activate(context: ExtensionContext) {
const disposable = views.registerView(
{
id: 'acme.chatbot',
name: 'Acme Chatbot',
icon: 'Bubble',
},
'superset.chatbot',
() => <ChatbotApp />,
);
context.subscriptions.push(disposable);
}
The registration process remains consistent with existing extension contribution patterns.
The only difference is that the host applies singleton resolution before selecting the provider to render.
4.8 Chatbot Descriptor Metadata
Chatbot registrations may include an optional icon descriptor.
{
id: 'acme.chatbot',
name: 'Acme Chatbot',
icon: 'Bubble',
}
This metadata is used by:
• Extension administration interfaces.
• Chatbot selection interfaces.
• Extension discovery surfaces.
Design Decision
The icon descriptor is treated as static registration metadata.
Runtime UI state such as:
• Notification indicators.
• Unread counts.
• Loading states.
• Thinking indicators.
belongs to the chatbot component itself rather than the registration descriptor.
This keeps the registry simple while allowing chatbot implementations complete control over their user experience.
If future requirements emerge for host-visible dynamic icon updates, that capability can be introduced independently without expanding the registration model defined by this SIP. 5. Context and Namespace Model
5.1 Overview
Chatbot extensions require access to contextual information about the user's current activity within Superset. This SIP introduces a namespace-based context model that allows extensions to consume stable, host-managed APIs rather than depending on internal frontend implementation details.
The host exposes context through a set of surface-specific namespaces. Each namespace owns the context for a particular application surface and provides:
• Synchronous state getters.
• Event-based change notifications.
• Stable extension-facing contracts.
• Context aligned with the current user's authorized application view.
Extensions consume these namespaces and compose them into higher-level context models tailored to their own use cases.
5.2 Design Principles
The namespace model is guided by the following principles.
Stable Extension Contracts
Extensions must depend on documented APIs rather than frontend implementation details.
In particular, extensions must not depend on:
• Redux slices.
• Store shape.
• Selectors.
• Component-local state.
• Routing implementation details.
This allows Superset to evolve its frontend architecture without breaking extension integrations.
Host-Owned Context Normalization
The host is responsible for transforming application state into semantic extension-facing contracts.
Extensions consume normalized context rather than deriving it from raw frontend state.
Backend-Authorized Context
Authorization remains a backend responsibility.
Namespaces expose context that has already been scoped by backend APIs according to the current user's permissions.
Namespaces do not implement authorization logic themselves and should not be considered security boundaries.
Event-Driven Updates
Context changes are propagated through events rather than polling.
Extensions can subscribe to context updates and react immediately when relevant application state changes.
5.3 Available Namespaces
The following namespaces are available to chatbot extensions.
Namespace
Status
Purpose
sqlLab
Existing
SQL Lab context and events
authentication
Existing
Current user and session context
commands
Existing
Host actions and commands
dashboard
New
Dashboard context
explore
New
Explore/chart context
dataset
New
Dataset identity context
navigation
New
Routing and page context
The new namespaces introduced by this SIP follow the same high-level contract pattern established by the existing sqlLab namespace.
5.4 Namespace API Shape
Each namespace follows a common structure:
const current = namespace.getCurrent();
const disposable = namespace.onDidChange((next) => {
// react to updates
});
The exact contracts differ by surface, but every namespace provides:
• One or more synchronous getters.
• Event-based change notifications.
• Stable semantic contracts.
This pattern allows extensions to remain synchronized with application state without polling.
5.5 Dashboard Namespace
The dashboard namespace provides contextual information about the currently active dashboard.
API
dashboard.getCurrentDashboard();
Contract
interface DashboardContext {
dashboardId: number;
title: string;
filters: FilterValue[];
charts: ChartSummary[];
}
interface ChartSummary {
chartId: number;
chartName: string;
vizType: string;
datasourceId: number | null;
datasourceName: string | null;
isVisible: boolean;}
The context includes:
• Dashboard identity.
• Active filter state.
• Dashboard charts.
• Per-chart visibility information.
Returning all charts while exposing visibility allows chatbot implementations to answer both:
• "Which charts are currently visible?"
• "Find the chart named Revenue by Region."
without requiring additional lookups.
Normalization Requirements
The namespace must expose semantic dashboard context rather than raw application state.
For example:
dashboard.getCurrentDashboard();
returns a normalized contract rather than Redux slices or internal entities.
This abstraction layer preserves compatibility as frontend implementation details evolve.
Page-Type Guarding
The getter returns undefined when the current page is not a dashboard.
Conceptually:
if (navigation.getPageType() !== "dashboard") {
return undefined;
}
This prevents stale dashboard state from leaking across application surfaces.
5.6 Explore Namespace
The explore namespace provides context for the currently active Explore session.
API
explore.getCurrentChart();
Contract
interface ChartContext {
chartId: number | null;
chartName: string | null;
datasourceId: number | null;
datasourceName: string | null;
vizType: string;
}
The namespace exposes:
• Chart identity. `chartId` and `chartName` are null for a new, unsaved chart that has not yet been persisted.
• Saved chart metadata (name, datasource, viz type)
• Current Explore context: `vizType` reflects the type currently selected in the editor, so the value tracks the live session rather than only the last saved state.
The contract is intentionally focused on chart-specific information relevant to chatbot integrations.
Reflecting the live editing session — rather than reconstructing chart state from
the route alone — is the primary reason this SIP exposes frontend context
directly (see §6.2, Option C).
Page-Type Guarding
The getter returns undefined when the current page is not an Explore surface.
Conceptually:
if (navigation.getPageType() !== "explore") {
return undefined;
}
This ensures the namespace reflects only active Explore context.
5.7 Dataset Namespace
The dataset namespace exposes the dataset currently being viewed or edited.
API
dataset.getCurrentDataset();
Contract
interface DatasetContext {
datasetId: number;
datasetName: string;
schema: string | null;
catalog: string | null;
databaseName: string | null;
isVirtual: boolean;}
This contract is intentionally identity-focused.
It answers:
• Which dataset is currently in focus?
• Is the dataset virtual or physical?
• Which database and schema does it belong to?
It does not expose:
• Column definitions.
• Lineage information.
• Dataset dependencies.
Those concerns are expected to be resolved by backend services using the dataset identifier.
Producer-Backed Context
Unlike dashboard and explore namespaces, dataset pages do not currently expose a shared source of truth suitable for namespace consumption.
For this reason, dataset context is published by dataset pages through a host-managed producer mechanism.
Dataset pages publish the active dataset as it loads, and:
dataset.getCurrentDataset();
returns the most recently published value.
Until dataset information has been published, the getter returns:
undefined;
This design keeps the public contract stable without requiring the introduction of a dedicated Redux slice.
Example Use Cases
The dataset namespace enables chatbot workflows such as:
• Explain this dataset.
• Summarize this dataset's purpose.
• Show lineage for this dataset.
• Which charts depend on this dataset?
The namespace provides the identity required to perform those lookups while avoiding duplication of backend metadata.
5.8 Navigation Namespace
The navigation namespace provides routing-related context.
API
navigation.getPageType();
Events
navigation.onDidChangePage(...)
Contract
type PageType =
| "home"
| "dashboard"
| "explore"
| "sqllab"
| "dataset"
| "other";
The namespace answers a single question:
"Which application surface is currently active?"
It intentionally does not expose entity-specific information.
Entity context remains owned by the corresponding surface namespace.
Examples:
dashboard.getCurrentDashboard();
explore.getCurrentChart();
dataset.getCurrentDataset();
This separation preserves clear ownership boundaries and prevents duplication across namespaces.
5.9 Context Composition
This SIP intentionally does not introduce a host-owned aggregate context object.
Instead, extensions compose the context they require from individual namespaces.
For example:
const pageContext = {
pageType: navigation.getPageType(),
dashboard: dashboard.getCurrentDashboard(),
chart: explore.getCurrentChart(),
dataset: dataset.getCurrentDataset(),
sqlLab: sqlLab.getCurrentTab(),
};
The extension assembles a higher-level context tailored to its own requirements.
The host remains responsible for:
• Context ownership.
• Context normalization.
• Authorization alignment.
The extension remains responsible for:
• Context composition.
• Prompt construction.
• Application-specific interpretation.
This separation avoids introducing a centralized context abstraction while allowing new surfaces to be added incrementally over time.
5.10 Compatibility and Evolution
Namespace contracts are part of the public Superset extension API surface.
Breaking changes require standard compatibility and deprecation processes.
Extensions should depend only on documented namespace contracts and must not rely on implementation details behind those contracts.
As new application surfaces become extension-aware, additional namespaces may be introduced without affecting existing integrations.
This additive model allows the extension ecosystem to evolve while preserving backward compatibility.
6. Design Decisions
This section consolidates the key architectural decisions made by this SIP and summarizes the alternatives that were evaluated.
The goal is to capture the rationale behind the extension model so that future contributors can understand not only what was selected, but why alternative approaches were rejected.
6.1 Decision Summary
Decision
Topic
Selected Approach
D1
Page Context Model
Extension-composed context from host-provided namespaces
D2
Chatbot Resolution
Host-managed singleton resolution
D3
Descriptor Metadata
Static icon metadata
D4
Administration Scope
Deployment-wide administration
D5
Per-Page Visibility
Deferred - open question, see §8
D6
Generalized Floating Slots
Deferred - open question, see §8
6.2 D1 — Page Context Model
A central design question is how chatbot extensions obtain contextual information about the currently active application surface.
Three approaches were considered.
Option A — Host-Owned Aggregate Context
The host exposes a single API:
context.getPageContext();
which returns a fully assembled context object containing dashboard, chart, dataset, navigation, and SQL Lab information.
Rejected Because
• The host becomes responsible for understanding every application surface.
• The aggregate contract grows whenever a new surface is introduced.
• Changes in any surface can trigger unnecessary recomputation.
• The host becomes coupled to a single canonical context model.
• Ownership boundaries become unclear over time.
Option B — Surface Namespaces Composed by Extensions (Selected)
The host exposes independent namespaces:
• dashboard
• explore
• dataset
• navigation
• sqlLab
Extensions compose these primitives into their own application-specific context.
Advantages
• Clear ownership boundaries.
• Independent evolution of namespaces.
• Additive extensibility.
• Reduced coupling between surfaces.
• Extensions subscribe only to the context they require.
Option C — Route-Only Context
The host exposes only routing information.
Chatbot providers independently reconstruct context through APIs or backend services.
Rejected Because
This approach cannot reliably represent transient frontend state.
Examples include:
• Unsaved chart edits.
• Temporary dashboard filters.
• Active dashboard tabs.
• SQL editor state.
• Draft configuration changes.
As a result, chatbot context would frequently drift from what the user is actually viewing.
Decision
Option B is selected.
The host owns context normalization while extensions own context composition.
This preserves separation of concerns, minimizes coupling, and provides a stable foundation for future extension capabilities.
6.3 D2 — Singleton Chatbot Resolution
When multiple chatbot extensions are installed, the host must determine which chatbot is rendered.
This decision shapes both the rendering model and the extension isolation model.
Option A — Expose Providers Through getViews()
Allow:
getViews(location);
to return both metadata and rendering providers.
Rejected Because
Rendering providers are executable logic.
Exposing providers would allow one extension to:
• Render another extension.
• Bypass host lifecycle management.
• Circumvent fault isolation.
• Assume ownership of another extension's UI.
This violates a deliberate separation between extension discovery and extension execution.
Option B — Host-Managed Provider Resolution (Selected)
The host exposes only metadata publicly while retaining provider resolution internally.
Conceptually:
const provider = getViewProvider("superset.chatbot", selectedId);
Chatbot selection is handled through a host-managed policy:
const chatbot = getActiveChatbot(adminSelectedId, enabledMap);
Advantages
• Preserves extension isolation.
• Preserves host ownership of rendering.
• Supports administrative selection.
• Supports enablement checks.
• Supports future policy evolution.
Option C — Reuse resolveView()
Use the existing rendering helper:
resolveView(id);
to render chatbot providers.
Rejected Because
resolveView() assumes the caller already knows which view should be rendered.
It does not account for:
• Administrative selection.
• Enablement state.
• Settings synchronization.
• Chatbot-specific resolution policy.
Decision
Option B is selected.
The host owns chatbot selection and rendering.
The registry remains a discovery mechanism rather than a rendering mechanism.
Architectural Principle
A core principle established by this SIP is:
"Extensions may discover registered views, but only the host may render registered views."
This preserves extension isolation and prevents cross-extension rendering dependencies.
6.4 D3 — Descriptor Metadata Ownership
Chatbot registrations may include metadata used by administrative and discovery interfaces.
A key question is whether descriptor metadata should be static or runtime-updatable.
Option A — Static Descriptor Metadata (Selected)
Metadata is defined at registration time and remains unchanged for the lifetime of the registration.
Example:
{
id: 'acme.chatbot',
name: 'Acme Chatbot',
icon: 'Bubble',
}
Advantages
• Simpler registry implementation.
• Clear ownership model.
• Consistent administration UI.
• No registry update lifecycle.
Option B — Runtime-Updatable Metadata
Extensions can update descriptor metadata after registration.
Examples:
• Notification badges.
• Thinking indicators.
• Dynamic branding.
Rejected Because
These states belong to the chatbot user interface rather than the registration descriptor.
Supporting dynamic metadata would:
• Increase registry complexity.
• Introduce update synchronization concerns.
• Provide limited benefit for current consumers.
Decision
Option A is selected.
Descriptor metadata remains static.
Dynamic UI state remains the responsibility of the chatbot component.
Future requirements for dynamic metadata can be addressed independently if needed.
6.5 D4 — Administration Scope
This SIP introduces deployment-wide chatbot administration.
A key question is whether availability should be deployment-scoped or user-scoped.
Option A — Deployment-Wide Administration (Selected)
Administrators manage:
• Extension availability.
• Default chatbot selection.
These settings apply to the entire deployment.
Advantages
• Clear administrative ownership.
• Simple operational model.
• Consistent with existing extension administration patterns.
• Avoids introducing multiple configuration layers.
Option B — User-Scoped Availability
Availability and chatbot selection become user-specific settings.
Rejected Because
Administrative availability and user preference represent different concerns.
Administrators answer:
"Which integrations are available in this deployment?"
Users answer:
"Which available integrations do I prefer?"
Combining these concerns into a single model creates unclear ownership and duplicated configuration responsibilities.
Decision
Option A is selected.
This SIP introduces only deployment-wide administration.
Future user preferences should be layered on top using the following model:
Effective Availability = Deployment Availability AND User Preference
The recommended persistence mechanism for user-specific preferences is the Extension Storage API.
This approach aligns with SIP-127 and preserves a clear separation between administrative configuration and user customization.
7. Risks and Future Considerations
The selected architecture introduces several tradeoffs.
Namespace Maintenance
As additional application surfaces become extension-aware, new namespaces may be required.
This increases the maintenance burden of the extension API surface.
Contract Evolution
Namespace contracts are intended to be stable.
Over time, extensions may require additional context that is not initially exposed.
Future additions must preserve compatibility and avoid leaking implementation details.
Context Growth
Dashboard and chart context may become increasingly rich over time.
Care must be taken to ensure context APIs remain focused and do not evolve into large aggregate objects.
Extension Expectations
Chatbot vendors may request direct access to internal application state for convenience.
This SIP intentionally rejects that approach in favor of stable semantic contracts.
Maintaining that boundary may require additional namespace evolution over time. 8. Open Questions
D5 — Per-Page Visibility
Should chatbot extensions be able to declare page visibility constraints?
Two approaches remain possible.
Extension-Controlled Visibility
Extensions observe:
navigation.onDidChangePage(...)
and decide whether to render themselves.
Host-Enforced Visibility
Extensions declare supported page types through manifest metadata and the host enforces visibility.
Recommendation
Defer this decision.
The current architecture already supports extension-controlled visibility without requiring additional platform capabilities.
D6 — Generalized Floating Contribution Areas
The current proposal introduces a chatbot-specific contribution location:
superset.chatbot
A future question is whether this should evolve into a more generic floating-widget framework.
Examples might include:
• Chatbots.
• Guided tours.
• Notification centers.
• Productivity assistants.
Recommendation
Keep the contribution area chatbot-specific.
If broader floating-widget requirements emerge, introduce a dedicated abstraction rather than expanding the scope of this SIP.
9. Related Documents
Contribution types
Client actions
The following proposals are related to this SIP.
Extension Storage API
Add storage API for extensions (#39171)
Introduces namespace-isolated storage for extensions with support for:
• Local storage.
• Session storage.
• Ephemeral server storage.
• Persistent database-backed storage.
This proposal is complementary to the administration model defined by this SIP and is the recommended foundation for future user-specific extension preferences.
SIP-127 — User Preferences
[SIP-127] User Preferences (#28047)
Establishes the per-user preference model used by Superset core.
The Extension Storage API serves as the extension-scoped equivalent of this pattern and provides the recommended approach for future user-specific chatbot preferences. 10. Migration Plan
Base branch enxdev/chat-prototype
Branch for testing test/chatbot-local
The following capabilities are required to fully realize this SIP.
Core Platform Changes
Implemented
• superset.chatbot contribution location.
• Host-side chatbot resolution.
• Administration UI for chatbot selection.
• Dashboard namespace.
• Explore namespace.
• Navigation namespace.
• Runtime settings synchronization.
Pending
• Dataset namespace implementation.
• Dashboard chart visibility context.
• Permission-scoped dashboard context endpoint.
• Manifest support for application-level contribution scopes.
• Optional descriptor icon support.
11. Implementation Phases
Phase 1 — Chatbot Mount Point
• Chatbot contribution location.
• Host-side rendering.
• Lifecycle management.
• Fault isolation.
Status: Complete
Phase 2 — Administration
• Enable/disable support.
• Default chatbot selection.
• Runtime synchronization.
Status: Complete
Phase 3 — Context APIs
• Dashboard namespace.
• Explore namespace.
• Navigation namespace.
• Dataset namespace.
Status: Partially Complete
Remaining work:
• Dataset namespace.
• Dashboard chart visibility context.
• Dashboard context endpoint.
Phase 4 — Client Actions
Client actions and agentic UI interactions remain outside the scope of this SIP and are expected to be addressed through a separate proposal.

View File

@@ -55,6 +55,13 @@ WORKDIR /app/superset-frontend
RUN mkdir -p /app/superset/static/assets \
/app/superset/translations
# Harden `npm ci` against transient npm-registry network blips (e.g. ECONNRESET),
# which otherwise fail the entire multi-platform image build with no retry.
ENV npm_config_fetch_retries=5 \
npm_config_fetch_retry_mintimeout=20000 \
npm_config_fetch_retry_maxtimeout=120000 \
npm_config_fetch_timeout=600000
# Mount package files and install dependencies if not in dev mode
# NOTE: we mount packages and plugins as they are referenced in package.json as workspaces
# ideally we'd COPY only their package.json. Here npm ci will be cached as long

View File

@@ -189,6 +189,11 @@ Try out Superset's [quickstart](https://superset.apache.org/docs/quickstart/) gu
- [Join our community's Slack](http://bit.ly/join-superset-slack)
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org). To join, simply send an email to [dev-subscribe@superset.apache.org](mailto:dev-subscribe@superset.apache.org)
- Follow us on social media:
[X](https://x.com/apachesuperset) |
[LinkedIn](https://www.linkedin.com/company/apache-superset) |
[Bluesky](https://bsky.app/profile/apachesuperset.bsky.social) |
[Reddit](https://reddit.com/r/apache-superset)
- If you want to help troubleshoot GitHub Issues involving the numerous database drivers that Superset supports, please consider adding your name and the databases you have access to on the [Superset Database Familiarity Rolodex](https://docs.google.com/spreadsheets/d/1U1qxiLvOX0kBTUGME1AHHi6Ywel6ECF8xk_Qy-V9R8c/edit#gid=0)
- Join Superset's Town Hall and [Operational Model](https://preset.io/blog/the-superset-operational-model-wants-you/) recurring meetings. Meeting info is available on the [Superset Community Calendar](https://superset.apache.org/community)

View File

@@ -109,7 +109,7 @@ If yes, it is in scope. If no, it is out of scope. The lists below apply that te
- Any action an Admin role can perform through documented configuration, API, or UI. The Admin role is a trusted operational principal by policy. Per MITRE CNA Operational Rules 4.1, a qualifying vulnerability must violate a security policy; behavior within a documented trust boundary does not.
- Deployment or operator decisions: the values of secrets and tokens, whether internal networks are reachable from the server, which database connectors or cache backends are enabled, which feature flags are set, where notifications are delivered, and which third-party plugins are loaded.
- Compromise, modification, or malicious control of trusted backend infrastructure. Apache Superset assumes the integrity of its metastore, cache backends (for example Redis or Memcached), message brokers, secret stores, and other operator-managed infrastructure. Findings that require an attacker to read from, write to, or otherwise tamper with these systems, including injecting malicious state, serialized objects, cache entries, task metadata, configuration, or database records, are post-compromise scenarios and do not constitute vulnerabilities in Apache Superset itself. A finding remains in scope only if an unprivileged user can cause such modification through a vulnerability in Apache Superset.
- Code paths whose intended purpose is example data, demos, fixtures, local development, or documentation, rather than the production runtime.
- The continued presence of expired key-value or metastore-cache entries that have not yet been deleted from the metadata database. Such entries are excluded from reads once expired, are purged opportunistically on write, and are removed in bulk by the scheduled `prune_key_value` maintenance task; their lingering until purged is an eventual-cleanup property, not a security boundary, and does not constitute a vulnerability.
- How a downstream application (spreadsheet program, email client, browser handling user-downloaded files) interprets output Apache Superset produced for it.
- Findings without a reproducible proof of concept against a supported release. The burden of demonstrating exploitability rests with the reporter; findings closed for lack of a proof of concept may be refiled if one is later produced.
- Brute force, rate limiting, denial of service, or resource exhaustion that does not bypass a documented control.

View File

@@ -1,322 +0,0 @@
# Chatbot Extensions — Tickets
Lightweight, pre-implementation tickets. Each says what to build and where the
boundaries are; it does not prescribe the final code. Stack order (bottom → top):
contribution point → frontend API mount → eager loading → admin UI → backend
settings/permissions → context sharing → import/delete.
---
## 1. Define the contribution point
**Goal:** Introduce the `superset.chatbot` contribution area and the host plumbing
needed to mount a single chatbot at the application-shell level, persistent across
routes. This is the keystone everything else builds on.
**Build:**
- Register `superset.chatbot` as a recognized contribution location in the view
registry.
- Add an app-shell / app-root contribution scope to the extension manifest schema
so the location can be declared in `extension.json` (the current schema is
SQL-Lab-only). Teach both manifest validation and runtime registration about it.
- Provide an exclusive-location resolver that selects exactly one renderable
chatbot for the slot, with a deterministic first-to-register fallback and a seam
for an externally supplied "active chatbot id" (so admin/runtime policy can plug
in later without touching the resolver).
- Host-managed mount layout: fixed bottom-right, 24px margin, z-index above content
and toasts, below modals.
**Out of scope:** fault isolation, admin selection UI, the lifecycle/teardown
contract, eager loading, streaming, context namespaces, authoring docs.
**Depends on:** nothing — this unblocks the rest.
**Done when:** an extension can register at `superset.chatbot` and render at the
app shell across routes; the resolver returns one provider (admin-id seam +
first-to-register fallback); unregistering removes the mount cleanly with no
duplicate bubbles.
Base branch: `enxdev/chat-prototype`
**External Links:** https://github.com/apache/superset/pull/40439
---
## 2. Host resolution & mount (frontend API entry point)
**Goal:** Turn a registered `superset.chatbot` view into a rendered, fault-isolated
bubble — the host-internal provider accessor, the selection policy, and the
fixed-position mount.
**Build:**
- Host-internal accessors on the views registry: `getViewProvider(location, id)`
and `getRegisteredViewIds(location)`. Keep the public `getViews` descriptor-only —
do not expose providers on the public surface.
- A registry change subscription so a mount can re-resolve without polling (fired
on register/unregister).
- The `getActiveChatbot(adminSelectedId?, enabledMap?)` resolver implementing the
selection policy: empty → none; drop disabled ids; admin-selected-and-enabled
wins; else first enabled in registration order.
- A `ChatbotMount` component at the app shell that renders the active provider
inside the host `ErrorBoundary`, re-resolves on registry change, and renders
nothing when no chatbot is active.
**Out of scope:** the contribution location itself (#40439); eager-loading the
bundle (#40441); the settings endpoint (#40443, consumed here with silent
fallback); admin UI; the lifecycle/teardown contract.
**Depends on:** #40439 (imports `CHATBOT_LOCATION`). The settings endpoint is a soft
forward-dependency — the mount falls back to first-registered-enabled if it 404s.
**Done when:** the provider accessor and resolver behave per the policy; the mount
renders/clears correctly and survives a throwing provider via `ErrorBoundary`;
`getViews` stays descriptor-only.
Base branch: `enxdev/feat/chatbot-contribution-point` (on #40439)
**External Links:** https://github.com/apache/superset/pull/40440
---
## 3. Eager loading & extension lifecycle/teardown
> Merged: this ticket also covers the **lifecycle & teardown contract** — both are
> implemented in the same PR (#40441), so they are tracked together.
**Goal:** Boot extension bundles at app-shell startup so contributions register
before the first route, and define the host contract for tearing those
contributions down on uninstall.
**Build — eager loading:**
- An `ExtensionsStartup` component that, once the session is confirmed and behind
`FeatureFlag.EnableExtensions`, kicks off `initializeExtensions()` in the
background. The host renders immediately; the mount re-resolves reactively when
registrations land.
- Wire `window.superset` so Module-Federation remotes can consume host namespaces.
- Mount `<ChatbotMount />` as a sibling of the route switch, inside
`ExtensionsStartup`.
- On bundle-load failure: a danger toast, host stays interactive, corner stays
empty. Add a global `unhandledrejection` logger (log only; do not suppress the
browser default).
**Build — lifecycle/teardown contract (Model A1, per-contribution dispose):**
- During the `./index` factory call, intercept the public registrars and collect
the returned `Disposable`s keyed by extension id.
- `deactivateExtension(id)` is the single teardown entrypoint: it fires every
collected `Disposable` and removes the extension from the index. A throwing
`Disposable` must not block the others (catch per-disposable). Idempotent;
unknown id is a no-op.
- Trigger semantics to document: **uninstall**`deactivateExtension(id)`;
**disable** → mount filters by `enabledMap`, does NOT fire disposables, re-enable
needs no reload; **replace** (singleton) → resolver re-selects, the losing
extension is not deactivated. Disposal order is best-effort (registration order),
not a contract — consumers must be order-independent.
**Out of scope:** selective per-type eager loading (not feasible without running the
factory); the mount-boundary `ErrorBoundary` (#40440); the settings endpoint and
its subscription primitive (#40443); context namespaces (#40444); an async-aware
`deactivate(): Promise<void>` — file separately only if a graceful-flush
requirement appears.
**Depends on:** #40440 (`ChatbotMount`, resolver, registry-subscription hook). Soft
build-time deps on #40443 (settings subscription) and #40444 (namespaces) — land
those first or stub the imports.
**Done when:** enabled extensions init once at startup behind the flag without
gating initial render; the bubble appears reactively on registration;
`deactivateExtension(id)` disposes all of an extension's contributions (per-disposable
catch, idempotent); load failure toasts without throwing; teardown is verified
end-to-end on the reference chatbot (including an abort-registry controller that must
still abort on deactivate).
Base branch: `enxdev/feat/chatbot-frontend-api` (on #40440)
**External Links:** https://github.com/apache/superset/pull/40441
---
## 4. Admin configuration UI
**Goal:** Let an admin enable/disable the chatbot and, when more than one chatbot is
installed, choose which is active.
**Resolve before building:**
- Is "disable the chatbot" the existing generic extension-disable, or a
chatbot-specific toggle? (Determines the ticket's size — prefer reusing the
existing flag.)
- Where does the UI live? Default: the existing extensions management surface, not a
new page.
- How does the "default chatbot" selection persist? Reuse existing extension-state
storage or a config value — do not invent a table.
- Which permission gates it? Default: the existing Extensions-API write permission.
**Build:**
- Enable/disable control that empties the `superset.chatbot` slot when off (no broken
placeholder).
- (Gated on the singleton-policy decision) A selection control listing candidates via
`getViews('superset.chatbot')`, activating the choice through the resolver, falling
back to first-to-register when unset.
- Switching the active chatbot or disabling it at runtime must dispose the previously
active chatbot via its `Disposable` and release its in-flight stream readers (via
`AbortController`) — no two bubbles, no leaked readers.
**Out of scope:** the singleton-policy decision itself; per-page visibility; the
resolver implementation (consumed here).
**Depends on:** #40440 (resolver) for selection; enable/disable does not wait on the
policy decision.
**Done when:** admin can enable/disable (gated by the chosen permission) and the slot
empties when off; selection picks the active chatbot with first-to-register fallback;
the persistence-mechanism and permission decisions are recorded in the ticket.
Base branch: `enxdev/chat-prototype`
**External Links:** https://github.com/apache/superset/pull/40442
---
## 5. Permissions
**Goal:** Guarantee the new page-context surface cannot expose anything the current
user can't already access through Superset's standard security model. Chatbot
extensions fetch data as any other frontend surface and inherit only the current
user's privileges; this ticket covers only the new host → extension context-sharing
path.
**Build:**
- The page-context namespaces (#40444) must derive entity metadata from the same
permission checks that gate the underlying page — not a raw Redux pass-through.
- Canonical threat (SIP §2.1): a dashboard the user can view that contains a chart
whose dataset they cannot query — that chart's metadata (id, name, datasource,
viz type, form_data) must be dropped from the context payload.
- Context carries only lightweight semantic data + identifiers that resolve through
already-protected APIs; never inline dataset rows or query results.
- Filtering applies equally to the initial read and every change-notification
payload. A chatbot an admin has disabled receives no context at all.
**Out of scope:** REST API authorization, RBAC, RLS (already enforced by Superset);
LLM/backend auth; the singleton selection policy. The chatbot authenticates via the
user's existing session (cookie + CSRF) — no separate credential is issued.
**Depends on:** the Spike sizing the new namespaces (the per-getter filtering lands
with those getters); the Context-sharing ticket consumes the filtered getters this
one specifies.
**Done when:** context never exposes entities/ids/metadata the user can't access
(even via a manually-entered URL); the dashboard payload omits charts whose dataset
the user can't query; no inline privileged payloads; filtering covers change events
as well as the initial read; a disabled chatbot gets nothing.
Base branch: `enxdev/chat-prototype`
**External Links:** https://github.com/apache/superset/pull/40443
---
## 6. Context sharing
**Goal:** Let the chatbot read semantic page context and subscribe to changes through
public per-surface core namespaces only — never the host Redux store.
**Approach:** Deliver context through per-surface namespaces — the existing `sqlLab`
namespace plus new `dashboard` / `explore` / `navigation` namespaces that mirror its
shape (a state getter + an `Event<T>` change subscription). No new aggregate context
API. The new namespaces copy `sqlLab`'s shape but must filter the Redux state they
read (the permission filtering itself is specified by #40443).
**Build:**
- Route all chatbot page-context reads through one narrow adapter module with a fixed
interface — the adapter is the deliverable, not scattered call sites — so swapping
to core namespaces is a one-line change.
- Back the adapter with `sqlLab` immediately; back the dashboard/explore/navigation
portions and wire change notifications through `navigation`'s page-change event once
those namespaces ship.
**Out of scope:** the permission-filtering logic (#40443 + upstream namespace work);
designing the namespace API surface (upstream OSS work, sized by the Spike).
**Depends on:** a Spike to size the new namespaces (state getters + events + the
per-getter permission filtering). The namespace _shape_ is settled by the `sqlLab`
precedent; the filtering is real design work.
**Done when:** all context reads go through the single adapter (zero direct Redux
imports, greppable); SQL Lab context works today; dashboard/explore context is either
delivered or explicitly tracked as OSS-blocked (not faked); change notifications need
no polling; no extra host re-renders.
Base branch: `enxdev/chat-prototype`
**External Links:** https://github.com/apache/superset/pull/40444
---
## 7. Import / delete UI
**Goal:** Add an actions column to the extensions list with buttons to delete an
extension, set-as-default (chatbot extensions only), and import a new extension.
**Build:**
- Import an extension bundle, refreshing the list on success.
- Delete an installed extension.
- A "set as default chatbot" control, shown only for chatbot extensions.
**Out of scope:** the settings endpoint itself (#40443); the resolver (#40440).
**Depends on:** #40442/#40443 for the settings + chatbot-selection plumbing.
**Done when:** an admin can import, delete, and set a default chatbot from the
actions column, with the list reflecting changes.
Base branch: `enxdev/chat-prototype`
**External Links:** https://github.com/apache/superset/pull/40450
---
## ~~8. Fault isolation & error boundaries~~ — CLOSED (no ticket needed)
The protective fault-isolation mechanisms are **already implemented** across the
mount and eager-loading PRs, so no standalone ticket is required:
- Render/lifecycle throw → host `ErrorBoundary` around the `superset.chatbot` slot
(#40440, reinforced by the `ChatbotRenderer` wrapper in #40441).
- Bundle-load failure → `.catch()` + danger toast in `ExtensionsLoader` (#40441).
- `activate()` throw → host try/catch in `ExtensionsLoader` (#40441).
- Escaped async rejection → `unhandledrejection` hook in `ExtensionsStartup` (#40441).
- Failed-activation cleanup → driven by `deactivateExtension` (ticket 3 / #40441).
The host stays safe under every failure class today. The only unbuilt pieces were the
**optional** "chatbot failed — Reload page" notification and structured
failure-class/telemetry logging — both judged not worth a ticket (the original spec
itself marked the reload notification "optional"). File a fresh ticket only if that
UX is later wanted.
(Original link, for reference only: PR #40433 `feat(extensions): adds chatbot P1-P2`
closed/superseded; never a dedicated fault-isolation PR.)
---
## Notes on consolidation
- **Lifecycle/teardown** was a separate ticket pointing at the same PR as **Eager
loading** (#40441) — merged into ticket 3 above. (This is the only true duplicate.)
- The **Permissions** ticket (#40443) is kept as-is. Note its PR also contains
backend settings-persistence code, but the original ticket only ever scoped the
permission-safe context surface — so the ticket stays "Permissions" and no
persistence ticket is invented.
- The **Permissions** ticket previously had a truncated base branch
(`enxdev/chat-protot`) — corrected to `enxdev/chat-prototype`.
- **Fault isolation** was **closed without a ticket** (see the struck-through section
above): its protective mechanisms already shipped in #40440/#40441, and the only
unbuilt pieces (the optional "Reload page" notification + structured telemetry
logging) were judged not worth a ticket.

View File

@@ -24,12 +24,71 @@ assists people when migrating to a new version.
## Next
### Duration formatter precision
The `DURATION` number formatter now uses `Intl.DurationFormat` for locale-aware output. By default, sub-second fields are omitted, so values that previously displayed fractional seconds with `pretty-ms`, such as `10500` milliseconds rendering as `10.5s`, now render as `10s`.
To preserve sub-second precision in custom duration formatters, enable `formatSubMilliseconds`.
### Cache warmup authenticates via SUPERSET_CACHE_WARMUP_USER
The `cache-warmup` Celery task now drives a real WebDriver session for reliable authentication and reads the user to authenticate as from the new `SUPERSET_CACHE_WARMUP_USER` config option. It no longer consults `CACHE_WARMUP_EXECUTORS` for the warmup path. `SUPERSET_CACHE_WARMUP_USER` defaults to `None`, so the task fails fast with a clear message until you set it. Operators who previously relied on `CACHE_WARMUP_EXECUTORS` for cache warmup must set `SUPERSET_CACHE_WARMUP_USER` to a dedicated least-privilege user with access to the dashboards they want warmed up before the next warmup run.
### YDB now uses a native sqlglot dialect
YDB SQL parsing now relies on the dedicated [`ydb-sqlglot-plugin`](https://pypi.org/project/ydb-sqlglot-plugin/) dialect, which registers itself with sqlglot automatically. YDB users must install this plugin (e.g., via `pip install "apache-superset[ydb]"`) to avoid a `ValueError` when Superset parses YDB queries.
### Embedded dashboards enforce configured Allowed Domains for postMessage
The embedded dashboard page now validates the origin of incoming `postMessage` events against the dashboard's configured **Allowed Domains**. The server-rendered embedded page exposes the configured domains in its bootstrap payload, and the frontend rejects message events whose origin is not in that list.
Enforcement only applies when the Allowed Domains list is non-empty. If the list is empty (the default), any origin is accepted, so there is no behavior change for embeds that did not configure Allowed Domains.
### Default guest/async JWT secrets are rejected at startup
Superset already refuses to start in production (non-debug, non-testing) when `SECRET_KEY` is left at its built-in default, and when `GUEST_TOKEN_JWT_SECRET` is left at its default while `EMBEDDED_SUPERSET` is enabled. This behavior is extended to `GLOBAL_ASYNC_QUERIES_JWT_SECRET`: if the `GLOBAL_ASYNC_QUERIES` feature flag is enabled and the secret is still the publicly known default (`test-secret-change-me`), Superset logs a clear error and refuses to start.
As with the existing `SECRET_KEY` check, this only fails in production. In debug mode, testing mode, or under the test runner, a warning is logged instead of exiting, so local development is unaffected.
To resolve the error, set a strong random value in `superset_config.py`:
```python
GLOBAL_ASYNC_QUERIES_JWT_SECRET = "<output of: openssl rand -base64 42>"
```
The check is only active when the relevant feature is enabled, so deployments that do not use global async queries (or embedding) are not affected.
### Guest token revocation (opt-in)
Embedded guest tokens can be coarsely revoked at runtime via a new opt-in mechanism. A new config flag `GUEST_TOKEN_REVOCATION_ENABLED` (default `False`) gates the feature. When enabled, every minted guest token carries a revocation version, and tokens whose version is below the current expected version (stored in the metadata database) are rejected at validation time.
Bump the expected version with the new CLI command to invalidate all outstanding guest tokens:
```bash
superset revoke-guest-tokens
```
This change is backward compatible. The feature is off by default, and even when enabled nothing is revoked until an admin explicitly bumps the version: the expected version starts at `0`, and tokens minted before this change (which carry no version claim) are treated as version `0`. No database migration is required.
### Dataset import validates catalog against the target connection
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
If you relied on importing datasets with a non-default catalog, enable "Allow changing catalogs" on the target connection, or set the dataset's catalog to the connection's default before importing.
### Extension supply-chain controls (denylist + version policy)
Two opt-in static gates control which extensions are allowed to load:
- `EXTENSION_DENYLIST` refuses extensions matching an id (every version) or `id@version` (a single version), e.g. `["compromised-extension", "other-ext@1.2.3"]`.
- `EXTENSION_VERSION_POLICY` enforces a minimum version per extension id, e.g. `{"acme.widget": "1.2.0"}` (PEP 440 comparison); a release below the minimum is refused.
Both default to empty (no behavior change). They apply to both the `LOCAL_EXTENSIONS` and `EXTENSIONS_PATH` load paths.
### Dynamic Group By respects the sort toggle for display values
The Dynamic Group By chart customization now orders its display values according to the "Sort display control values" toggle: ascending (AZ), descending (ZA), or the dataset's source order when the toggle is unset. Previously the dropdown always sorted alphabetically. Existing dashboards where the toggle was never set will show options in source order instead of AZ; open the customization and enable the toggle to restore alphabetical ordering.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:

View File

@@ -61,6 +61,31 @@ services:
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/templates:/etc/nginx/templates:ro
# Wait for the webpack dev server's manifest.json to be served before
# starting nginx. This prevents 404s on static assets at startup. The
# probe targets host.docker.internal so it works regardless of whether
# the dev server runs in the superset-node container
# (BUILD_SUPERSET_FRONTEND_IN_DOCKER=true, the default) or directly on
# the host (BUILD_SUPERSET_FRONTEND_IN_DOCKER=false).
command:
- /bin/bash
- -c
- |
url="http://host.docker.internal:9000/static/assets/manifest.json"
max_attempts=150 # ~5 minutes at 2s intervals
echo "Waiting for webpack dev server at $url..."
attempt=0
until curl -sf --max-time 5 -o /dev/null "$url"; do
attempt=$((attempt + 1))
if [ "$attempt" -ge "$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
exit 1
fi
sleep 2
done
echo "Webpack dev server is ready; starting nginx."
exec nginx -g 'daemon off;'
redis:
image: redis:7

View File

@@ -80,7 +80,23 @@ case "${1}" in
;;
app)
echo "Starting web app (using development server)..."
flask run -p $PORT --reload --debugger --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
# Environment-based debugger control for security
# Only enable Werkzeug interactive debugger when explicitly requested
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
export FLASK_DEBUG=1
DEBUGGER_FLAG="--debugger"
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
else
export FLASK_DEBUG=0
DEBUGGER_FLAG="--no-debugger"
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
;;
app-gunicorn)
echo "Starting web app..."

View File

@@ -86,6 +86,39 @@ instead requires a cachelib object.
See [Async Queries via Celery](/admin-docs/configuration/async-queries-celery) for details.
## Celery beat
Superset has a Celery task that will periodically warm up the cache based on different strategies.
To use it, add the following to your `superset_config.py`:
```python
from celery.schedules import crontab
from superset.config import CeleryConfig
# User that will be used to authenticate and render dashboards for cache warmup
SUPERSET_CACHE_WARMUP_USER = "user_with_permission_to_dashboards"
# Extend the default CeleryConfig to add cache warmup schedule
class CustomCeleryConfig(CeleryConfig):
beat_schedule = {
**CeleryConfig.beat_schedule,
'cache-warmup-hourly': {
'task': 'cache-warmup',
'schedule': crontab(minute=0, hour='*'), # hourly
'kwargs': {
'strategy_name': 'top_n_dashboards',
'top_n': 5,
'since': '7 days ago',
},
},
}
CELERY_CONFIG = CustomCeleryConfig
```
This will cache the top 5 most popular dashboards every hour. For other
strategies, check the `superset/tasks/cache.py` file.
## Caching Thumbnails
This is an optional feature that can be turned on by activating its [feature flag](/admin-docs/configuration/configuring-superset#feature-flags) on config:

View File

@@ -157,8 +157,15 @@ superset load_examples
superset init
# To start a development web server on port 8088, use -p to bind to another port
superset run -p 8088 --with-threads --reload --debugger
superset run -p 8088 --with-threads --reload
# For debugging with interactive console (⚠️ localhost only)
# superset run -p 8088 --with-threads --reload --debugger
```
:::warning Security Note
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
:::
If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
locally by default at `localhost:8088`) and login using the username and password you created.

View File

@@ -157,8 +157,15 @@ superset load_examples
superset init
# To start a development web server on port 8088, use -p to bind to another port
superset run -p 8088 --with-threads --reload --debugger
superset run -p 8088 --with-threads --reload
# For debugging with interactive console (⚠️ localhost only)
# superset run -p 8088 --with-threads --reload --debugger
```
:::warning Security Note
The `--debugger` flag enables Werkzeug's interactive console at `/console`. Only use this for local development and never bind to `0.0.0.0` or expose the server to networks when debugging is enabled.
:::
If everything worked, you should be able to navigate to `hostname:port` in your browser (e.g.
locally by default at `localhost:8088`) and login using the username and password you created.

View File

@@ -102,6 +102,8 @@ Affecting the Docker build process:
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
- **SUPERSET_LOG_LEVEL (default=info)**: Can be set to debug, info, warning, error, critical
for more verbose logging
- **SUPERSET_DEBUG_ENABLED (default=false)**: Enable Werkzeug debugger with interactive console.
Set to `true` for debugging: `SUPERSET_DEBUG_ENABLED=true docker compose up`
For more env vars that affect your configuration, see this
[superset_config.py](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py)

View File

@@ -917,6 +917,23 @@ const config: Config = {
footer: {
links: [],
copyright: `
<div class="footer__social-links">
<a href="https://bit.ly/join-superset-slack" target="_blank" rel="noopener noreferrer" title="Join us on Slack" aria-label="Slack">
<img src="/img/community/slack-symbol.svg" alt="Slack" />
</a>
<a href="https://x.com/apachesuperset" target="_blank" rel="noopener noreferrer" title="Follow us on X" aria-label="X">
<img src="/img/community/x-symbol.svg" alt="X" />
</a>
<a href="https://www.linkedin.com/company/apache-superset" target="_blank" rel="noopener noreferrer" title="Follow us on LinkedIn" aria-label="LinkedIn">
<img src="/img/community/linkedin-symbol.svg" alt="LinkedIn" />
</a>
<a href="https://bsky.app/profile/apachesuperset.bsky.social" target="_blank" rel="noopener noreferrer" title="Follow us on Bluesky" aria-label="Bluesky">
<img src="/img/community/bluesky-symbol.svg" alt="Bluesky" />
</a>
<a href="https://reddit.com/r/apache-superset" target="_blank" rel="noopener noreferrer" title="Follow us on Reddit" aria-label="Reddit">
<img src="/img/community/reddit-symbol.svg" alt="Reddit" />
</a>
</div>
<div class="footer__ci-services">
<span>CI powered by</span>
<a href="https://www.netlify.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/netlify.png" alt="Netlify" title="Netlify - Deploy Previews" /></a>

View File

@@ -1,6 +1,6 @@
{
"copyright": {
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a>&nbsp;|&nbsp;\n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
"message": "\n <div class=\"footer__social-links\">\n <a href=\"https://bit.ly/join-superset-slack\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Join us on Slack\" aria-label=\"Slack\">\n <img src=\"/img/community/slack-symbol.svg\" alt=\"Slack\" />\n </a>\n <a href=\"https://x.com/apachesuperset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on X\" aria-label=\"X\">\n <img src=\"/img/community/x-symbol.svg\" alt=\"X\" />\n </a>\n <a href=\"https://www.linkedin.com/company/apache-superset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on LinkedIn\" aria-label=\"LinkedIn\">\n <img src=\"/img/community/linkedin-symbol.svg\" alt=\"LinkedIn\" />\n </a>\n <a href=\"https://bsky.app/profile/apachesuperset.bsky.social\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on Bluesky\" aria-label=\"Bluesky\">\n <img src=\"/img/community/bluesky-symbol.svg\" alt=\"Bluesky\" />\n </a>\n <a href=\"https://reddit.com/r/apache-superset\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Follow us on Reddit\" aria-label=\"Reddit\">\n <img src=\"/img/community/reddit-symbol.svg\" alt=\"Reddit\" />\n </a>\n </div>\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/admin-docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a>&nbsp;|&nbsp;\n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
"description": "The footer copyright"
}
}

View File

@@ -43,7 +43,7 @@
"version:remove:components": "node scripts/manage-versions.mjs remove components"
},
"dependencies": {
"@ant-design/icons": "^6.2.3",
"@ant-design/icons": "^6.2.5",
"@docusaurus/core": "^3.10.1",
"@docusaurus/faster": "^3.10.1",
"@docusaurus/plugin-client-redirects": "^3.10.1",
@@ -72,11 +72,11 @@
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.40",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.32",
"baseline-browser-mapping": "^2.10.33",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.1.1",
"js-yaml": "^4.2.0",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
@@ -101,15 +101,15 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.60.0",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.60.0",
"typescript-eslint": "^8.60.1",
"webpack": "^5.107.2"
},
"browserslist": {

View File

@@ -260,10 +260,45 @@ a > span > svg {
.footer {
position: relative;
padding-top: 90px;
padding-top: 130px;
font-size: 15px;
}
.footer__social-links {
background-color: #173036;
position: absolute;
top: 52px;
left: 0;
width: 100%;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.footer__social-links a {
display: inline-flex;
align-items: center;
transition: opacity 0.2s, transform 0.2s;
}
.footer__social-links a:hover {
opacity: 0.8;
transform: scale(1.1);
}
.footer__social-links img {
height: 24px;
width: 24px;
/* The brand SVGs ship in their native colors (e.g. Slack's dark aubergine,
X's near-black), which disappear on the dark footer. Render them all as
uniform white silhouettes. The icons are single-path glyphs whose
counters (the LinkedIn "in", Slack gaps, Reddit face) are transparent
cut-outs, so they stay legible against the footer background. */
filter: brightness(0) invert(1);
}
.footer__ci-services {
background-color: #0d3e49;
color: #e1e1e1;
@@ -309,6 +344,21 @@ a > span > svg {
}
@media only screen and (max-width: 996px) {
.footer {
padding-top: 120px;
}
.footer__social-links {
top: 44px;
gap: 20px;
padding: 8px 16px;
}
.footer__social-links img {
height: 20px;
width: 20px;
}
.footer__ci-services {
gap: 12px;
padding: 10px 16px;

View File

@@ -0,0 +1,21 @@
<!--
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.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#FF4500">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12c-.688 0-1.25.561-1.25 1.25 0 .687.562 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,21 @@
<!--
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.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#4A154B">
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.124 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.52 2.521h-2.522V8.834zm-1.271 0a2.528 2.528 0 0 1-2.521 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.166 0a2.528 2.528 0 0 1 2.521 2.522v6.312zm-2.521 10.124a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.166 24a2.528 2.528 0 0 1-2.521-2.52v-2.522h2.521zm0-1.271a2.528 2.528 0 0 1-2.521-2.521 2.528 2.528 0 0 1 2.521-2.521h6.312A2.528 2.528 0 0 1 24 15.165a2.528 2.528 0 0 1-2.52 2.521h-6.313z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -212,14 +212,14 @@
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.2.3":
version "6.2.3"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
"@ant-design/icons@^6.2.3", "@ant-design/icons@^6.2.5":
version "6.2.5"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.5.tgz#31c142aa6ce5eaf99598aaead222f4c459693512"
integrity sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/icons-svg" "^4.4.2"
"@rc-component/util" "^1.10.1"
"@rc-component/util" "^1.11.0"
clsx "^2.1.1"
"@ant-design/react-slick@~2.0.0":
@@ -3021,10 +3021,10 @@
os-homedir "^1.0.1"
regexpu-core "^4.5.4"
"@pkgr/core@^0.2.9":
version "0.2.9"
resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz"
integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
"@pkgr/core@^0.3.6":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.3.6.tgz#3569708bd4be4d8870ba32bf1c456dac81600d97"
integrity sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==
"@pnpm/config.env-replace@^1.1.0":
version "1.1.0"
@@ -4812,110 +4812,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/type-utils" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/type-utils" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.60.0", "@typescript-eslint/parser@^8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
dependencies:
"@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
"@typescript-eslint/project-service@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.60.0"
"@typescript-eslint/types" "^8.60.0"
"@typescript-eslint/tsconfig-utils" "^8.60.1"
"@typescript-eslint/types" "^8.60.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
"@typescript-eslint/scope-manager@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
dependencies:
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/tsconfig-utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz#3af78c48956227a407dea9626b8db8ca53f130d2"
integrity sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==
"@typescript-eslint/tsconfig-utils@^8.60.0":
"@typescript-eslint/tsconfig-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/type-utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
"@typescript-eslint/tsconfig-utils@^8.60.1":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
"@typescript-eslint/type-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
dependencies:
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.0.tgz#e77ad768e933263b1960b2fe79de75fe1cc6e7db"
integrity sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==
"@typescript-eslint/types@^8.60.0":
"@typescript-eslint/types@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
"@typescript-eslint/typescript-estree@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
"@typescript-eslint/types@^8.60.1":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
"@typescript-eslint/typescript-estree@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
dependencies:
"@typescript-eslint/project-service" "8.60.0"
"@typescript-eslint/tsconfig-utils" "8.60.0"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/project-service" "8.60.1"
"@typescript-eslint/tsconfig-utils" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
"@typescript-eslint/utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
"@typescript-eslint/visitor-keys@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
dependencies:
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/types" "8.60.1"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -5578,10 +5578,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.32, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.32"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz#b6b553a4285fdd606327a617de36a5351e3aaa64"
integrity sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==
baseline-browser-mapping@^2.10.33, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.33"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz#27c299b096404978831958d429f48390424c4f9b"
integrity sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==
batch@0.6.1:
version "0.6.1"
@@ -7522,13 +7522,13 @@ eslint-config-prettier@^10.1.8:
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz"
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
eslint-plugin-prettier@^5.5.5:
version "5.5.5"
resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz"
integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==
eslint-plugin-prettier@^5.5.6:
version "5.5.6"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz#363ebe4d769bce157ccdd8129ce3efd91dc62564"
integrity sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==
dependencies:
prettier-linter-helpers "^1.0.1"
synckit "^0.11.12"
synckit "^0.11.13"
eslint-plugin-react@^7.37.5:
version "7.37.5"
@@ -9341,7 +9341,7 @@ js-yaml@4.1.0:
dependencies:
argparse "^2.0.1"
js-yaml@=4.1.1, js-yaml@^4.1.0, js-yaml@^4.1.1:
js-yaml@=4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -9356,6 +9356,13 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0, js-yaml@^4.1.1, js-yaml@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524"
integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==
dependencies:
argparse "^2.0.1"
jsdoc-type-pratt-parser@^4.0.0:
version "4.8.0"
resolved "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz"
@@ -13483,9 +13490,9 @@ shebang-regex@^3.0.0:
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.8.3:
version "1.8.3"
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz"
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
version "1.8.4"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==
shelljs@0.8.5:
version "0.8.5"
@@ -14096,12 +14103,12 @@ swc-loader@^0.2.6, swc-loader@^0.2.7:
dependencies:
"@swc/counter" "^0.1.3"
synckit@^0.11.12:
version "0.11.12"
resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz"
integrity sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==
synckit@^0.11.13:
version "0.11.13"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.13.tgz#062a5ea57d81befc35892f8254de5c567e97c80a"
integrity sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==
dependencies:
"@pkgr/core" "^0.2.9"
"@pkgr/core" "^0.3.6"
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3:
version "2.3.3"
@@ -14382,15 +14389,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.60.0:
version "8.60.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
typescript-eslint@^8.60.1:
version "8.60.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640"
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.60.0"
"@typescript-eslint/parser" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
"@typescript-eslint/eslint-plugin" "8.60.1"
"@typescript-eslint/parser" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
typescript@~6.0.3:
version "6.0.3"

View File

@@ -1,138 +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.
-->
# Reference Chatbot Extension
Canonical environment-validation extension for the `superset.chatbot`
contribution area. **Not** a product chatbot — there is no LLM, no backend,
no persistence. Its purpose is to exercise the extension platform end-to-end:
- `views.registerView` at `superset.chatbot` (singleton resolution)
- Lifecycle activation + a master disposable that tears down everything
- `commands.registerCommand` for `core.chatbot__open|close|toggle`
- Mock streaming with `AbortController` cancellation on dispose
- Defense-in-depth React error boundary inside the panel
- A single P3 page-context seam that lights up automatically as the
`dashboard` / `explore` / `dataset` / `navigation` namespaces become
available at runtime on the host
It is intended as the reference implementation third-party chatbot extension
authors copy. Anything that ships as host-internal (the mount point, the
admin picker, the `getActiveChatbot` resolver) is **not** here — see the
host side at `superset-frontend/src/components/ChatbotMount/` and
`superset-frontend/src/core/chatbot/`.
## Layout
```
extensions/chat/
├── extension.json Manifest (app.chatbot view + commands)
├── package.json
├── tsconfig.json
├── webpack.config.js ModuleFederation → window.superset
├── jest.config.js Self-contained unit tests
└── src/
├── index.tsx MF entry — calls activate() once
├── activate.ts Returns master disposable
├── commands.ts core.chatbot__open|close|toggle
├── state.ts Module-scoped open/closed + emitter
├── ReferenceChatbot.tsx Root component (bubble ↔ panel)
├── components/
│ ├── Bubble.tsx
│ ├── Panel.tsx
│ └── ErrorBoundary.tsx
├── streaming/
│ ├── mockStream.ts AsyncIterable<string> + AbortSignal
│ └── registry.ts Cross-component abort tracking
├── context/
│ └── pageContext.ts P3 namespace seam (defensive)
└── __tests__/
├── sdkMock.ts In-memory @apache-superset/core mock
└── activate.test.tsx
```
## Run the unit tests
```bash
cd extensions/chat
npm install # first time only
npx jest
```
The tests mock `@apache-superset/core` via `src/__tests__/sdkMock.ts` so they
do not depend on host runtime wiring.
## Build / bundle for deployment
```bash
# from the extension folder
npm install
npm run build
# packaging into a .supx is handled by the Superset extensions CLI
pip install apache-superset-extensions-cli
superset-extensions bundle # produces apache-superset.reference-chatbot-0.1.0.supx
```
Drop the `.supx` into the `EXTENSIONS_PATH` of a Superset instance that has
`FEATURE_FLAGS = { "ENABLE_EXTENSIONS": True }`.
## Selecting it as the active chatbot
The host's singleton picker reads `active_chatbot_id` from the admin
settings endpoint (`/api/v1/extensions/settings`). Set it to:
```
apache-superset.reference-chatbot
```
If no admin selection exists, the host falls back to the first-to-register
chatbot — installing this extension alone is enough for the bubble to appear.
## P3 integration seams
All page-context derivation lives in [`src/context/pageContext.ts`](src/context/pageContext.ts).
Each namespace branch (`dashboard`, `explore`, `dataset`, `navigation`) is
called defensively — when the host implementation lands, the returned value
becomes non-undefined automatically with no other change in the extension.
The panel re-reads context on `popstate`. Once `navigation.onDidChangePage`
is live on the host, the panel's `useEffect` should subscribe to it instead;
that is the only file in the extension that needs to change for full P3
context sync.
## Known intentional non-features
- No conversation persistence — by design (extension scope per SIP §2).
- No real network. The mock stream is a `setTimeout` token emitter so the
cancellation contract is exercised without external dependencies.
- No keyboard shortcut binding (Cmd+K). Extensions own that, but it adds
surface area not needed for platform validation.
- No notification badge / icon mutation. SIP §3.2 recommends static icons;
the bubble re-renders freely already.
## TODOs
- **P1**: if/when the host gains `deactivate(): Promise<void>`, wrap the
master disposer in `activate.ts` to flush async work before returning.
- **P3**: replace the `popstate` listener in `Panel.tsx` with
`navigation.onDidChangePage` once that event is wired up host-side.
- **P4**: if the host pre-registers `core.chatbot__*` as host-owned intents,
swap `commands.registerCommand` for the implementation hook in
`commands.ts`. Command IDs do not change.

View File

@@ -1,40 +0,0 @@
{
"publisher": "apache-superset",
"name": "reference-chatbot",
"displayName": "Reference Chatbot",
"description": "Canonical environment-validation chatbot extension for the superset.chatbot contribution area. Exercises registration, lifecycle, singleton resolution, commands, fault isolation, and streaming teardown. Not a product chatbot.",
"version": "0.1.0",
"license": "Apache-2.0",
"permissions": [],
"contributes": {
"views": {
"app": {
"chatbot": [
{
"id": "apache-superset.reference-chatbot",
"name": "Reference Chatbot",
"description": "Validates the chatbot extension environment end-to-end.",
"icon": "Bubble"
}
]
}
},
"commands": [
{
"id": "core.chatbot__open",
"title": "Open chatbot",
"description": "Opens the reference chatbot panel."
},
{
"id": "core.chatbot__close",
"title": "Close chatbot",
"description": "Closes the reference chatbot panel."
},
{
"id": "core.chatbot__toggle",
"title": "Toggle chatbot",
"description": "Toggles the reference chatbot panel."
}
]
}
}

View File

@@ -1,53 +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.
*/
const path = require('path');
// When run as a standalone package (`cd extensions/chat && npm test`), modules
// resolve from this folder's own node_modules. When run from the superset-frontend
// workspace (CI, dev convenience), resolve ts-jest there too.
const tsJest = (() => {
try {
require.resolve('ts-jest');
return 'ts-jest';
} catch {
return path.resolve(
__dirname,
'..',
'..',
'superset-frontend',
'node_modules',
'ts-jest',
);
}
})();
module.exports = {
testEnvironment: 'jsdom',
rootDir: __dirname,
testMatch: ['<rootDir>/src/**/*.test.{ts,tsx}'],
// When running from the extension folder without node_modules installed,
// resolve react / react-dom from the superset-frontend workspace.
modulePaths: [path.resolve(__dirname, '..', '..', 'superset-frontend', 'node_modules')],
moduleNameMapper: {
'^@apache-superset/core$': '<rootDir>/src/__tests__/sdkMock.ts',
},
transform: {
'^.+\\.tsx?$': [tsJest, { tsconfig: '<rootDir>/tsconfig.test.json' }],
},
};

View File

@@ -1,26 +0,0 @@
{
"name": "@apache-superset/reference-chatbot",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"description": "Reference chatbot extension that validates the Superset chatbot extension platform.",
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --stats-error-details --mode production"
},
"peerDependencies": {
"@apache-superset/core": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@apache-superset/core": "^0.1.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"ts-loader": "^9.5.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^5.0.0"
}
}

View File

@@ -1,45 +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 React, { useEffect, useState } from 'react';
import { commands } from '@apache-superset/core';
import { Bubble } from './components/Bubble';
import { Panel } from './components/Panel';
import { ExtensionErrorBoundary } from './components/ErrorBoundary';
import { isOpen, subscribe } from './state';
/**
* Root extension component. Mirrors module-state into React via `subscribe`
* so the bubble↔panel transition is driven by the same command handlers
* that external callers use (`core.chatbot__open`, `__close`, `__toggle`).
*/
export const ReferenceChatbot: React.FC = () => {
const [open, setOpenState] = useState<boolean>(isOpen());
useEffect(() => subscribe(setOpenState), []);
return (
<ExtensionErrorBoundary>
{open ? (
<Panel onClose={() => commands.executeCommand('core.chatbot__close')} />
) : (
<Bubble onClick={() => commands.executeCommand('core.chatbot__open')} />
)}
</ExtensionErrorBoundary>
);
};

View File

@@ -1,144 +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 { commands } from '@apache-superset/core';
import { registry, reset } from './sdkMock';
import { activate, VIEW_ID, CHATBOT_LOCATION } from '../activate';
import { isOpen } from '../state';
import { streamReply } from '../streaming/mockStream';
import {
registerActiveController,
unregisterActiveController,
abortAllActiveControllers,
} from '../streaming/registry';
beforeEach(() => {
reset();
});
test('registers one view at superset.chatbot and three chatbot commands', () => {
const disposable = activate();
try {
expect(registry.views.size).toBe(1);
const entry = registry.views.get(VIEW_ID);
expect(entry?.location).toBe(CHATBOT_LOCATION);
expect(entry?.view.icon).toBe('Bubble');
expect(Array.from(registry.commands.keys()).sort()).toEqual([
'core.chatbot__close',
'core.chatbot__open',
'core.chatbot__toggle',
]);
} finally {
disposable.dispose();
}
});
test('executeCommand drives open/close/toggle through module state', async () => {
const disposable = activate();
try {
expect(isOpen()).toBe(false);
await commands.executeCommand('core.chatbot__open');
expect(isOpen()).toBe(true);
await commands.executeCommand('core.chatbot__toggle');
expect(isOpen()).toBe(false);
await commands.executeCommand('core.chatbot__toggle');
expect(isOpen()).toBe(true);
await commands.executeCommand('core.chatbot__close');
expect(isOpen()).toBe(false);
} finally {
disposable.dispose();
}
});
test('disposing the master disposable unregisters view + commands', () => {
const disposable = activate();
expect(registry.views.size).toBe(1);
expect(registry.commands.size).toBe(3);
disposable.dispose();
expect(registry.views.size).toBe(0);
expect(registry.commands.size).toBe(0);
});
test('disposal is idempotent', () => {
const disposable = activate();
disposable.dispose();
expect(() => disposable.dispose()).not.toThrow();
expect(registry.views.size).toBe(0);
});
test('re-activate after dispose works (validates replace semantics)', () => {
const first = activate();
first.dispose();
const second = activate();
try {
expect(registry.views.size).toBe(1);
expect(registry.commands.size).toBe(3);
expect(isOpen()).toBe(false); // resetState() cleared open flag
} finally {
second.dispose();
}
});
test('aborting an active controller stops the stream cleanly', async () => {
const controller = new AbortController();
registerActiveController(controller);
const iter = streamReply('hello world', controller.signal);
const received: string[] = [];
const consume = (async () => {
for await (const tok of iter) received.push(tok);
})();
// Abort after a single tick — the iterator must return without throwing.
await new Promise(r => setTimeout(r, 50));
abortAllActiveControllers();
await expect(consume).resolves.toBeUndefined();
unregisterActiveController(controller);
expect(received.length).toBeLessThan(20); // would be ~20+ tokens if uncancelled
});
test('disposing the extension aborts any in-flight controller', async () => {
const disposable = activate();
const controller = new AbortController();
registerActiveController(controller);
const iter = streamReply('a longer prompt to ensure many tokens', controller.signal);
const consume = (async () => {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
for await (const _tok of iter) {
// drain
}
})();
await new Promise(r => setTimeout(r, 30));
disposable.dispose();
await expect(consume).resolves.toBeUndefined();
expect(controller.signal.aborted).toBe(true);
});

View File

@@ -1,119 +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.
*/
/**
* In-memory mock of `@apache-superset/core` for unit-testing the extension.
*
* Mirrors only the surfaces the reference chatbot consumes:
* - views.registerView returns a disposable that removes the view
* - commands.registerCommand / executeCommand round-trip handlers
* - sqlLab.getCurrentTab returns undefined (no SQL Lab in tests)
*
* The mock is intentionally observable: tests can read `registry.views` and
* `registry.commands` to assert contract compliance.
*/
import type { ReactElement } from 'react';
type Provider = () => ReactElement;
interface ViewDescriptor {
id: string;
name: string;
icon?: string;
description?: string;
}
interface DisposableLike {
dispose(): void;
}
interface RegisteredView {
view: ViewDescriptor;
location: string;
provider: Provider;
}
interface RegisteredCommand {
id: string;
title: string;
handler: (...args: any[]) => any;
}
export const registry = {
views: new Map<string, RegisteredView>(),
commands: new Map<string, RegisteredCommand>(),
};
export const reset = (): void => {
registry.views.clear();
registry.commands.clear();
};
export const views = {
registerView(
view: ViewDescriptor,
location: string,
provider: Provider,
): DisposableLike {
registry.views.set(view.id, { view, location, provider });
return {
dispose: () => {
registry.views.delete(view.id);
},
};
},
getViews(location: string) {
return Array.from(registry.views.values())
.filter(v => v.location === location)
.map(v => v.view);
},
};
export const commands = {
registerCommand(
command: { id: string; title: string },
handler: (...args: any[]) => any,
): DisposableLike {
registry.commands.set(command.id, {
id: command.id,
title: command.title,
handler,
});
return {
dispose: () => {
registry.commands.delete(command.id);
},
};
},
async executeCommand(id: string, ...rest: any[]): Promise<unknown> {
const cmd = registry.commands.get(id);
return cmd?.handler(...rest);
},
getCommands() {
return Array.from(registry.commands.values()).map(c => ({
id: c.id,
title: c.title,
}));
},
};
export const sqlLab = {
getCurrentTab: () => undefined,
};

View File

@@ -1,88 +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 React from 'react';
import { views } from '@apache-superset/core';
import { ReferenceChatbot } from './ReferenceChatbot';
import { registerChatbotCommands } from './commands';
import { abortAllActiveControllers } from './streaming/registry';
import { resetState } from './state';
export const VIEW_ID = 'apache-superset.reference-chatbot';
export const CHATBOT_LOCATION = 'superset.chatbot';
interface DisposableLike {
dispose(): void;
}
/**
* Registers the reference chatbot and returns a single disposable that
* tears down everything it created. Idempotent across activate/dispose cycles.
*
* Cleanup order matters: stop in-flight streams first so listeners do not
* receive late tokens, then unregister commands (so user clicks during teardown
* become no-ops), then unregister the view (so the host's ChatbotMount unmounts
* the React tree), and finally reset module state.
*
* Returns a plain `{ dispose }` object rather than constructing a Disposable
* from the SDK — the SDK class is host-injected and only reliably available
* via window.superset at runtime, while plain disposable-likes work in both
* runtime and unit-test contexts.
*
* TODO(P1): when the host gains an async `deactivate(): Promise<void>` hook,
* wrap the master disposer to flush in-flight async work before returning.
*/
export const activate = (): DisposableLike => {
const commandDisposables = registerChatbotCommands();
const viewDisposable = views.registerView(
{
id: VIEW_ID,
name: 'Reference Chatbot',
icon: 'Bubble',
description: 'Validates the chatbot extension environment end-to-end.',
},
CHATBOT_LOCATION,
() => React.createElement(ReferenceChatbot),
);
let disposed = false;
return {
dispose() {
if (disposed) return;
disposed = true;
try {
abortAllActiveControllers();
} catch {
// streams are best-effort during teardown
}
commandDisposables.forEach(d => {
try {
d.dispose();
} catch {
// a single command failing to unregister must not block the rest
}
});
try {
viewDisposable.dispose();
} catch {
// ignore
}
resetState();
},
};
};

View File

@@ -1,47 +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 { commands } from '@apache-superset/core';
import { isOpen, setOpen } from './state';
interface DisposableLike {
dispose(): void;
}
/**
* Registers the three chatbot intent commands and returns their disposables.
*
* TODO(P4): if/when the host pre-registers `core.chatbot__*` as host-owned
* intents that extensions implement instead of own, swap registerCommand for
* the implementation hook. The command ids stay the same so call sites do not
* change.
*/
export const registerChatbotCommands = (): DisposableLike[] => [
commands.registerCommand(
{ id: 'core.chatbot__open', title: 'Open chatbot' },
() => setOpen(true),
),
commands.registerCommand(
{ id: 'core.chatbot__close', title: 'Close chatbot' },
() => setOpen(false),
),
commands.registerCommand(
{ id: 'core.chatbot__toggle', title: 'Toggle chatbot' },
() => setOpen(!isOpen()),
),
];

View File

@@ -1,46 +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 React from 'react';
interface Props {
onClick: () => void;
}
export const Bubble: React.FC<Props> = ({ onClick }) => (
<button
type="button"
onClick={onClick}
data-test="reference-chatbot-bubble"
aria-label="Open reference chatbot"
style={{
width: 56,
height: 56,
borderRadius: '50%',
border: 'none',
background: '#1f6feb',
color: '#fff',
fontSize: 24,
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
}}
>
?
</button>
);

View File

@@ -1,66 +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 React from 'react';
interface State {
error: Error | null;
}
/**
* Defense-in-depth boundary. The host already wraps the mount in its own
* ErrorBoundary; this one keeps a panel crash from also bringing down the
* bubble next to it.
*/
export class ExtensionErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
State
> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error): void {
// eslint-disable-next-line no-console
console.error('[reference-chatbot] render error', error);
}
render() {
if (this.state.error) {
return (
<div
data-test="reference-chatbot-error"
style={{
padding: 12,
border: '1px solid #f5222d',
borderRadius: 6,
background: '#fff1f0',
color: '#a8071a',
fontSize: 12,
maxWidth: 320,
}}
>
Reference chatbot crashed: {this.state.error.message}
</div>
);
}
return <>{this.props.children}</>;
}
}

View File

@@ -1,307 +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 React, { useCallback, useEffect, useRef, useState } from 'react';
import { streamReply } from '../streaming/mockStream';
import { getPageContext, PageContext, subscribeToPageChanges } from '../context/pageContext';
import { registerActiveController, unregisterActiveController } from '../streaming/registry';
interface Props {
onClose: () => void;
}
interface Message {
id: number;
from: 'user' | 'bot';
text: string;
}
let messageSeq = 0;
/**
* Builds the full set of context fields the host exposes for the current
* surface, as ordered [label, value] rows. Whatever the host provides for where
* the user is, the panel shows — nothing is summarized away. Returns an empty
* array for surfaces with no active entity (list/home pages), where the
* `page:` line alone is the context.
*/
const contextRows = (ctx: PageContext): Array<[string, string]> => {
const rows: Array<[string, string]> = [];
const push = (label: string, value: unknown) => {
if (value !== undefined && value !== null && value !== '') {
rows.push([label, String(value)]);
}
};
const chart = ctx.chart as
| {
chartId?: number | null;
chartName?: string | null;
vizType?: string;
datasourceId?: number | null;
datasourceName?: string | null;
}
| undefined;
if (chart) {
push('chart', chart.chartName ?? (chart.chartId == null ? '(unsaved)' : ''));
push('chartId', chart.chartId);
push('viz', chart.vizType);
push('datasource', chart.datasourceName);
push('datasourceId', chart.datasourceId);
}
const dashboard = ctx.dashboard as
| { dashboardId?: number; title?: string; filters?: Array<{ label: string; value: unknown }> }
| undefined;
if (dashboard) {
push('dashboard', dashboard.title);
push('dashboardId', dashboard.dashboardId);
const filters = dashboard.filters ?? [];
if (filters.length) {
push(
'filters',
filters.map(f => `${f.label}=${JSON.stringify(f.value)}`).join(', '),
);
}
}
const dataset = ctx.dataset as
| {
datasetId?: number;
datasetName?: string;
schema?: string | null;
catalog?: string | null;
databaseName?: string | null;
isVirtual?: boolean;
}
| undefined;
if (dataset) {
push('dataset', dataset.datasetName);
push('datasetId', dataset.datasetId);
push('schema', dataset.schema);
push('catalog', dataset.catalog);
push('database', dataset.databaseName);
if (typeof dataset.isVirtual === 'boolean') {
push('virtual', dataset.isVirtual);
}
}
if (ctx.sqlLab) {
push('tab', ctx.sqlLab.title);
}
return rows;
};
export const Panel: React.FC<Props> = ({ onClose }) => {
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const [streaming, setStreaming] = useState(false);
const [pageContext, setPageContext] = useState<PageContext>(() => getPageContext());
const controllerRef = useRef<AbortController | null>(null);
useEffect(
() => subscribeToPageChanges(() => setPageContext(getPageContext())),
[],
);
useEffect(
() => () => {
// Component unmount cancels any in-flight stream.
controllerRef.current?.abort();
},
[],
);
const send = useCallback(async () => {
const prompt = input.trim();
if (!prompt || streaming) return;
setInput('');
const userMsg: Message = { id: ++messageSeq, from: 'user', text: prompt };
const botMsg: Message = { id: ++messageSeq, from: 'bot', text: '' };
setMessages(prev => [...prev, userMsg, botMsg]);
setStreaming(true);
const controller = new AbortController();
controllerRef.current = controller;
registerActiveController(controller);
try {
for await (const token of streamReply(prompt, controller.signal)) {
setMessages(prev =>
prev.map(m => (m.id === botMsg.id ? { ...m, text: m.text + token } : m)),
);
}
} finally {
unregisterActiveController(controller);
controllerRef.current = null;
setStreaming(false);
}
}, [input, streaming]);
const cancel = useCallback(() => {
controllerRef.current?.abort();
}, []);
return (
<div
data-test="reference-chatbot-panel"
style={{
width: 360,
maxHeight: 480,
display: 'flex',
flexDirection: 'column',
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: 8,
boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
overflow: 'hidden',
fontSize: 13,
}}
>
<header
style={{
padding: '8px 12px',
background: '#1f6feb',
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>Reference Chatbot</span>
<button
type="button"
onClick={onClose}
aria-label="Close chatbot"
data-test="reference-chatbot-close"
style={{
background: 'transparent',
border: 'none',
color: '#fff',
fontSize: 16,
cursor: 'pointer',
}}
>
×
</button>
</header>
<div
data-test="reference-chatbot-context"
style={{
padding: '6px 12px',
background: '#f6f8fa',
borderBottom: '1px solid #eaecef',
fontFamily: 'monospace',
fontSize: 11,
color: '#57606a',
wordBreak: 'break-all',
}}
>
<div>page: {pageContext.pageType}</div>
{contextRows(pageContext).map(([label, value]) => (
<div key={label}>
{label}: {value}
</div>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
{messages.length === 0 && (
<p style={{ color: '#8c8c8c' }}>
Ask anything replies are canned tokens streamed by the reference extension.
</p>
)}
{messages.map(m => (
<div
key={m.id}
data-test={`reference-chatbot-msg-${m.from}`}
style={{
margin: '6px 0',
textAlign: m.from === 'user' ? 'right' : 'left',
}}
>
<span
style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: 6,
background: m.from === 'user' ? '#1f6feb' : '#eef0f3',
color: m.from === 'user' ? '#fff' : '#1f2328',
maxWidth: '85%',
whiteSpace: 'pre-wrap',
}}
>
{m.text || '…'}
</span>
</div>
))}
</div>
<footer
style={{
padding: 8,
borderTop: '1px solid #eaecef',
display: 'flex',
gap: 6,
}}
>
<input
aria-label="Chat input"
data-test="reference-chatbot-input"
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
}}
placeholder="Type a message"
style={{
flex: 1,
padding: '4px 8px',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
/>
{streaming ? (
<button
type="button"
onClick={cancel}
data-test="reference-chatbot-cancel"
style={{ padding: '4px 10px' }}
>
Stop
</button>
) : (
<button
type="button"
onClick={send}
data-test="reference-chatbot-send"
disabled={!input.trim()}
style={{ padding: '4px 10px' }}
>
Send
</button>
)}
</footer>
</div>
);
};

View File

@@ -1,159 +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.
*/
/**
* Single integration seam for the P3 namespaces.
*
* Each surface namespace is consumed via a try/catch — the host may ship a
* version where a namespace function is declared but not yet implemented at
* runtime, and the reference extension must keep working in that case. As
* each namespace lights up on the host, that branch starts returning real
* data without any change here.
*
* Route inference is the fallback when navigation.getPageType() is absent.
*/
import * as core from '@apache-superset/core';
export type PageType =
| 'home'
| 'dashboard'
| 'dashboard_list'
| 'chart'
| 'chart_list'
| 'sqllab'
| 'query_history'
| 'saved_queries'
| 'dataset'
| 'dataset_list'
| 'unknown';
export interface PageContext {
pageType: PageType;
dashboard?: unknown;
chart?: unknown;
dataset?: unknown;
sqlLab?: { tabId: string; title: string };
href: string;
}
const tryCall = <T>(fn: () => T | undefined): T | undefined => {
try {
return fn();
} catch {
return undefined;
}
};
const inferPageType = (pathname: string): PageType => {
if (pathname.startsWith('/sqllab/history')) return 'query_history';
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
if (pathname.startsWith('/sqllab')) return 'sqllab';
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
if (
pathname.startsWith('/superset/dashboard') ||
pathname.startsWith('/dashboard')
)
return 'dashboard';
if (pathname.startsWith('/chart/list')) return 'chart_list';
if (pathname.startsWith('/explore') || pathname.startsWith('/chart'))
return 'chart';
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
if (pathname.startsWith('/tablemodelview') || pathname.startsWith('/dataset'))
return 'dataset';
if (pathname === '/' || pathname.startsWith('/superset/welcome'))
return 'home';
return 'unknown';
};
const readSqlLabTab = (): PageContext['sqlLab'] => {
const tab = tryCall(() => (core as any).sqlLab?.getCurrentTab?.());
return tab ? { tabId: tab.id, title: tab.title } : undefined;
};
const readPageType = (pathname: string): PageType => {
const fromNav = tryCall(() => (core as any).navigation?.getPageType?.());
return (fromNav as PageType | undefined) ?? inferPageType(pathname);
};
/**
* Subscribe to page-context changes and invoke `onChange` whenever any part of
* the context may have changed. Returns a cleanup function.
*
* Three classes of change are watched:
* - Navigation (`navigation.onDidChangePage`, or `popstate` as a fallback for
* hosts without the namespace) — the user moved to a different surface.
* - Entity hydration (`explore.onDidChangeChart`, `dashboard.onDidChangeDashboard`,
* `dataset.onDidChangeDataset`) — the surface's entity loaded or changed
* *after* navigation settled. This matters because a surface (notably Explore)
* can finish hydrating several seconds after the route change fires, so a
* navigation-only subscription would read empty entity context and never
* refresh once the real data arrives.
* - In-surface SQL Lab changes (`sqlLab.onDidChangeActiveTab`,
* `sqlLab.onDidChangeTabTitle`) — switching or renaming a tab does not change
* the route, so without these the panel would keep showing the first tab.
*/
export const subscribeToPageChanges = (onChange: () => void): (() => void) => {
const disposers: Array<() => void> = [];
const nav = tryCall(() => (core as any).navigation);
if (nav?.onDidChangePage) {
const sub = nav.onDidChangePage(onChange);
disposers.push(() => sub.dispose());
} else {
window.addEventListener('popstate', onChange);
disposers.push(() => window.removeEventListener('popstate', onChange));
}
// Entity-context change events. Each is optional — a host may not implement a
// given namespace yet — so subscribe defensively and collect any disposer.
const subscribeEntity = (
getNamespace: () => any,
method: string,
): void => {
const sub = tryCall(() => getNamespace()?.[method]?.(onChange));
if (sub?.dispose) {
disposers.push(() => sub.dispose());
}
};
subscribeEntity(() => (core as any).explore, 'onDidChangeChart');
subscribeEntity(() => (core as any).dashboard, 'onDidChangeDashboard');
subscribeEntity(() => (core as any).dataset, 'onDidChangeDataset');
// SQL Lab tab switches/renames happen without a route change.
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeActiveTab');
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeTabTitle');
return () => disposers.forEach(dispose => dispose());
};
export const getPageContext = (): PageContext => {
const { pathname, href } =
typeof window !== 'undefined'
? window.location
: { pathname: '', href: '' };
return {
pageType: readPageType(pathname),
dashboard: tryCall(() => (core as any).dashboard?.getCurrentDashboard?.()),
chart: tryCall(() => (core as any).explore?.getCurrentChart?.()),
dataset: tryCall(() => (core as any).dataset?.getCurrentDataset?.()),
sqlLab: readSqlLabTab(),
href,
};
};

View File

@@ -1,57 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Module-scoped open/closed state plus a tiny emitter the UI subscribes to.
*
* Lives entirely inside the extension — never reaches into the host store.
* Reset on dispose so re-activation starts cleanly.
*/
export type OpenStateListener = (open: boolean) => void;
let open = false;
const listeners = new Set<OpenStateListener>();
export const isOpen = (): boolean => open;
export const setOpen = (next: boolean): void => {
if (next === open) return;
open = next;
listeners.forEach(fn => {
try {
fn(open);
} catch {
// A listener throwing must not block other listeners or flip our state back.
}
});
};
export const subscribe = (fn: OpenStateListener): (() => void) => {
listeners.add(fn);
return () => {
listeners.delete(fn);
};
};
/** Drains listeners and resets state. Called from the master Disposable. */
export const resetState = (): void => {
open = false;
listeners.clear();
};

View File

@@ -1,73 +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.
*/
/**
* Mock streaming reply used to validate stream teardown semantics.
*
* The reference chatbot is environment-validation only — there is no LLM.
* This iterator yields canned tokens on a timer and exits cleanly when its
* AbortSignal is fired. Disposal of the extension aborts any in-flight
* controller, which is the contract that proves async cancellation works.
*/
const TICK_MS = 40;
const buildReply = (prompt: string): string => {
const trimmed = prompt.trim();
if (!trimmed) {
return 'Reference chatbot online. Send a message to validate streaming.';
}
return (
`[reference-chatbot] received "${trimmed}". ` +
'Streaming token-by-token to validate cancellation and teardown.'
);
};
const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException('aborted', 'AbortError'));
return;
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new DOMException('aborted', 'AbortError'));
};
signal.addEventListener('abort', onAbort, { once: true });
});
export async function* streamReply(
prompt: string,
signal: AbortSignal,
): AsyncIterableIterator<string> {
const tokens = buildReply(prompt).split(/(\s+)/);
for (const token of tokens) {
if (signal.aborted) return;
try {
await sleep(TICK_MS, signal);
} catch {
return;
}
yield token;
}
}

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"target": "es2019",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "es2019"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/__tests__"]
}

View File

@@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@apache-superset/core": ["src/__tests__/sdkMock.ts"]
},
"typeRoots": [
"./node_modules/@types",
"../../superset-frontend/node_modules/@types"
],
"types": ["jest", "node"]
},
"include": ["src"],
"exclude": []
}

View File

@@ -1,108 +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.
*/
const path = require('path');
const fs = require('fs');
const { ModuleFederationPlugin } = require('webpack').container;
const packageConfig = require('./package.json');
const extensionConfig = require('./extension.json');
const MODULE_FEDERATION_NAME = 'apacheSuperset_referenceChatbot';
/**
* Emits the `manifest.json` the host reads from the extension `dist/` root.
*
* The host (`superset/extensions/utils.py`) expects an extension dist laid out
* as `dist/manifest.json` plus the federated bundle under `dist/frontend/dist/`.
* The manifest carries `extension.json` verbatim, plus the composite `id` and a
* `frontend` block naming the content-hashed `remoteEntry` so the host can load
* the right file. Because the hash is only known after the build, the manifest
* is written from the final asset names rather than checked in.
*/
class EmitManifestPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tap('EmitManifestPlugin', compilation => {
const assets = Object.keys(compilation.assets);
const remoteEntry = assets.find(name => /^remoteEntry\..*\.js$/.test(name));
if (!remoteEntry) {
throw new Error('EmitManifestPlugin: no remoteEntry asset was emitted');
}
const manifest = {
...extensionConfig,
id: `${extensionConfig.publisher}.${extensionConfig.name}`,
frontend: {
remoteEntry,
moduleFederationName: MODULE_FEDERATION_NAME,
},
};
fs.writeFileSync(
path.resolve(__dirname, 'dist', 'manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`,
);
});
}
}
module.exports = (env, argv) => {
const isProd = argv.mode === 'production';
return {
entry: isProd ? {} : './src/index.tsx',
mode: isProd ? 'production' : 'development',
devtool: isProd ? false : 'eval-cheap-module-source-map',
devServer: {
port: 3030,
headers: { 'Access-Control-Allow-Origin': '*' },
},
output: {
clean: true,
filename: isProd ? undefined : '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist', 'frontend', 'dist'),
publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`,
},
resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
externalsType: 'window',
externals: { '@apache-superset/core': 'superset' },
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
],
},
plugins: [
new ModuleFederationPlugin({
name: MODULE_FEDERATION_NAME,
filename: 'remoteEntry.[contenthash].js',
exposes: { './index': './src/index.tsx' },
shared: {
react: {
singleton: true,
requiredVersion: packageConfig.peerDependencies.react,
import: false,
},
'react-dom': {
singleton: true,
requiredVersion: packageConfig.peerDependencies['react-dom'],
import: false,
},
},
}),
new EmitManifestPlugin(),
],
};
};

View File

@@ -1,138 +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.
-->
# Reference Chatbot Extension
Canonical environment-validation extension for the `superset.chatbot`
contribution area. **Not** a product chatbot — there is no LLM, no backend,
no persistence. Its purpose is to exercise the extension platform end-to-end:
- `views.registerView` at `superset.chatbot` (singleton resolution)
- Lifecycle activation + a master disposable that tears down everything
- `commands.registerCommand` for `core.chatbot__open|close|toggle`
- Mock streaming with `AbortController` cancellation on dispose
- Defense-in-depth React error boundary inside the panel
- A single P3 page-context seam that lights up automatically as the
`dashboard` / `explore` / `dataset` / `navigation` namespaces become
available at runtime on the host
It is intended as the reference implementation third-party chatbot extension
authors copy. Anything that ships as host-internal (the mount point, the
admin picker, the `getActiveChatbot` resolver) is **not** here — see the
host side at `superset-frontend/src/components/ChatbotMount/` and
`superset-frontend/src/core/chatbot/`.
## Layout
```
extensions/chat/
├── extension.json Manifest (app.chatbot view + commands)
├── package.json
├── tsconfig.json
├── webpack.config.js ModuleFederation → window.superset
├── jest.config.js Self-contained unit tests
└── src/
├── index.tsx MF entry — calls activate() once
├── activate.ts Returns master disposable
├── commands.ts core.chatbot__open|close|toggle
├── state.ts Module-scoped open/closed + emitter
├── ReferenceChatbot.tsx Root component (bubble ↔ panel)
├── components/
│ ├── Bubble.tsx
│ ├── Panel.tsx
│ └── ErrorBoundary.tsx
├── streaming/
│ ├── mockStream.ts AsyncIterable<string> + AbortSignal
│ └── registry.ts Cross-component abort tracking
├── context/
│ └── pageContext.ts P3 namespace seam (defensive)
└── __tests__/
├── sdkMock.ts In-memory @apache-superset/core mock
└── activate.test.tsx
```
## Run the unit tests
```bash
cd extensions/chat
npm install # first time only
npx jest
```
The tests mock `@apache-superset/core` via `src/__tests__/sdkMock.ts` so they
do not depend on host runtime wiring.
## Build / bundle for deployment
```bash
# from the extension folder
npm install
npm run build
# packaging into a .supx is handled by the Superset extensions CLI
pip install apache-superset-extensions-cli
superset-extensions bundle # produces apache-superset.reference-chatbot-0.1.0.supx
```
Drop the `.supx` into the `EXTENSIONS_PATH` of a Superset instance that has
`FEATURE_FLAGS = { "ENABLE_EXTENSIONS": True }`.
## Selecting it as the active chatbot
The host's singleton picker reads `active_chatbot_id` from the admin
settings endpoint (`/api/v1/extensions/settings`). Set it to:
```
apache-superset.reference-chatbot
```
If no admin selection exists, the host falls back to the first-to-register
chatbot — installing this extension alone is enough for the bubble to appear.
## P3 integration seams
All page-context derivation lives in [`src/context/pageContext.ts`](src/context/pageContext.ts).
Each namespace branch (`dashboard`, `explore`, `dataset`, `navigation`) is
called defensively — when the host implementation lands, the returned value
becomes non-undefined automatically with no other change in the extension.
The panel re-reads context on `popstate`. Once `navigation.onDidChangePage`
is live on the host, the panel's `useEffect` should subscribe to it instead;
that is the only file in the extension that needs to change for full P3
context sync.
## Known intentional non-features
- No conversation persistence — by design (extension scope per SIP §2).
- No real network. The mock stream is a `setTimeout` token emitter so the
cancellation contract is exercised without external dependencies.
- No keyboard shortcut binding (Cmd+K). Extensions own that, but it adds
surface area not needed for platform validation.
- No notification badge / icon mutation. SIP §3.2 recommends static icons;
the bubble re-renders freely already.
## TODOs
- **P1**: if/when the host gains `deactivate(): Promise<void>`, wrap the
master disposer in `activate.ts` to flush async work before returning.
- **P3**: replace the `popstate` listener in `Panel.tsx` with
`navigation.onDidChangePage` once that event is wired up host-side.
- **P4**: if the host pre-registers `core.chatbot__*` as host-owned intents,
swap `commands.registerCommand` for the implementation hook in
`commands.ts`. Command IDs do not change.

View File

@@ -1,23 +0,0 @@
{
"publisher": "apache-superset",
"name": "alt-chatbot",
"displayName": "Alt Chatbot",
"description": "Second chatbot for testing multi-chatbot selection in the superset.chatbot contribution area.",
"version": "0.1.0",
"license": "Apache-2.0",
"permissions": [],
"contributes": {
"views": {
"app": {
"chatbot": [
{
"id": "apache-superset.alt-chatbot",
"name": "Alt Chatbot",
"description": "Second chatbot for testing singleton resolution.",
"icon": "Star"
}
]
}
}
}
}

View File

@@ -1,53 +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.
*/
const path = require('path');
// When run as a standalone package (`cd extensions/chat && npm test`), modules
// resolve from this folder's own node_modules. When run from the superset-frontend
// workspace (CI, dev convenience), resolve ts-jest there too.
const tsJest = (() => {
try {
require.resolve('ts-jest');
return 'ts-jest';
} catch {
return path.resolve(
__dirname,
'..',
'..',
'superset-frontend',
'node_modules',
'ts-jest',
);
}
})();
module.exports = {
testEnvironment: 'jsdom',
rootDir: __dirname,
testMatch: ['<rootDir>/src/**/*.test.{ts,tsx}'],
// When running from the extension folder without node_modules installed,
// resolve react / react-dom from the superset-frontend workspace.
modulePaths: [path.resolve(__dirname, '..', '..', 'superset-frontend', 'node_modules')],
moduleNameMapper: {
'^@apache-superset/core$': '<rootDir>/src/__tests__/sdkMock.ts',
},
transform: {
'^.+\\.tsx?$': [tsJest, { tsconfig: '<rootDir>/tsconfig.test.json' }],
},
};

View File

@@ -1,26 +0,0 @@
{
"name": "@apache-superset/alt-chatbot",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"description": "Second chatbot extension for testing multi-chatbot selection in the Superset chatbot contribution area.",
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --stats-error-details --mode production"
},
"peerDependencies": {
"@apache-superset/core": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@apache-superset/core": "^0.1.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"ts-loader": "^9.5.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^5.0.0"
}
}

View File

@@ -1,46 +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 React, { useEffect, useState } from 'react';
import { Bubble } from './components/Bubble';
import { Panel } from './components/Panel';
import { ExtensionErrorBoundary } from './components/ErrorBoundary';
import { isOpen, setOpen, subscribe } from './state';
/**
* Root extension component. Mirrors module-state into React via `subscribe`.
*
* Unlike the Reference Chatbot, Alt registers no `core.chatbot__*` commands
* (those ids are globally owned by Reference), so the bubble↔panel transition
* drives the local open-state directly via `setOpen`.
*/
export const ReferenceChatbot: React.FC = () => {
const [open, setOpenState] = useState<boolean>(isOpen());
useEffect(() => subscribe(setOpenState), []);
return (
<ExtensionErrorBoundary>
{open ? (
<Panel onClose={() => setOpen(false)} />
) : (
<Bubble onClick={() => setOpen(true)} />
)}
</ExtensionErrorBoundary>
);
};

View File

@@ -1,131 +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 { registry, reset } from './sdkMock';
import { activate, VIEW_ID, CHATBOT_LOCATION } from '../activate';
import { isOpen, setOpen } from '../state';
import { streamReply } from '../streaming/mockStream';
import {
registerActiveController,
unregisterActiveController,
abortAllActiveControllers,
} from '../streaming/registry';
beforeEach(() => {
reset();
});
test('registers one view at superset.chatbot and no commands', () => {
const disposable = activate();
try {
expect(registry.views.size).toBe(1);
const entry = registry.views.get(VIEW_ID);
expect(entry?.location).toBe(CHATBOT_LOCATION);
expect(entry?.view.icon).toBe('Star');
// Alt Chatbot is view-only — the core.chatbot__* command ids are owned by
// the Reference Chatbot, so Alt registers none of its own.
expect(registry.commands.size).toBe(0);
} finally {
disposable.dispose();
}
});
test('setOpen drives open/close through module state', () => {
const disposable = activate();
try {
expect(isOpen()).toBe(false);
setOpen(true);
expect(isOpen()).toBe(true);
setOpen(false);
expect(isOpen()).toBe(false);
} finally {
disposable.dispose();
}
});
test('disposing the master disposable unregisters the view', () => {
const disposable = activate();
expect(registry.views.size).toBe(1);
disposable.dispose();
expect(registry.views.size).toBe(0);
expect(registry.commands.size).toBe(0);
});
test('disposal is idempotent', () => {
const disposable = activate();
disposable.dispose();
expect(() => disposable.dispose()).not.toThrow();
expect(registry.views.size).toBe(0);
});
test('re-activate after dispose works (validates replace semantics)', () => {
const first = activate();
first.dispose();
const second = activate();
try {
expect(registry.views.size).toBe(1);
expect(isOpen()).toBe(false); // resetState() cleared open flag
} finally {
second.dispose();
}
});
test('aborting an active controller stops the stream cleanly', async () => {
const controller = new AbortController();
registerActiveController(controller);
const iter = streamReply('hello world', controller.signal);
const received: string[] = [];
const consume = (async () => {
for await (const tok of iter) received.push(tok);
})();
// Abort after a single tick — the iterator must return without throwing.
await new Promise(r => setTimeout(r, 50));
abortAllActiveControllers();
await expect(consume).resolves.toBeUndefined();
unregisterActiveController(controller);
expect(received.length).toBeLessThan(20); // would be ~20+ tokens if uncancelled
});
test('disposing the extension aborts any in-flight controller', async () => {
const disposable = activate();
const controller = new AbortController();
registerActiveController(controller);
const iter = streamReply('a longer prompt to ensure many tokens', controller.signal);
const consume = (async () => {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
for await (const _tok of iter) {
// drain
}
})();
await new Promise(r => setTimeout(r, 30));
disposable.dispose();
await expect(consume).resolves.toBeUndefined();
expect(controller.signal.aborted).toBe(true);
});

View File

@@ -1,119 +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.
*/
/**
* In-memory mock of `@apache-superset/core` for unit-testing the extension.
*
* Mirrors only the surfaces the reference chatbot consumes:
* - views.registerView returns a disposable that removes the view
* - commands.registerCommand / executeCommand round-trip handlers
* - sqlLab.getCurrentTab returns undefined (no SQL Lab in tests)
*
* The mock is intentionally observable: tests can read `registry.views` and
* `registry.commands` to assert contract compliance.
*/
import type { ReactElement } from 'react';
type Provider = () => ReactElement;
interface ViewDescriptor {
id: string;
name: string;
icon?: string;
description?: string;
}
interface DisposableLike {
dispose(): void;
}
interface RegisteredView {
view: ViewDescriptor;
location: string;
provider: Provider;
}
interface RegisteredCommand {
id: string;
title: string;
handler: (...args: any[]) => any;
}
export const registry = {
views: new Map<string, RegisteredView>(),
commands: new Map<string, RegisteredCommand>(),
};
export const reset = (): void => {
registry.views.clear();
registry.commands.clear();
};
export const views = {
registerView(
view: ViewDescriptor,
location: string,
provider: Provider,
): DisposableLike {
registry.views.set(view.id, { view, location, provider });
return {
dispose: () => {
registry.views.delete(view.id);
},
};
},
getViews(location: string) {
return Array.from(registry.views.values())
.filter(v => v.location === location)
.map(v => v.view);
},
};
export const commands = {
registerCommand(
command: { id: string; title: string },
handler: (...args: any[]) => any,
): DisposableLike {
registry.commands.set(command.id, {
id: command.id,
title: command.title,
handler,
});
return {
dispose: () => {
registry.commands.delete(command.id);
},
};
},
async executeCommand(id: string, ...rest: any[]): Promise<unknown> {
const cmd = registry.commands.get(id);
return cmd?.handler(...rest);
},
getCommands() {
return Array.from(registry.commands.values()).map(c => ({
id: c.id,
title: c.title,
}));
},
};
export const sqlLab = {
getCurrentTab: () => undefined,
};

View File

@@ -1,83 +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 React from 'react';
import { views } from '@apache-superset/core';
import { ReferenceChatbot } from './ReferenceChatbot';
import { abortAllActiveControllers } from './streaming/registry';
import { resetState } from './state';
export const VIEW_ID = 'apache-superset.alt-chatbot';
export const CHATBOT_LOCATION = 'superset.chatbot';
interface DisposableLike {
dispose(): void;
}
/**
* Registers the reference chatbot and returns a single disposable that
* tears down everything it created. Idempotent across activate/dispose cycles.
*
* Cleanup order matters: stop in-flight streams first so listeners do not
* receive late tokens, then unregister commands (so user clicks during teardown
* become no-ops), then unregister the view (so the host's ChatbotMount unmounts
* the React tree), and finally reset module state.
*
* Returns a plain `{ dispose }` object rather than constructing a Disposable
* from the SDK — the SDK class is host-injected and only reliably available
* via window.superset at runtime, while plain disposable-likes work in both
* runtime and unit-test contexts.
*
* TODO(P1): when the host gains an async `deactivate(): Promise<void>` hook,
* wrap the master disposer to flush in-flight async work before returning.
*/
export const activate = (): DisposableLike => {
// Alt Chatbot deliberately registers no commands: the `core.chatbot__*`
// command ids are owned by the Reference Chatbot, and command ids are global,
// so a second registrant would collide. Alt is a view-only chatbot used to
// exercise multi-chatbot selection.
const viewDisposable = views.registerView(
{
id: VIEW_ID,
name: 'Alt Chatbot',
icon: 'Star',
description: 'Second chatbot for testing singleton resolution.',
},
CHATBOT_LOCATION,
() => React.createElement(ReferenceChatbot),
);
let disposed = false;
return {
dispose() {
if (disposed) return;
disposed = true;
try {
abortAllActiveControllers();
} catch {
// streams are best-effort during teardown
}
try {
viewDisposable.dispose();
} catch {
// ignore
}
resetState();
},
};
};

View File

@@ -1,66 +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 React from 'react';
interface State {
error: Error | null;
}
/**
* Defense-in-depth boundary. The host already wraps the mount in its own
* ErrorBoundary; this one keeps a panel crash from also bringing down the
* bubble next to it.
*/
export class ExtensionErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
State
> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error): void {
// eslint-disable-next-line no-console
console.error('[reference-chatbot] render error', error);
}
render() {
if (this.state.error) {
return (
<div
data-test="reference-chatbot-error"
style={{
padding: 12,
border: '1px solid #f5222d',
borderRadius: 6,
background: '#fff1f0',
color: '#a8071a',
fontSize: 12,
maxWidth: 320,
}}
>
Reference chatbot crashed: {this.state.error.message}
</div>
);
}
return <>{this.props.children}</>;
}
}

View File

@@ -1,307 +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 React, { useCallback, useEffect, useRef, useState } from 'react';
import { streamReply } from '../streaming/mockStream';
import { getPageContext, PageContext, subscribeToPageChanges } from '../context/pageContext';
import { registerActiveController, unregisterActiveController } from '../streaming/registry';
interface Props {
onClose: () => void;
}
interface Message {
id: number;
from: 'user' | 'bot';
text: string;
}
let messageSeq = 0;
/**
* Builds the full set of context fields the host exposes for the current
* surface, as ordered [label, value] rows. Whatever the host provides for where
* the user is, the panel shows — nothing is summarized away. Returns an empty
* array for surfaces with no active entity (list/home pages), where the
* `page:` line alone is the context.
*/
const contextRows = (ctx: PageContext): Array<[string, string]> => {
const rows: Array<[string, string]> = [];
const push = (label: string, value: unknown) => {
if (value !== undefined && value !== null && value !== '') {
rows.push([label, String(value)]);
}
};
const chart = ctx.chart as
| {
chartId?: number | null;
chartName?: string | null;
vizType?: string;
datasourceId?: number | null;
datasourceName?: string | null;
}
| undefined;
if (chart) {
push('chart', chart.chartName ?? (chart.chartId == null ? '(unsaved)' : ''));
push('chartId', chart.chartId);
push('viz', chart.vizType);
push('datasource', chart.datasourceName);
push('datasourceId', chart.datasourceId);
}
const dashboard = ctx.dashboard as
| { dashboardId?: number; title?: string; filters?: Array<{ label: string; value: unknown }> }
| undefined;
if (dashboard) {
push('dashboard', dashboard.title);
push('dashboardId', dashboard.dashboardId);
const filters = dashboard.filters ?? [];
if (filters.length) {
push(
'filters',
filters.map(f => `${f.label}=${JSON.stringify(f.value)}`).join(', '),
);
}
}
const dataset = ctx.dataset as
| {
datasetId?: number;
datasetName?: string;
schema?: string | null;
catalog?: string | null;
databaseName?: string | null;
isVirtual?: boolean;
}
| undefined;
if (dataset) {
push('dataset', dataset.datasetName);
push('datasetId', dataset.datasetId);
push('schema', dataset.schema);
push('catalog', dataset.catalog);
push('database', dataset.databaseName);
if (typeof dataset.isVirtual === 'boolean') {
push('virtual', dataset.isVirtual);
}
}
if (ctx.sqlLab) {
push('tab', ctx.sqlLab.title);
}
return rows;
};
export const Panel: React.FC<Props> = ({ onClose }) => {
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const [streaming, setStreaming] = useState(false);
const [pageContext, setPageContext] = useState<PageContext>(() => getPageContext());
const controllerRef = useRef<AbortController | null>(null);
useEffect(
() => subscribeToPageChanges(() => setPageContext(getPageContext())),
[],
);
useEffect(
() => () => {
// Component unmount cancels any in-flight stream.
controllerRef.current?.abort();
},
[],
);
const send = useCallback(async () => {
const prompt = input.trim();
if (!prompt || streaming) return;
setInput('');
const userMsg: Message = { id: ++messageSeq, from: 'user', text: prompt };
const botMsg: Message = { id: ++messageSeq, from: 'bot', text: '' };
setMessages(prev => [...prev, userMsg, botMsg]);
setStreaming(true);
const controller = new AbortController();
controllerRef.current = controller;
registerActiveController(controller);
try {
for await (const token of streamReply(prompt, controller.signal)) {
setMessages(prev =>
prev.map(m => (m.id === botMsg.id ? { ...m, text: m.text + token } : m)),
);
}
} finally {
unregisterActiveController(controller);
controllerRef.current = null;
setStreaming(false);
}
}, [input, streaming]);
const cancel = useCallback(() => {
controllerRef.current?.abort();
}, []);
return (
<div
data-test="reference-chatbot-panel"
style={{
width: 360,
maxHeight: 480,
display: 'flex',
flexDirection: 'column',
background: '#fff',
border: '1px solid #d9d9d9',
borderRadius: 8,
boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
overflow: 'hidden',
fontSize: 13,
}}
>
<header
style={{
padding: '8px 12px',
background: '#2da44e',
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>Alt Chatbot</span>
<button
type="button"
onClick={onClose}
aria-label="Close chatbot"
data-test="reference-chatbot-close"
style={{
background: 'transparent',
border: 'none',
color: '#fff',
fontSize: 16,
cursor: 'pointer',
}}
>
×
</button>
</header>
<div
data-test="reference-chatbot-context"
style={{
padding: '6px 12px',
background: '#f6f8fa',
borderBottom: '1px solid #eaecef',
fontFamily: 'monospace',
fontSize: 11,
color: '#57606a',
wordBreak: 'break-all',
}}
>
<div>page: {pageContext.pageType}</div>
{contextRows(pageContext).map(([label, value]) => (
<div key={label}>
{label}: {value}
</div>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
{messages.length === 0 && (
<p style={{ color: '#8c8c8c' }}>
Ask anything replies are canned tokens streamed by the Alt Chatbot extension.
</p>
)}
{messages.map(m => (
<div
key={m.id}
data-test={`reference-chatbot-msg-${m.from}`}
style={{
margin: '6px 0',
textAlign: m.from === 'user' ? 'right' : 'left',
}}
>
<span
style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: 6,
background: m.from === 'user' ? '#2da44e' : '#eef0f3',
color: m.from === 'user' ? '#fff' : '#1f2328',
maxWidth: '85%',
whiteSpace: 'pre-wrap',
}}
>
{m.text || '…'}
</span>
</div>
))}
</div>
<footer
style={{
padding: 8,
borderTop: '1px solid #eaecef',
display: 'flex',
gap: 6,
}}
>
<input
aria-label="Chat input"
data-test="reference-chatbot-input"
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
}}
placeholder="Type a message"
style={{
flex: 1,
padding: '4px 8px',
border: '1px solid #d9d9d9',
borderRadius: 4,
}}
/>
{streaming ? (
<button
type="button"
onClick={cancel}
data-test="reference-chatbot-cancel"
style={{ padding: '4px 10px' }}
>
Stop
</button>
) : (
<button
type="button"
onClick={send}
data-test="reference-chatbot-send"
disabled={!input.trim()}
style={{ padding: '4px 10px' }}
>
Send
</button>
)}
</footer>
</div>
);
};

View File

@@ -1,159 +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.
*/
/**
* Single integration seam for the P3 namespaces.
*
* Each surface namespace is consumed via a try/catch — the host may ship a
* version where a namespace function is declared but not yet implemented at
* runtime, and the reference extension must keep working in that case. As
* each namespace lights up on the host, that branch starts returning real
* data without any change here.
*
* Route inference is the fallback when navigation.getPageType() is absent.
*/
import * as core from '@apache-superset/core';
export type PageType =
| 'home'
| 'dashboard'
| 'dashboard_list'
| 'chart'
| 'chart_list'
| 'sqllab'
| 'query_history'
| 'saved_queries'
| 'dataset'
| 'dataset_list'
| 'unknown';
export interface PageContext {
pageType: PageType;
dashboard?: unknown;
chart?: unknown;
dataset?: unknown;
sqlLab?: { tabId: string; title: string };
href: string;
}
const tryCall = <T>(fn: () => T | undefined): T | undefined => {
try {
return fn();
} catch {
return undefined;
}
};
const inferPageType = (pathname: string): PageType => {
if (pathname.startsWith('/sqllab/history')) return 'query_history';
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
if (pathname.startsWith('/sqllab')) return 'sqllab';
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
if (
pathname.startsWith('/superset/dashboard') ||
pathname.startsWith('/dashboard')
)
return 'dashboard';
if (pathname.startsWith('/chart/list')) return 'chart_list';
if (pathname.startsWith('/explore') || pathname.startsWith('/chart'))
return 'chart';
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
if (pathname.startsWith('/tablemodelview') || pathname.startsWith('/dataset'))
return 'dataset';
if (pathname === '/' || pathname.startsWith('/superset/welcome'))
return 'home';
return 'unknown';
};
const readSqlLabTab = (): PageContext['sqlLab'] => {
const tab = tryCall(() => (core as any).sqlLab?.getCurrentTab?.());
return tab ? { tabId: tab.id, title: tab.title } : undefined;
};
const readPageType = (pathname: string): PageType => {
const fromNav = tryCall(() => (core as any).navigation?.getPageType?.());
return (fromNav as PageType | undefined) ?? inferPageType(pathname);
};
/**
* Subscribe to page-context changes and invoke `onChange` whenever any part of
* the context may have changed. Returns a cleanup function.
*
* Three classes of change are watched:
* - Navigation (`navigation.onDidChangePage`, or `popstate` as a fallback for
* hosts without the namespace) — the user moved to a different surface.
* - Entity hydration (`explore.onDidChangeChart`, `dashboard.onDidChangeDashboard`,
* `dataset.onDidChangeDataset`) — the surface's entity loaded or changed
* *after* navigation settled. This matters because a surface (notably Explore)
* can finish hydrating several seconds after the route change fires, so a
* navigation-only subscription would read empty entity context and never
* refresh once the real data arrives.
* - In-surface SQL Lab changes (`sqlLab.onDidChangeActiveTab`,
* `sqlLab.onDidChangeTabTitle`) — switching or renaming a tab does not change
* the route, so without these the panel would keep showing the first tab.
*/
export const subscribeToPageChanges = (onChange: () => void): (() => void) => {
const disposers: Array<() => void> = [];
const nav = tryCall(() => (core as any).navigation);
if (nav?.onDidChangePage) {
const sub = nav.onDidChangePage(onChange);
disposers.push(() => sub.dispose());
} else {
window.addEventListener('popstate', onChange);
disposers.push(() => window.removeEventListener('popstate', onChange));
}
// Entity-context change events. Each is optional — a host may not implement a
// given namespace yet — so subscribe defensively and collect any disposer.
const subscribeEntity = (
getNamespace: () => any,
method: string,
): void => {
const sub = tryCall(() => getNamespace()?.[method]?.(onChange));
if (sub?.dispose) {
disposers.push(() => sub.dispose());
}
};
subscribeEntity(() => (core as any).explore, 'onDidChangeChart');
subscribeEntity(() => (core as any).dashboard, 'onDidChangeDashboard');
subscribeEntity(() => (core as any).dataset, 'onDidChangeDataset');
// SQL Lab tab switches/renames happen without a route change.
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeActiveTab');
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeTabTitle');
return () => disposers.forEach(dispose => dispose());
};
export const getPageContext = (): PageContext => {
const { pathname, href } =
typeof window !== 'undefined'
? window.location
: { pathname: '', href: '' };
return {
pageType: readPageType(pathname),
dashboard: tryCall(() => (core as any).dashboard?.getCurrentDashboard?.()),
chart: tryCall(() => (core as any).explore?.getCurrentChart?.()),
dataset: tryCall(() => (core as any).dataset?.getCurrentDataset?.()),
sqlLab: readSqlLabTab(),
href,
};
};

View File

@@ -1,57 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Module-scoped open/closed state plus a tiny emitter the UI subscribes to.
*
* Lives entirely inside the extension — never reaches into the host store.
* Reset on dispose so re-activation starts cleanly.
*/
export type OpenStateListener = (open: boolean) => void;
let open = false;
const listeners = new Set<OpenStateListener>();
export const isOpen = (): boolean => open;
export const setOpen = (next: boolean): void => {
if (next === open) return;
open = next;
listeners.forEach(fn => {
try {
fn(open);
} catch {
// A listener throwing must not block other listeners or flip our state back.
}
});
};
export const subscribe = (fn: OpenStateListener): (() => void) => {
listeners.add(fn);
return () => {
listeners.delete(fn);
};
};
/** Drains listeners and resets state. Called from the master Disposable. */
export const resetState = (): void => {
open = false;
listeners.clear();
};

View File

@@ -1,73 +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.
*/
/**
* Mock streaming reply used to validate stream teardown semantics.
*
* The reference chatbot is environment-validation only — there is no LLM.
* This iterator yields canned tokens on a timer and exits cleanly when its
* AbortSignal is fired. Disposal of the extension aborts any in-flight
* controller, which is the contract that proves async cancellation works.
*/
const TICK_MS = 40;
const buildReply = (prompt: string): string => {
const trimmed = prompt.trim();
if (!trimmed) {
return 'Reference chatbot online. Send a message to validate streaming.';
}
return (
`[reference-chatbot] received "${trimmed}". ` +
'Streaming token-by-token to validate cancellation and teardown.'
);
};
const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException('aborted', 'AbortError'));
return;
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new DOMException('aborted', 'AbortError'));
};
signal.addEventListener('abort', onAbort, { once: true });
});
export async function* streamReply(
prompt: string,
signal: AbortSignal,
): AsyncIterableIterator<string> {
const tokens = buildReply(prompt).split(/(\s+)/);
for (const token of tokens) {
if (signal.aborted) return;
try {
await sleep(TICK_MS, signal);
} catch {
return;
}
yield token;
}
}

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"target": "es2019",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "es2019"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/__tests__"]
}

View File

@@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@apache-superset/core": ["src/__tests__/sdkMock.ts"]
},
"typeRoots": [
"./node_modules/@types",
"../../superset-frontend/node_modules/@types"
],
"types": ["jest", "node"]
},
"include": ["src"],
"exclude": []
}

View File

@@ -1,108 +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.
*/
const path = require('path');
const fs = require('fs');
const { ModuleFederationPlugin } = require('webpack').container;
const packageConfig = require('./package.json');
const extensionConfig = require('./extension.json');
const MODULE_FEDERATION_NAME = 'apacheSuperset_altChatbot';
/**
* Emits the `manifest.json` the host reads from the extension `dist/` root.
*
* The host (`superset/extensions/utils.py`) expects an extension dist laid out
* as `dist/manifest.json` plus the federated bundle under `dist/frontend/dist/`.
* The manifest carries `extension.json` verbatim, plus the composite `id` and a
* `frontend` block naming the content-hashed `remoteEntry` so the host can load
* the right file. Because the hash is only known after the build, the manifest
* is written from the final asset names rather than checked in.
*/
class EmitManifestPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tap('EmitManifestPlugin', compilation => {
const assets = Object.keys(compilation.assets);
const remoteEntry = assets.find(name => /^remoteEntry\..*\.js$/.test(name));
if (!remoteEntry) {
throw new Error('EmitManifestPlugin: no remoteEntry asset was emitted');
}
const manifest = {
...extensionConfig,
id: `${extensionConfig.publisher}.${extensionConfig.name}`,
frontend: {
remoteEntry,
moduleFederationName: MODULE_FEDERATION_NAME,
},
};
fs.writeFileSync(
path.resolve(__dirname, 'dist', 'manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`,
);
});
}
}
module.exports = (env, argv) => {
const isProd = argv.mode === 'production';
return {
entry: isProd ? {} : './src/index.tsx',
mode: isProd ? 'production' : 'development',
devtool: isProd ? false : 'eval-cheap-module-source-map',
devServer: {
port: 3031,
headers: { 'Access-Control-Allow-Origin': '*' },
},
output: {
clean: true,
filename: isProd ? undefined : '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist', 'frontend', 'dist'),
publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`,
},
resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
externalsType: 'window',
externals: { '@apache-superset/core': 'superset' },
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
],
},
plugins: [
new ModuleFederationPlugin({
name: MODULE_FEDERATION_NAME,
filename: 'remoteEntry.[contenthash].js',
exposes: { './index': './src/index.tsx' },
shared: {
react: {
singleton: true,
requiredVersion: packageConfig.peerDependencies.react,
import: false,
},
'react-dom': {
singleton: true,
requiredVersion: packageConfig.peerDependencies['react-dom'],
import: false,
},
},
}),
new EmitManifestPlugin(),
],
};
};

View File

@@ -15,7 +15,7 @@
# limitations under the License.
#
apiVersion: v2
appVersion: "5.0.0"
appVersion: "6.1.0"
description: Apache Superset is a modern, enterprise-ready business intelligence web application
name: superset
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.16.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.15.5](https://img.shields.io/badge/Version-0.15.5-informational?style=flat-square)
![Version: 0.16.0](https://img.shields.io/badge/Version-0.16.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -64,7 +64,7 @@ dependencies = [
"holidays>=0.45, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.6.1, <2",
"jsonpath-ng>=1.8.0, <2",
"Mako>=1.2.2",
"markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
@@ -89,12 +89,12 @@ dependencies = [
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash",
"pyarrow>=16.1.0, <21", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
"pyarrow>=24.0.0, <25", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
"pyyaml>=6.0.0, <7.0.0",
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.14.0, <5.0",
"selenium>=4.44.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
@@ -107,9 +107,9 @@ dependencies = [
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
"wtforms>=2.3.3, <4",
"wtforms>=3.2.2, <4",
"wtforms-json",
"xlsxwriter>=3.0.7, <3.3",
"xlsxwriter>=3.2.9, <3.3",
]
[project.optional-dependencies]
@@ -121,7 +121,7 @@ bigquery = [
"sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [
@@ -147,7 +147,6 @@ exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
"joserfc>=1.0.0,<2.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
@@ -155,18 +154,18 @@ fastmcp = [
]
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
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",
"tableschema",
"thrift>=0.14.1, <1.0.0",
"thrift>=0.23.0, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
mssql = ["pymssql>=2.2.8, <3"]
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
@@ -181,7 +180,7 @@ ocient = [
oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.37.0, <2"]
playwright = ["playwright>=1.60.0, <2"]
postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"]
@@ -196,20 +195,20 @@ spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.14.1, <1",
"thrift>=0.23.0, <1",
]
tdengine = [
"taospy>=2.7.21",
"taos-ws-py>=0.3.8"
"taos-ws-py>=0.6.9"
]
teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.5.9, < 0.7"]
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.0.0"]
doris = ["pydoris>=1.0.0, <2.0.0"]
oceanbase = ["oceanbase_py>=0.0.1"]
ydb = ["ydb-sqlalchemy>=0.1.2"]
oceanbase = ["oceanbase_py>=0.0.1.2"]
ydb = ["ydb-sqlalchemy>=0.1.2", "ydb-sqlglot-plugin>=0.2.5"]
development = [
# no bounds for apache-superset-extensions-cli until a stable version
"apache-superset-extensions-cli",
@@ -232,7 +231,7 @@ development = [
"pytest-asyncio",
"pytest-cov",
"pytest-mock",
"python-ldap>=3.4.4",
"python-ldap>=3.4.7",
"ruff",
"sqloxide",
"statsd",
@@ -448,6 +447,7 @@ requirement_txt_file = "requirements/base.txt"
authorized_licenses = [
"academic free license (afl)",
"any-osi",
"apache-2.0",
"apache license 2.0",
"apache software",
"apache software, bsd",
@@ -457,6 +457,7 @@ authorized_licenses = [
"isc license (iscl)",
"isc license",
"mit",
"mit and psf-2.0",
"mit-cmu",
"mozilla public license 2.0 (mpl 2.0)",
"osi approved",

View File

@@ -30,7 +30,7 @@ cryptography>=46.0.7,<47.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.3.11,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
pyarrow>=20.0.0,<21.0.0
pyarrow>=24.0.0,<25.0.0
# Security: CVE-2026-27459 - pyopenssl certificate validation
pyopenssl>=26.0.0,<27.0.0
# Security: CVE-2026-25645 (MEDIUM) - Insecure Temporary File

View File

@@ -50,7 +50,7 @@ cattrs==25.1.1
# via requests-cache
celery==5.5.2
# via apache-superset (pyproject.toml)
certifi==2025.6.15
certifi==2026.5.20
# via
# requests
# selenium
@@ -161,7 +161,7 @@ geopy==2.4.1
# via apache-superset (pyproject.toml)
google-auth==2.43.0
# via shillelagh
greenlet==3.1.1
greenlet==3.5.0
# via
# apache-superset (pyproject.toml)
# shillelagh
@@ -194,7 +194,7 @@ jinja2==3.1.6
# via
# flask
# flask-babel
jsonpath-ng==1.7.0
jsonpath-ng==1.8.0
# via apache-superset (pyproject.toml)
jsonschema==4.23.0
# via
@@ -286,15 +286,13 @@ pillow==12.2.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2
# via apache-superset (pyproject.toml)
prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.51
# via click-repl
pyarrow==20.0.0
pyarrow==24.0.0
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -380,7 +378,7 @@ rpds-py==0.25.0
# referencing
rsa==4.9.1
# via google-auth
selenium==4.32.0
selenium==4.44.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
@@ -423,7 +421,7 @@ sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.10.0
# via apache-superset (pyproject.toml)
trio==0.30.0
trio==0.33.0
# via
# selenium
# trio-websocket
@@ -480,7 +478,7 @@ wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket
wtforms==3.2.1
wtforms==3.2.2
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
@@ -490,7 +488,7 @@ wtforms-json==0.3.5
# via apache-superset (pyproject.toml)
xlrd==2.0.1
# via pandas
xlsxwriter==3.0.9
xlsxwriter==3.2.9
# via
# apache-superset (pyproject.toml)
# pandas

View File

@@ -112,7 +112,7 @@ celery==5.5.2
# via
# -c requirements/base-constraint.txt
# apache-superset
certifi==2025.6.15
certifi==2026.5.20
# via
# -c requirements/base-constraint.txt
# httpcore
@@ -183,7 +183,6 @@ cryptography==46.0.7
# -c requirements/base-constraint.txt
# apache-superset
# authlib
# joserfc
# paramiko
# pyjwt
# pyopenssl
@@ -332,7 +331,7 @@ geopy==2.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
gevent==24.2.1
gevent==26.4.0
# via apache-superset
google-api-core==2.23.0
# via
@@ -374,7 +373,7 @@ googleapis-common-protos==1.66.0
# via
# google-api-core
# grpcio-status
greenlet==3.1.1
greenlet==3.5.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -472,9 +471,7 @@ jmespath==1.1.0
# via
# boto3
# botocore
joserfc==1.6.8
# via apache-superset
jsonpath-ng==1.7.0
jsonpath-ng==1.8.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -677,10 +674,6 @@ platformdirs==4.3.8
# virtualenv
pluggy==1.5.0
# via pytest
ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0
# via apache-superset
polyline==2.0.2
@@ -718,7 +711,7 @@ psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
# via fastmcp
pyarrow==20.0.0
pyarrow==24.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -841,7 +834,7 @@ python-dotenv==1.2.2
# apache-superset
# fastmcp
# pydantic-settings
python-ldap==3.4.5
python-ldap==3.4.7
# via apache-superset
python-multipart==0.0.29
# via mcp
@@ -928,7 +921,7 @@ s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.32.0
selenium==4.44.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1026,7 +1019,7 @@ tqdm==4.67.1
# prophet
trino==0.330.0
# via apache-superset
trio==0.30.0
trio==0.33.0
# via
# -c requirements/base-constraint.txt
# selenium
@@ -1128,7 +1121,7 @@ wsproto==1.2.0
# via
# -c requirements/base-constraint.txt
# trio-websocket
wtforms==3.2.1
wtforms==3.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1143,7 +1136,7 @@ xlrd==2.0.1
# via
# -c requirements/base-constraint.txt
# pandas
xlsxwriter==3.0.9
xlsxwriter==3.2.9
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -55,10 +55,21 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
&& mv messages.pot.tmp superset/translations/messages.pot
# --no-fuzzy-matching: when a *new* source string is added, Babel's fuzzy
# matcher otherwise guesses a "close" existing translation and marks it
# `#, fuzzy` in every language catalog. Those guesses are (a) usually wrong
# (e.g. a new "valuename" string mapped onto an unrelated "table name"
# translation) and (b) counted by check_translation_regression.py as a
# regression, so every PR that merely adds a translatable string failed the
# babel-extract check. Disabling fuzzy matching means new strings land as
# cleanly untranslated (empty msgstr) instead — accurate, and no spurious
# regression. Renames likewise drop the stale translation rather than
# stranding a wrong guess; the string is re-translated by the community.
pybabel update \
-i superset/translations/messages.pot \
-d superset/translations \
--ignore-obsolete
--ignore-obsolete \
--no-fuzzy-matching
# Chop off last blankline from po/pot files, see https://github.com/python-babel/babel/issues/799
for file in $( find superset/translations/** );

View File

@@ -18,14 +18,32 @@
"""
Check that source-code changes don't cause translation regressions.
What counts as a regression
---------------------------
A regression is an *existing translation that a source change invalidated*.
The check keys on the **increase in fuzzy entries** rather than a drop in the
translated count, because a count drop happens identically for a benign
*deletion* and a real *rename*, so it cannot distinguish the two — whereas a
``#, fuzzy`` marker unambiguously flags a stranded translation.
Note ``babel_update.sh`` runs ``pybabel update`` with ``--no-fuzzy-matching``,
so *adding* (or renaming) a source string does **not** auto-generate a fuzzy
guess against an unrelated existing translation — new strings land as cleanly
untranslated (empty ``msgstr``). This deliberately avoids the prior behaviour
where *every* PR that merely added a translatable string tripped this check on
spurious fuzzies. As a result the check now guards against ``#, fuzzy`` entries
that arrive another way — e.g. a committed ``.po`` edit — rather than ones the
update step synthesises. *Deleting* a string is still not a regression: with
``--ignore-obsolete`` it is simply dropped and no fuzzy is created.
Usage
-----
Count non-fuzzy translated entries in all .po files and write JSON to stdout:
Count translated + fuzzy entries in all .po files and write JSON to stdout:
python check_translation_regression.py --count
Compare the current .po state against a previously-recorded baseline and fail
if any language lost translations:
if a source change invalidated existing translations (new fuzzies):
python check_translation_regression.py --compare /path/to/before.json
@@ -50,8 +68,8 @@ Typical CI workflow
Running babel_update on the base branch first isolates regressions caused by
the PR's source diff from any pre-existing drift on the base branch, while the
PR worktree run still allows committed .po updates to restore lost
translations.
PR worktree run still allows committed .po updates to resolve the fuzzies (and
thus clear the regression) before merging.
"""
import argparse
@@ -71,8 +89,13 @@ DEFAULT_TRANSLATIONS_DIR = (
SKIP_LANGS = {"en"}
def count_translated(po_file: Path) -> int:
"""Return the number of non-fuzzy translated messages in a .po file.
def count_stats(po_file: Path) -> dict[str, int]:
"""Return ``{"translated": int, "fuzzy": int}`` for a .po file.
``translated`` is the number of non-fuzzy translated messages; ``fuzzy`` is
the number of fuzzy translations. The fuzzy count is what the regression
check keys on — a source rename invalidates an existing translation by
making it fuzzy, whereas a deletion simply drops it (``--ignore-obsolete``).
Raises:
subprocess.CalledProcessError: if ``msgfmt`` fails (e.g. malformed
@@ -90,29 +113,50 @@ def count_translated(po_file: Path) -> int:
check=True,
)
# stderr: "123 translated messages, 4 fuzzy translations, 56 untranslated messages."
match = re.search(r"(\d+) translated message", result.stderr)
if not match:
# The fuzzy and untranslated clauses are omitted by msgfmt when they are 0.
translated_match = re.search(r"(\d+) translated message", result.stderr)
if not translated_match:
raise RuntimeError(
f"Could not parse msgfmt --statistics output for {po_file}: "
f"{result.stderr!r}"
)
return int(match.group(1))
fuzzy_match = re.search(r"(\d+) fuzzy translation", result.stderr)
return {
"translated": int(translated_match.group(1)),
"fuzzy": int(fuzzy_match.group(1)) if fuzzy_match else 0,
}
def get_counts(translations_dir: Path) -> dict[str, int]:
counts: dict[str, int] = {}
def get_counts(
translations_dir: Path,
failures: Optional[set[str]] = None,
) -> dict[str, dict[str, int]]:
"""Count translated/fuzzy entries for every ``.po`` file in a directory.
If ``failures`` is provided, the name of each language whose ``.po`` file
is present on disk but could not be counted (msgfmt non-zero exit, or
unparseable output) is added to it. Such a language is deliberately absent
from the returned mapping — but, unlike a language whose catalog was simply
deleted, it must not be mistaken for an intentional removal: a caller that
cares about the distinction (see :func:`cmd_compare`) can inspect
``failures`` and treat it as a hard error.
"""
counts: dict[str, dict[str, int]] = {}
for po_file in sorted(translations_dir.glob("*/LC_MESSAGES/messages.po")):
lang = po_file.parent.parent.name
if lang in SKIP_LANGS:
continue
try:
counts[lang] = count_translated(po_file)
counts[lang] = count_stats(po_file)
except (subprocess.CalledProcessError, RuntimeError) as exc:
# A malformed .po file (msgfmt non-zero exit, or stderr we
# can't parse) is a real problem worth seeing, but it shouldn't
# take the whole regression check down with it — that would
# hide every other language's status. Skip and warn instead;
# the missing lang will not appear in the comparison output.
# hide every other language's status. Skip and warn here; the
# caller is told which langs failed via ``failures`` so it can
# decide whether a present-but-uncountable catalog is fatal.
if failures is not None:
failures.add(lang)
print(
f"WARNING: skipping {lang}{po_file} could not be counted: {exc}",
file=sys.stderr,
@@ -120,18 +164,42 @@ def get_counts(translations_dir: Path) -> dict[str, int]:
return counts
def _normalize(entry: object) -> dict[str, int]:
"""Coerce a baseline entry into ``{"translated", "fuzzy"}``.
Tolerates the legacy baseline format where each language mapped directly to
an integer translated count (no fuzzy data); such entries contribute a
fuzzy baseline of 0.
"""
if isinstance(entry, dict):
return {
"translated": int(entry.get("translated", 0)),
"fuzzy": int(entry.get("fuzzy", 0)),
}
if isinstance(entry, int):
return {"translated": entry, "fuzzy": 0}
raise TypeError(f"Unsupported baseline entry: {entry!r}")
def build_regression_report(regressions: list[tuple[str, int, int]]) -> str:
"""Build a markdown report for posting as a PR comment."""
"""Build a markdown report for posting as a PR comment.
Each regression tuple is ``(lang, before_fuzzy, after_fuzzy)``.
"""
rows = "\n".join(
f"| `{lang}` | {b} | {a} | -{b - a} |" for lang, b, a in regressions
f"| `{lang}` | {b} | {a} | +{a - b} |" for lang, b, a in regressions
)
affected = ", ".join(f"`{lang}`" for lang, _, _ in regressions)
return (
"## ⚠️ Translation Regression Detected\n\n"
f"This PR causes existing translations to become fuzzy or be removed "
f"in {affected}. Please fix the affected `.po` files before merging.\n\n"
"| Language | Before | After | Lost |\n"
"|----------|-------:|------:|-----:|\n"
f"A source change in this PR renamed or reworded strings, invalidating "
f"existing translations (they are now `#, fuzzy`) in {affected}. Please "
f"resolve the affected `.po` files before merging.\n\n"
"_Note: intentionally **deleting** a translatable string is not a "
"regression and is not flagged here — only translations invalidated by "
"a renamed/reworded source string are._\n\n"
"| Language | Fuzzy before | Fuzzy after | New |\n"
"|----------|-------------:|------------:|----:|\n"
f"{rows}\n\n"
"### How to fix\n\n"
"**1. Install dependencies** (if not already set up):\n\n"
@@ -169,26 +237,49 @@ def cmd_compare(
report_path: Optional[str] = None,
) -> None:
with open(before_path) as f:
before: dict[str, int] = json.load(f)
before_raw: dict[str, object] = json.load(f)
before = {lang: _normalize(entry) for lang, entry in before_raw.items()}
after = get_counts(translations_dir)
failures: set[str] = set()
after = get_counts(translations_dir, failures=failures)
# A baseline language whose catalog is *missing* from `after` is fine —
# that's an intentional catalog deletion (handled below like any other
# deletion). But a language whose .po file is still present yet could not
# be counted (msgfmt failed / output unparseable) is a hard error: leaving
# it out silently would let a corrupt catalog pass as "no regression".
broken = sorted(lang for lang in failures if lang in before)
if broken:
print("Translation check failed!\n")
for lang in broken:
print(f" {lang}: catalog present but could not be counted (msgfmt error)")
print(
"\nFix the malformed .po file(s) above before merging — a catalog "
"that cannot be parsed must not be silently dropped."
)
sys.exit(1)
# A regression is an *increase* in fuzzy entries: the PR's source diff
# renamed/reworded strings, leaving their committed translations stranded.
# A plain drop in the translated count is NOT used — deleting a string
# lowers it identically to a rename but is a legitimate change, and with
# `pybabel update --ignore-obsolete` a deletion creates no fuzzy entry.
regressions: list[tuple[str, int, int]] = []
for lang, before_count in sorted(before.items()):
after_count = after.get(lang, 0)
if after_count < before_count:
regressions.append((lang, before_count, after_count))
for lang, before_stats in sorted(before.items()):
after_stats = after.get(lang, {"translated": 0, "fuzzy": 0})
if after_stats["fuzzy"] > before_stats["fuzzy"]:
regressions.append((lang, before_stats["fuzzy"], after_stats["fuzzy"]))
if regressions:
print("Translation regression detected!\n")
for lang, b, a in regressions:
lost = b - a
print(f" {lang}: {b} -> {a} (-{lost} string(s) became fuzzy or removed)")
print(
f" {lang}: {a - b} translation(s) invalidated "
f"(fuzzy {b} -> {a}) by a renamed/reworded source string"
)
print(
"\nStrings renamed or deleted by this PR invalidated existing translations."
)
print(
"Update the affected .po files to restore the lost entries before merging."
"\nResolve the newly-fuzzy entries in the affected .po files "
"before merging."
)
if report_path:
Path(report_path).write_text(
@@ -199,15 +290,15 @@ def cmd_compare(
# All good — print a summary so it's easy to read in CI logs.
print("No translation regressions.\n")
for lang in sorted(after):
b = before.get(lang, 0)
a = after[lang]
if a > b:
delta = f"+{a - b}"
elif a == b:
delta = "no change"
else:
delta = f"-{b - a}"
print(f" {lang}: {b} -> {a} ({delta})")
before_stats = before.get(lang, {"translated": 0, "fuzzy": 0})
after_stats = after[lang]
t_delta = after_stats["translated"] - before_stats["translated"]
f_delta = after_stats["fuzzy"] - before_stats["fuzzy"]
print(
f" {lang}: translated {before_stats['translated']} -> "
f"{after_stats['translated']} ({t_delta:+d}), fuzzy "
f"{before_stats['fuzzy']} -> {after_stats['fuzzy']} ({f_delta:+d})"
)
def main() -> None:

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