Compare commits

...

223 Commits

Author SHA1 Message Date
Enzo Martellucci
d42da24365 test(extensions): reference chatbots consume the page-context taxonomy
Local testing scaffolding for the chatbot context work — not intended for the
upstream PR (it lives on the test/chatbot-local branch).

- chat (Reference Chatbot): subscribe to entity- and SQL-Lab-context change
  events (onDidChangeChart/Dashboard/Dataset, onDidChangeActiveTab/TabTitle) so
  the panel refreshes after late hydration and on in-surface changes, not only
  on navigation; add chart_list/dashboard_list/dataset_list/query_history/
  saved_queries to the consumer PageType + inference; render the full per-surface
  context vertically.
- chat2 (Alt Chatbot): scaffold real source (previously a prebuilt dist only)
  mirroring chat with its own identity — alt-chatbot id, apacheSuperset_altChatbot
  federation name, "Alt Chatbot" green UI, view-only (no command registration, to
  avoid colliding with Reference's core.chatbot__* ids), open/close via local
  state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:28:10 +02:00
Enzo Martellucci
92224ae270 feat(extensions): refine page-context surface taxonomy for chatbot
Extend the navigation PageType taxonomy so context-consuming extensions can
distinguish browse/list and SQL Lab sub-surfaces, and fix two SQL Lab context
bugs surfaced by it.

Navigation:
- Add chart_list, dashboard_list, dataset_list, query_history and saved_queries
  to PageType, and classify the matching routes in derivePageType (list pages
  and /sqllab/history, /savedqueryview/list). List/sub-pages previously
  collapsed into 'other', and /sqllab/history was mislabeled 'sqllab'.

SQL Lab:
- onDidChangeActiveTab now resolves the active tab via getCurrentTab() instead
  of getTab(action.queryEditor.id). The action payload's editor has no merged
  unsaved dbId yet, so the old parser returned undefined and the event was
  silently swallowed, leaving consumers stuck on the first tab.
- getCurrentTab() now guards on navigation.getPageType() === 'sqllab', so the
  SQL Lab tab no longer leaks onto non-editor surfaces (the slice persists
  after navigating away). Mirrors the explore/dashboard getter guards.

Tests cover the new page types, the switch-away tab event, and the off-surface
getCurrentTab guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:27:27 +02:00
Enzo Martellucci
87956741ff chore: updates chatbot sip 2026-06-05 16:10:01 +02:00
Enzo Martellucci
8efaf38a2f fix(extensions): repair reference-chatbot build so served bundle isn't stale 2026-06-05 11:48:56 +02:00
Enzo Martellucci
979e01e7fb chore: updates SIP 2026-06-04 22:44:04 +02:00
Enzo Martellucci
e2a971ef69 fix: lint 2026-06-04 11:57:27 +02:00
Enzo Martellucci
23f6133983 fix(extensions): enforce CSRF protection on ExtensionsRestApi
FAB's BaseApi defaults csrf_exempt to True, so ExtensionsRestApi — which uses
cookie/session auth (allow_browser_login) and exposes state-changing routes
(settings PUT, extension upload POST, delete) — was silently exempt from CSRF
protection. Superset's own BaseSupersetApi sets csrf_exempt = False for exactly
this reason; mirror that here. Fixes test_csrf_exempt_blueprints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:39:41 +02:00
Enzo Martellucci
be364bb093 test(extensions): register POST/DELETE routes via ENABLE_EXTENSIONS flag
The extension upload/delete endpoints are only mounted when ENABLE_EXTENSIONS
is enabled at app-init time, so the endpoint tests 404'd depending on test
ordering. Parametrize the app fixture on TestPostEndpoint/TestDeleteEndpoint
so the routes are registered deterministically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:36:23 +02:00
Enzo Martellucci
40dcace5d0 test(extensions): register settings route via ENABLE_EXTENSIONS flag
The settings endpoints are only mounted when ENABLE_EXTENSIONS is enabled at
app-init time. The endpoint tests relied on another test enabling the flag
first, so they 404'd in CI's ordering. Parametrize the app fixture on both
endpoint test classes so the route is registered deterministically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:55:34 +02:00
Enzo Martellucci
74f9b72f64 refactor(extensions): route settings persistence through Command + DAO
Addresses review feedback that the settings endpoints bypassed the
Command->DAO pattern used across the codebase.

- superset/daos/extension.py: ExtensionSettingsDAO / ExtensionEnabledDAO
  (BaseDAO subclasses). Upserts use a portable check-then-write path so all
  metadata backends work without dialect-specific SQL or NotImplementedError.
- superset/commands/extension/settings/: Get/Update commands. UpdateCommand
  validates the payload (rejects non-string/oversized active_chatbot_id and
  oversized/non-string enabled keys) before any write, and wraps writes in
  @transaction so DB errors surface as ExtensionSettingsUpdateFailedError.
- api.py now constructs and runs the commands; pre-validates so malformed
  input returns 400 (the FAB @safe wrapper would otherwise yield 500).
- Remove superset/extensions/settings.py; tests rewritten against the
  Command + DAO layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:23:00 +02:00
Enzo Martellucci
579c8d8377 fix(extensions): validate settings payload and harden chatbot resolver
Addresses remaining PR review comments:

- Reject invalid active_chatbot_id types (e.g. ints) and oversized ids with
  a 400 instead of silently coercing to null / failing at the DB layer.
- Reject oversized / non-string enabled-map keys with a 400 before the DB
  enforces the column length.
- Share the id column length via EXTENSION_ID_MAX_LENGTH so validation and
  the schema cannot drift.
- Chatbot resolver now falls through to the next candidate when a view id
  fails to resolve, instead of returning undefined on the first miss.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:03:09 +02:00
Enzo Martellucci
369e4adc76 feat(extensions): restore dataset context namespace with a real producer
Re-introduce the `dataset` namespace end-to-end so chatbot extensions can read
which dataset the user is viewing/editing. It was previously removed because the
SDK exported a typed contract with no runtime producer (calls threw); this adds
the producer so the contract is backed.

- SDK: DatasetContext + getCurrentDataset/onDidChangeDataset, barrel export, and
  the "./dataset" package.json subpath.
- Host: src/core/dataset with setCurrentDataset (producer) + getter + change
  event; exported from src/core and registered on window.superset.
- Producer: the dataset edit page (EditPage) fetches the dataset and publishes
  { datasetId, datasetName, schema, catalog, databaseName, isVirtual } via
  setCurrentDataset on load, clearing it on unmount. Fields map per the SIP
  contract; databaseName/schema/catalog are nullable.
- Use a stable module-level error handler for useSingleViewResource so
  fetchResource keeps a fixed identity — an inline handler made the fetch effect
  re-fire every render (Maximum update depth exceeded).
- Add the dataset-entity fetch mock to EditDataset.test, and add useRouter to
  ExtensionsStartup.test renders (it uses useLocation; tests must wrap a Router).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:36:05 +02:00
Enzo Martellucci
76466e3845 adds tickets.md 2026-06-02 16:55:59 +02:00
Enzo Martellucci
0249b8c1b3 fix(extensions): track async registrations via activate(context)
Port the activate(context) lifecycle fix (PR #40441 review feedback) to the
integration branch, and align the chatbot SIP.

Previously deactivateExtension only disposed registrations captured by the
synchronous window.superset registrar-wrapping during module evaluation, so
contributions registered from an async continuation leaked on deactivation.
Extensions now export activate(context) and push each Disposable onto
context.subscriptions, whose lifetime is bound to the context object rather
than a synchronous window — async and synchronous registrations are tracked
alike. The registrar-wrapping is retained as a synchronous-only fallback for
legacy side-effect extensions.

- Add ExtensionContext / ExtensionModule to @apache-superset/core
- loadModule awaits module.activate(context), returns context.subscriptions
- deactivateExtension disposes context.subscriptions
- Fix stale subscribeToLocation comment -> subscribeToRegistry
- Tests: legacy synchronous disposal + async-in-activate tracking
- SIP: describe the activate(context) model; resolve the async-registration
  open item (async dispose-await remains pending)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:07:43 +02:00
Enzo Martellucci
a583f1859c Merge branch 'master' into test/chatbot-local 2026-06-02 12:53:52 +02:00
Enzo Martellucci
6d174fa71f docs(extensions): fix logic errors and code-doc drift in chatbot SIP
- Fix broken getCurrentDashboard example (syntax error, raw nativeFilters/
  slices exposure contradicting the "normalized only" point, wrong field
  names, missing ChartSummary.isVisible)
- Correct page-type vocabulary: `chart` -> `explore` to match PageType
- Stop claiming `icon` is "proposed" — it already exists on the View
  descriptor and the registration example already passes it
- Remove phantom "UI-control state" and navigation "focused entity"
  promises that no contract actually exposes
- Distinguish implemented namespaces (dashboard/explore/navigation) from
  specified-but-not-implemented ones (dataset, DashboardContext.charts)
- Fix malformed `explore` status marker; align getViews descriptor field
  list to include `icon`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:51:08 +02:00
Mehmet Salih Yavuz
e2ed989639 fix(reports): skip permalink when dashboard state has no anchor or filters (#40530) 2026-06-02 11:37:30 +03:00
Kamil Gabryjelski
2abbb64e6b feat(gsheets): restore public/private sheet selector (#40466)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 07:10:21 +02:00
Evan Rusackas
c6faa50338 fix(cli): encrypt sqlalchemy_uri password on import_datasources (#31983) (#40584)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 22:08:48 -07:00
Evan Rusackas
817a35f445 fix(mcp): deny deactivated user accounts in MCP authentication (#40631)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-01 22:08:29 -07:00
Amin Ghadersohi
a6d2c95480 chore(deps): migrate MCP service JWT errors from authlib.jose to joserfc (#40582) 2026-06-02 00:41:17 -04:00
dependabot[bot]
c29591b3b1 chore(deps): update @babel/runtime requirement from ^7.29.2 to ^7.29.7 in /superset-frontend/packages/superset-ui-core (#40606)
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-01 20:11:00 -07:00
dependabot[bot]
365914f1c7 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.4 to 8.60.0 in /superset-websocket (#40595)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 20:10:34 -07:00
dependabot[bot]
41da35e9db chore(deps-dev): bump typescript-eslint from 8.59.4 to 8.60.0 in /docs (#40598)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 17:03:52 -07:00
dependabot[bot]
861e668f74 chore(deps-dev): bump @babel/preset-env from 7.29.5 to 7.29.7 in /superset-frontend (#40601)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 17:03:37 -07:00
dependabot[bot]
41059c68bb chore(deps-dev): bump @babel/plugin-transform-modules-commonjs from 7.28.6 to 7.29.7 in /superset-frontend (#40603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 17:03:30 -07:00
dependabot[bot]
94092d2f72 chore(deps-dev): bump @babel/preset-typescript from 7.28.5 to 7.29.7 in /superset-frontend (#40604)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 17:03:22 -07:00
dependabot[bot]
986148d924 chore(deps-dev): bump typescript-eslint from 8.59.4 to 8.60.0 in /superset-websocket (#40596)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:42:12 -07:00
dependabot[bot]
f04221a06c chore(deps-dev): bump webpack from 5.107.1 to 5.107.2 in /docs (#40597)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:42:05 -07:00
dependabot[bot]
70aa96458a chore(deps-dev): bump @babel/cli from 7.28.6 to 7.29.7 in /superset-frontend (#40599)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:41:51 -07:00
dependabot[bot]
8beea84952 chore(deps-dev): bump @typescript-eslint/parser from 8.59.4 to 8.60.0 in /docs (#40600)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:41:16 -07:00
dependabot[bot]
3f0fbbaac9 chore(deps-dev): update @babel/types requirement from ^7.29.0 to ^7.29.7 in /superset-frontend/plugins/plugin-chart-pivot-table (#40605)
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-01 16:40:36 -07:00
dependabot[bot]
ce602fc5a8 chore(deps-dev): bump @babel/types from 7.29.0 to 7.29.7 in /superset-frontend (#40608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:40:16 -07:00
dependabot[bot]
8731974e5c chore(deps-dev): bump @babel/preset-react from 7.28.5 to 7.29.7 in /superset-frontend (#40609)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:40:10 -07:00
dependabot[bot]
a06eb8fc78 chore(deps): bump react-arborist from 3.7.0 to 3.8.0 in /superset-frontend (#40610)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:39:55 -07:00
dependabot[bot]
aa8b474c58 chore(deps-dev): bump @babel/plugin-transform-export-namespace-from from 7.27.1 to 7.29.7 in /superset-frontend (#40611)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:39:48 -07:00
dependabot[bot]
efdfefeea2 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.4 to 8.60.0 in /superset-frontend (#40613)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:39:38 -07:00
dependabot[bot]
f77fa3ae39 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.29.0 to 7.29.7 in /superset-frontend (#40614)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:39:29 -07:00
dependabot[bot]
bffc3fc58f chore(deps-dev): bump @babel/eslint-parser from 7.28.6 to 7.29.7 in /superset-frontend (#40615)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:39:23 -07:00
Evan Rusackas
2b8e31bf68 ci(docs): skip Netlify docs preview on PRs that don't touch docs (#40590)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-01 19:06:57 -04:00
Ville Brofeldt
74d1c83ec5 ci: preserve PR translation updates during regression checks (#40581) 2026-06-01 15:54:20 -07:00
Enzo Martellucci
15ad31effd updates the chatbot sip 2026-06-01 23:45:04 +02:00
dependabot[bot]
1523d797ca chore(deps-dev): bump pyinstrument from 4.4.0 to 5.1.2 (#40377)
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-01 13:34:12 -07:00
Amin Ghadersohi
97be689b5c fix(daos): lazy-import SoftDeleteMixin to fix pytest collection error (#40573) 2026-06-01 15:09:43 -04:00
Evan Rusackas
dab628c13a fix(i18n): auto-add ASF license header in backfill_po.py (#40395)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 11:53:35 -07:00
Evan Rusackas
e1bc8e7ae8 chore(deps): bump uuid, qs, js-yaml, and @cypress/request in frontend lockfiles (#40561)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-06-01 11:43:24 -07:00
Amin Ghadersohi
5312d0adf8 feat(mcp): add list_reports and get_report_info tools (#40348)
Co-authored-by: Richard Fogaça <richardfogaca@gmail.com>
2026-06-01 14:23:53 -04:00
Michael Gerber
041ecbc248 fix(embedded): resolve guest user permissions in user_view_menu_names (#39197)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-01 10:30:05 -07:00
Joe Li
a33fcb0edd feat: add embedded dashboard E2E tests to Playwright CI (#39300)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 09:42:08 -07:00
Jean Massucatto
316dd3db22 fix(dataset): sort by data type when clicking Datatype column header (#40411) 2026-06-01 09:32:33 -07:00
jesperct
d52e28c564 fix(dashboard): preserve user selection when Dynamic Group By overlaps base (#40031) 2026-06-01 09:24:45 -07:00
jesperct
a2eda11a81 fix(dataset): validate catalog against allow_multi_catalog on import (#40376) 2026-06-01 09:24:28 -07:00
Shaitan
63249c8c97 fix(chart): add regression coverage for UpdateChartCommand ownership (#39997)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:15:38 -07:00
Enzo Martellucci
32c42076dd adds the admin extension list details 2026-06-01 15:08:53 +02:00
Ville Brofeldt
acc80242f0 chore: fix translations (#40564) 2026-05-31 21:43:55 -07:00
Ville Brofeldt
a621520387 fix(ci): repair Python version extraction in uv-pip-compile.sh (#40565) 2026-06-01 00:30:07 +07:00
Hugh A. Miles II
9a79588d35 feat(dashboard): add Rison-encoded URL filter support (#39795)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:06:09 -04:00
Evan Rusackas
7e8b8e25a5 chore(ci): resolve remaining GitHub Actions static-analysis findings (#40556)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-31 17:55:26 +07:00
Evan Rusackas
ab5ea1f7d3 chore(deps): force d3-color 3.1.0 in docs (#40541)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-31 00:33:05 -07:00
Evan Rusackas
a340293fef chore(deps): bump idna, Authlib, fontTools, python-ldap, virtualenv (#40560)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-31 11:08:27 +07:00
Evan Rusackas
469979714f chore(deps): force serialize-javascript 7.0.5 in docs (#40540)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-31 10:54:45 +07:00
Evan Rusackas
585745695c fix(chart): load viz_type from request params when creating chart via API (#40536)
Co-authored-by: hdahme <harrison.dahme@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:44:09 +07:00
Evan Rusackas
c7bbfff475 fix(ci): gate welcome message on first-time contributor association (#40543)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:39:38 +07:00
Shaitan
8e47eb1cc1 docs(security): explicit security model, role/capability matrix, AGENTS.md linkage (#40503)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:27:00 -07:00
Shaitan
afc4f3c9b3 fix(database): extend URI blocklist to cover duckdb dialect (#40402)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:26:38 -07:00
dependabot[bot]
35125d8521 chore(deps-dev): bump syntaqlite from 0.1.0 to 0.5.9 (#40548)
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-05-30 13:24:38 -07:00
dependabot[bot]
ab66f0066a chore(deps-dev): update sqlalchemy-cratedb requirement from <1,>=0.40.1 to >=0.41.0,<1 (#40549)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-30 13:24:21 -07:00
dependabot[bot]
dfa4dbb96c chore(deps-dev): update denodo-sqlalchemy requirement from <2.1.0,>=1.0.6 to >=2.0.5,<2.1.0 (#40550)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-30 13:24:00 -07:00
dependabot[bot]
f10a296ed0 chore(deps): bump nh3 from 0.2.21 to 0.3.5 (#40551)
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-05-30 13:23:28 -07:00
Mateus Trentz
f17e4de9cd fix(superset-frontend): color formats big numbers when value is 0 (#28038)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-30 10:30:22 -07:00
Amin Ghadersohi
87be424f9c feat(mcp): add list and get tools for row level security and plugins (#40347) 2026-05-30 10:41:12 -04:00
Kasia
4d95a8d034 feat(listview): compact filter pills with popover for CRUD views (#40169) 2026-05-30 10:30:40 +02:00
Đỗ Trọng Hải
2d6e68b5f2 fix(ci): remove deprecated ephemeral env workflows + resolve fixable GHA-related security issues (#40121)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-30 14:09:46 +07:00
Evan Rusackas
2e7bec3646 chore(ci): harden GitHub Actions workflows per static analysis (#40545)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-30 13:13:43 +07:00
Evan Rusackas
f4787a4f25 chore(deps): bump ws to 8.20.1 in docs (#40538)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-30 13:08:13 +07:00
Evan Rusackas
fa4e571db5 chore(deps): force uuid 11.1.1 in docs (#40542)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-30 13:04:31 +07:00
Evan Rusackas
838ee27c29 chore(deps): bump protobuf to 5.29.6 and google-cloud-bigquery-storage to 2.26.0 (#40537)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-30 11:49:15 +07:00
Evan Rusackas
7f54b0b13d test(database): regression test for sqla engine creation (#27897) (#40237)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 21:47:49 -07:00
Evan Rusackas
f165c3fa78 fix(ci): grant security-events write to GHA validator workflow (#40539)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-29 21:46:54 -07:00
Evan Rusackas
8c6271e9ff chore(deps): bump urllib3, Mako, and python-multipart for high-severity CVEs (#40534)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-30 11:39:33 +07:00
Evan Rusackas
44a8d9d469 chore(deps): pin lodash, lodash-es, and yaml in docs to patched releases (#40535)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-29 21:26:39 -07:00
Amin Ghadersohi
d350792d43 feat(mcp): add list and get tools for CSS templates and themes (#40343) 2026-05-30 00:20:48 -04:00
Joe Li
c9136af8b6 fix(ci): trigger python dep check when pyproject.toml changes (#39792)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-29 21:13:56 -07:00
David Kopelent
f0838353a5 feat(i18n): add Czech (cs) locale support for dates (#40241)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-29 21:13:52 -07:00
Evan Rusackas
16b56873b0 fix(ci): restrict workflow_run jobs to trusted origins and add zizmor audit (#40533)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-30 11:11:40 +07:00
Amin Ghadersohi
62b4ee3d9e feat(mcp): add list and get tools for users and roles (#40345)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 23:46:30 -04:00
dependabot[bot]
43c3c06035 chore(deps): bump vm2 from 3.11.3 to 3.11.5 in /superset-frontend (#40529)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 20:25:32 -07:00
Amin Ghadersohi
40de44f6de feat(mcp): add list and get tools for action log and tasks (#40344) 2026-05-29 23:16:10 -04:00
Đỗ Trọng Hải
b8ea4448d6 feat(ci): perform static security analysis for GHA workflows (#40510)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-29 20:15:14 -07:00
Amin Ghadersohi
8d8eeb3505 feat(mcp): add list_tags and get_tag_info MCP tools (#40349)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:16:59 -04:00
Amin Ghadersohi
a69bbcb044 feat(mcp): add list and get tools for saved queries and query history (#40346)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:16:11 -04:00
Amin Ghadersohi
f614863ed7 feat(mcp): add list and get tools for annotation layers and annotations (#40342) 2026-05-29 22:14:28 -04:00
Elizabeth Thompson
53e2793bc3 fix: coerce out-of-bounds nanosecond timestamps to NaT instead of raising (#40127)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
2026-05-29 14:32:33 -07:00
dependabot[bot]
e73d2d0bf6 chore(deps): bump tmp from 0.2.4 to 0.2.7 in /superset-frontend/cypress-base (#40476)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-29 13:43:14 -07:00
dependabot[bot]
ae5823fb9c chore(deps-dev): bump tmp from 0.2.5 to 0.2.7 in /superset-frontend (#40480)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-29 13:42:49 -07:00
dependabot[bot]
63e3a18e8f chore(deps): bump baseline-browser-mapping from 2.10.31 to 2.10.32 in /docs (#40484)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:42:32 -07:00
dependabot[bot]
661ff31c6d chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40485)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:42:19 -07:00
dependabot[bot]
ead21d9789 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40486)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:42:04 -07:00
dependabot[bot]
c3bda6baea chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#40487)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:41:50 -07:00
dependabot[bot]
61cb0aeae7 chore(deps): bump @swc/core from 1.15.33 to 1.15.40 in /docs (#40515)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:41:35 -07:00
dependabot[bot]
cf9ee99b5a chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 0.10.1 to 0.10.2 in /superset-frontend (#40516)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:41:22 -07:00
dependabot[bot]
e1948c87c6 chore(deps-dev): bump webpack-sources from 3.4.1 to 3.5.0 in /superset-frontend (#40517)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:41:08 -07:00
dependabot[bot]
63ae80ab62 chore(deps-dev): bump baseline-browser-mapping from 2.10.31 to 2.10.32 in /superset-frontend (#40518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:40:53 -07:00
dependabot[bot]
8f88bbcc79 chore(deps-dev): bump @swc/core from 1.15.33 to 1.15.40 in /superset-frontend (#40519)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 13:40:39 -07:00
Mike Bridge
b2320820b4 feat(core): SoftDeleteMixin and restore infrastructure (#39977)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:08:10 -07:00
Mehmet Salih Yavuz
8853ab5c75 feat(mcp): add get_dashboard_layout companion tool (#40328) 2026-05-29 14:31:16 +03:00
Evan Rusackas
b0da0cf202 chore(ci): update GHA actions to Node.js 24-compatible versions (#40477)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-29 11:12:41 +07:00
dependabot[bot]
96b96ad7d4 chore(deps): bump ws from 8.20.1 to 8.21.0 in /superset-websocket (#40483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 11:10:18 +07:00
Shrey Shekhar
f7e1f96894 fix(i18n): Wrapped default Chat Error message in t() to support translation (#40504) 2026-05-29 11:04:15 +07:00
Nishita Matlani
ec09cec6bd fix(ci): correct first-interaction inputs in welcome workflow (#40508)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-29 10:53:38 +07:00
Shaitan
f663f47628 fix(embedding): require non-default JWT secret when embedded dashboards are enabled (#39999)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 14:26:40 -07:00
Abdul Rehman
8a0026b173 fix(groups): display user full name in role edit user dropdown (#39942)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-28 14:25:45 -07:00
Elizabeth Thompson
f037449b75 fix(roles): resolve HTTP 429 and 414 errors on role management page (#39465)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:34:54 -07:00
Gabriel Torres Ruiz
e68251fa70 feat(mcp): support custom SQL metrics in generate_chart and update_chart (#40448) 2026-05-28 14:52:43 -03:00
Amin Ghadersohi
0dc58d1042 feat(mcp): browser hello page with working middleware and config-driven content (#40471)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:47:38 -04:00
Enzo Martellucci
7f6f805ffa fix(extensions): sync second-round review fixes to chatbot-local branch
- Validate manifest.id segments in POST endpoint before building dest_file path
- Add hostile manifest.id test (../../tmp/evil → 400)
- Add sqlite-backed round-trip tests for settings.py upsert logic
- Add HTTP tests for GET/PUT /api/v1/extensions/settings endpoints
- Wrap handleDelete in useCallback; already in columns useMemo deps
- Fix MySQL comment drift in _upsert_settings_row (read-then-update, not merge)
- Add intentional no-admin-gate comment to get_settings endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:21:30 +02:00
Enzo Martellucci
df6e0095dc fix(extensions): sync PR review fixes to test/chatbot-local
- ExtensionsList.tsx: unique Tooltip ids per row; onKeyDown Enter/Space for
  star and delete role=button spans; data-test attrs; remove stale loading dep
- ExtensionsList.test.tsx: 10 unit tests (import validation, delete confirm,
  star toggle, keyboard a11y, file upload)
- api.py: path-traversal validation, upload size limit, LOCAL_EXTENSIONS 409
- test_api.py: 19 Python unit tests for POST and DELETE endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:05:43 +02:00
Enzo Martellucci
c4b9f7b6e5 fix(extensions): fully remove dataset namespace — SDK contract and orphaned host impl
The host removed dataset from window.superset but the SDK still exported
the typed contract. An extension importing dataset from @apache-superset/core
would get a fully typed namespace whose runtime calls throw at access time,
which is worse than not shipping it.

Removes:
- packages/superset-core/src/dataset/index.ts (SDK type declarations)
- export * as dataset from './dataset' in superset-core/src/index.ts
- "./dataset" subpath from superset-core/package.json exports
- src/core/dataset/index.ts (orphaned host implementation)

The namespace will be re-introduced once a producer (DatasetCreation or
equivalent) calls setCurrentDataset to back the contract at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 09:29:41 +02:00
jesperct
c73106b7a2 fix(chart-echarts): drop white textBorder from Funnel segment labels (#40468) 2026-05-27 23:59:51 -07:00
dependabot[bot]
9441240e5c chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40436)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-27 15:28:56 -07:00
dependabot[bot]
08164e33bb chore(deps): bump d3-cloud from 1.2.8 to 1.2.9 in /superset-frontend (#40437)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-27 15:28:41 -07:00
dependabot[bot]
894058fe3d chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#40438)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-27 15:28:24 -07:00
dependabot[bot]
6bd1b46216 chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 (#40458)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-27 15:28:06 -07:00
Enzo Martellucci
23d4574caf fix(extensions): address context-sharing PR review — blockers, quality, and tests
- Add CREATE_NEW_SLICE and SLICE_UPDATED to exploreChangePredicate so
  onDidChangeChart fires when a chart is saved for the first time
- Remove dataset namespace: no producer exists yet; ships it once a
  caller is wired in DatasetCreation or equivalent
- Remove ...supersetCore spread from window.superset assignment so
  un-contracted symbols from @apache-superset/core are not leaked onto
  the global object; list namespaces explicitly instead
- Add defensive array copy for filter values in buildDashboardContext
  so extension mutations cannot affect Redux state
- Lazy-initialize currentPageType in navigation to avoid module-load
  window.location access (throws in non-browser test environments)
- Fix /sqllab exact-match missing from derivePageType
- Add unit tests: navigation (7), explore (9), dashboard (11) — 27 tests
  covering page-type gating, dispose semantics, predicate coverage, and
  defensive copy invariant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 00:16:37 +02:00
Enzo Martellucci
da4d09a006 fix(extensions): complete model move — remove from core.py, fix import, ensure ORM registration
- Create superset/extensions/models.py with ExtensionSettings and ExtensionEnabled.
- Remove both classes from superset/models/core.py.
- Update superset/extensions/settings.py import to the new path.
- ORM registration is guaranteed by the existing import chain:
  api.py → settings.py → extensions/models.py — no additional import needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:51:38 +02:00
Enzo Martellucci
1645a9652f fix(extensions): address PR review — rollback, import cleanup, tests, guard
- ExtensionsList: snapshot previous settings before optimistic write;
  rollback setSettings + notifyExtensionSettingsChanged in .catch() so a
  failed PUT leaves the UI consistent with server state. Drop
  setSettings(json.result) from .then() — optimistic write is source of
  truth. Switch onClick → onChange. Consolidate Switch/Select/etc into
  single @superset-ui/core/components import.
- ChatbotMount: revert undefined loading gate (immediate render, fall
  back on fetch error); guard json.result with ?? fallback; merge React
  imports; promote ChatbotRenderer comment to JSDoc.
- Tests: add getActiveChatbot coverage for admin-pin, stale-pin fallback,
  enabled-filter exclusion, all-disabled. Add ChatbotMount test for
  provider function throwing synchronously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:36:05 +02:00
Enzo Martellucci
22d9332794 fix(extensions): push settings payload through pub/sub to eliminate re-fetch delay on toggle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:02:36 +02:00
Enzo Martellucci
504826bb24 refactor(extensions): replace per-location pub/sub with registry-wide version counter + useSyncExternalStore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 22:52:57 +02:00
Deva
ef4514f5ab fix(treemap): remove gaps between chart nodes (#40181) 2026-05-27 22:35:06 +03:00
Amin Ghadersohi
e041f25385 fix(mcp): return error when target_tab not found in add_chart_to_existing_dashboard (#40124) 2026-05-27 14:29:43 -04:00
Evan Rusackas
d744f5715c fix(dashboard-export): include and re-attach roles in import/export (#21000) (#40136)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: SBIN2010 <Sbin2010@mail.ru>
2026-05-27 10:49:07 -07:00
Amin Ghadersohi
fb60662353 chore(mcp): revert browser-friendly hello page for GET /mcp from browsers (#40467) 2026-05-27 11:43:01 -04:00
Enzo Martellucci
4e8145f14b fix(extensions): match /sqllab path without trailing slash in derivePageType
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:26:45 +02:00
Onur Taşhan
207a7bf7f9 fix: preserve dashboard certification when saving layout changes (#40193)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 10:03:43 -04:00
Enzo Martellucci
a74684b062 fix(extensions): port CodeAnt fixes — page-type guards, deactivation cleanup, settings race, feature flag gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 12:10:16 +02:00
Enzo Martellucci
96f2fb3659 fix(extensions): defer chatbot render until settings load; isolate provider errors
- Don't call getActiveChatbot before settings arrive — start with null
  so no chatbot is rendered until the admin selection is known, avoiding
  a flash of the wrong chatbot when multiple are registered
- On settings fetch failure fall back to first-to-register instead of
  rendering nothing forever
- Wrap provider call in a ChatbotRenderer child component so
  ErrorBoundary actually catches provider-level throws (calling the
  provider inline during render means errors bubble before the boundary
  can mount)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 09:57:05 +02:00
Amin Ghadersohi
09a94fa26b feat(mcp): return browser-friendly hello page for GET /mcp from browsers (#40309)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 18:52:37 -07:00
Mike Bridge
7e088792b9 test(model): roll back uncommitted ds_col mutations in timestamp-expression tests (#40451)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
2026-05-26 21:17:08 -03:00
Enzo Martellucci
e3efa0ae71 feat(extensions): consolidate actions column and live settings sync
- Single Actions column: switch (enable/disable), star (default chatbot,
  chatbot-type only, toggleable), trash (hidden for LOCAL_EXTENSIONS)
- Add `deletable` field to build_extension_data so the frontend knows
  which extensions can be removed via the UI
- Add `publisher` field to Extension type for correct delete URL
- Add notifyExtensionSettingsChanged / subscribeToExtensionSettings
  pub/sub in core/extensions so ChatbotMount re-fetches on any settings
  change without a page reload
- Wire ChatbotMount to subscribe to settings changes via fetchSettings
  callback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:51:05 +02:00
Enzo Martellucci
97ba65b0cd feat(extensions): port import/delete UI and backend endpoints to test branch
- POST /api/v1/extensions/ — admin upload of .supx bundles
- DELETE /api/v1/extensions/<publisher>/<name> — admin removal
- Import button in ExtensionsList SubMenu (file picker, .supx only)
- Per-row delete action with confirmation dialog
- Keeps existing Select (default chatbot) and Switch (enabled) UI
- Add publisher to build_extension_data response
- Add subscribeToLocation / getRegisteredViewIds / getViewProvider to
  src/core/views/index.ts so ExtensionsList can detect chatbot extensions
- Fix scripts/oxlint.sh set -e / [ -n "" ] false-positive exit
- Fix settings.py MySQL fallback: use read-then-update instead of
  try/except/rollback to satisfy the custom consider-using-transaction rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:30:32 +02:00
Maxime Beauchemin
b6f545e61e feat(mcp): resolve call_tool proxy name and capture error_type in logging (#38915)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-05-26 14:37:37 -04:00
Amin Ghadersohi
952a6f3a23 fix(mcp): prevent encoding error on tools/list when middleware raises (#40446) 2026-05-26 12:50:31 -04:00
Enzo Martellucci
d7592913ea feat(extensions): reference chatbot extension — docs and example
Ships the full extensions/chat reference implementation that exercises
the chatbot extension platform end-to-end: activation lifecycle and
master disposable (teardown contract), React error boundary (fault
isolation), mock streaming with AbortController cancellation, commands
registration, and the pageContext helper that composes host namespaces.

Local branch only — not intended for upstream merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:56:28 +02:00
Enzo Martellucci
8f03c5a1ea feat(extensions): context sharing namespaces (navigation, explore, dashboard, dataset)
Adds four stable namespaces to @apache-superset/core that give extensions
host-managed access to page context without coupling to Redux internals:

- navigation: getPageType(), onDidChangePage (routing signal only)
- explore: getCurrentChart(), onDidChangeChart (ChartContext from Redux)
- dashboard: getCurrentDashboard(), onDidChangeDashboard (DashboardContext
  with active native filter values from Redux)
- dataset: getCurrentDataset(), onDidChangeDataset (push model)

All four are wired into window.superset via ExtensionsStartup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:49:22 +02:00
Enzo Martellucci
8b52fff57b feat(extensions): backend settings persistence and admin-only permissions
Adds ExtensionSettings and ExtensionEnabled models with migration.
GET /api/v1/extensions/settings is public; PUT is restricted to Admin
role via security_manager.is_admin(). Uses dialect-aware ON CONFLICT DO
UPDATE upserts and @transaction() for safe concurrent writes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:49:15 +02:00
Enzo Martellucci
197e14de1b feat(extensions): admin configuration UI for extensions
Adds enable/disable toggles per extension and an active-chatbot selector
(shown when multiple chatbot extensions are registered) to the Extensions
list view. Settings are persisted via PUT /api/v1/extensions/settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:49:07 +02:00
Enzo Martellucci
7c9efd529b feat(extensions): eager-load extensions at app-shell startup
ExtensionsStartup initializes extensions behind the EnableExtensions
feature flag immediately after the user session is confirmed, wires
window.superset, and isolates unhandled rejections from extension code.
ChatbotMount is mounted at the app root via App.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:49:01 +02:00
Enzo Martellucci
f8e7ee9dc2 feat(extensions): define the chatbot entry point in the frontend API
Adds getActiveChatbot() singleton resolver (first-to-register + admin
active_chatbot_id + enabled-flag enforcement), subscribeToLocation() for
reactive re-resolution, and ChatbotMount — the fixed bottom-right slot
that persists across routes and renders the active chatbot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:48:53 +02:00
Enzo Martellucci
c77c9b29f9 feat(extensions): define the superset.chatbot contribution point
Adds the `superset.chatbot` app-level location to ViewContributions and
exports ChatbotView from the contributions namespace. Introduces
src/views/contributions.ts as the host-side CHATBOT_LOCATION constant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:32:42 +02:00
dependabot[bot]
8b551d3f74 chore(deps-dev): bump duckdb from 1.4.2 to 1.5.2 (#40381)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 22:56:50 -07:00
dependabot[bot]
709ef9b615 chore(deps): bump d3-cloud from 1.2.8 to 1.2.9 in /superset-frontend (#40321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 19:08:10 -07:00
dependabot[bot]
e9d46d843f chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40415)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 19:07:41 -07:00
dependabot[bot]
9cc2deb903 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#40416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 19:07:27 -07:00
dependabot[bot]
03d25277ba chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#40417)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 19:07:14 -07:00
dependabot[bot]
bbe2f207d2 chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#40418)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 19:07:00 -07:00
dependabot[bot]
c381677dfd chore(deps): bump click from 8.2.1 to 8.4.0 (#40312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 18:55:38 -07:00
dependabot[bot]
09572cd5ef chore(deps): bump tabulate from 0.9.0 to 0.10.0 (#40315)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
2026-05-25 18:55:24 -07:00
Alexandru Soare
33585b0480 feat(mcp): make form_data_key optional in update_chart_preview (#39680) 2026-05-25 15:09:36 +03:00
Alexandru Soare
b64561f3a3 chore(mcp): Simplify chart preview response (#40020) 2026-05-25 13:16:27 +03:00
xavier-GitHub76
fe484f6bb2 fix(UserListModal): Success notification mentions user and not group (#40284) 2026-05-25 00:03:14 +03:00
dependabot[bot]
8caa74354f chore(deps): bump markdown from 3.8.1 to 3.10.2 (#40389)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 23:18:33 -07:00
Evan Rusackas
9c90a6854c ci(translations): hard-block translation regressions in CI (#39443)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-23 16:30:14 -07:00
Evan Rusackas
2fef4e41f2 feat(i18n): add Finnish (fi) translations (AI-generated, needs review) (#40390)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:29:44 -07:00
Jean Massucatto
965ec47296 fix(explore): hide value input for unary filter operators (#39924) 2026-05-23 11:43:49 -07:00
Evan Rusackas
b21450681d feat(i18n): add Thai (th) translations (AI-generated, needs review) (#40391)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 11:35:19 -07:00
Torsten Stöter
5003ee1499 docs: remove out-of-place phrase (#40226) 2026-05-23 13:42:52 +07:00
Abdul Rehman
816794b198 fix(frontend): handle null/undefined path in ensureAppRoot (#39940)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-23 13:22:27 +07:00
dependabot[bot]
841871f1e7 chore(deps): bump qs from 6.14.2 to 6.15.2 in /superset-websocket/utils/client-ws-app (#40382)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 12:00:20 +07:00
dependabot[bot]
55203bbc74 chore(deps): bump qs from 6.14.2 to 6.15.2 in /docs (#40383)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 11:55:04 +07:00
Evan Rusackas
2fa3bbd91c chore(ci): limit /app/prefix matrix variant to master merges (#40385)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:11:21 -07:00
Evan Rusackas
168b49bf34 chore(cypress): remove dead _skip spec files and skipped inline tests (#40384)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:11:13 -07:00
Evan Rusackas
838ac8f553 fix(ci): stop cancelling Hold Label Check runs (#40380)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:10:23 -07:00
Evan Rusackas
42668cf634 ci(docker): pin QEMU binfmt image to stabilize arm64 builds (#40235)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-22 21:09:54 -07:00
Evan Rusackas
8d985d223b ci(e2e): run backend under gunicorn instead of flask dev server (#40234)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-22 21:09:14 -07:00
Evan Rusackas
e57387098b fix(bigquery): limit result set size to prevent browser memory crashes (#38588)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: ethan-l-geotab <ethanliong@geotab.com>
2026-05-22 21:07:45 -07:00
Evan Rusackas
af6ac4d09c feat(i18n): AI-assisted translation backfill tooling + Spanish translations (#39448)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-22 21:07:27 -07:00
yousoph
f8e13770fc fix(dashboard): add top padding to "Create new chart" button in builder pane (#40033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:37:04 -07:00
Miha Rejec
5cdd542ae5 fix(i18n): translate DateFilter tooltip for time range values (#40286)
Co-authored-by: Miha Rejec <mihar@comland.si>
2026-05-23 00:22:31 +03:00
dependabot[bot]
e40648dfcb chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 in /superset-websocket (#39425)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:39:07 -07:00
dependabot[bot]
c728b4a11f chore(deps): bump sqlglot from 28.10.0 to 30.8.0 (#40186)
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: Claude <claude@anthropic.com>
2026-05-22 11:35:30 -07:00
dependabot[bot]
0febe32dc9 chore(deps): bump geostyler from 18.5.1 to 18.6.0 in /superset-frontend (#40323)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-22 11:34:30 -07:00
dependabot[bot]
2a0ebd7055 chore(deps-dev): bump ts-jest from 29.4.10 to 29.4.11 in /superset-websocket (#40363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:34:16 -07:00
dependabot[bot]
6e23e4541d chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40365)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:33:58 -07:00
dependabot[bot]
5af8fe77fa chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:33:44 -07:00
dependabot[bot]
3599c78a03 chore(deps): bump react-arborist from 3.6.1 to 3.7.0 in /superset-frontend (#40371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:33:28 -07:00
dependabot[bot]
d97b5d6509 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40372)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:33:12 -07:00
dependabot[bot]
869ab37f59 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40366)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 11:28:14 -07:00
Joe Li
3b4892c48c fix(select): replace cached options with search results in AsyncSelect (#40039)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:25:03 -07:00
Đỗ Trọng Hải
91d96419fe feat(sec): delays version-bumping PR to avoid prematurely usage of compromised packages (#39783)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-22 11:01:10 -07:00
Ali Gouta
42149f6a78 fix(chart): fix label and description translation on UI and enhance french translations (#40229)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-22 10:47:55 -07:00
Evan Rusackas
c945ef6763 chore(oxlint): enable import/newline-after-import + react/no-unstable-nested-components (#40319)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:11:38 -07:00
Beto Dealmeida
21059b54f0 feat(semantic layers): form for SL with a single SV (#40280) 2026-05-22 12:04:25 -04:00
Mehmet Salih Yavuz
8ab4695ba3 fix(mcp): use name URL param so AI-generated SQL Lab titles render (#40288) 2026-05-22 17:35:08 +03:00
dependabot[bot]
b0d26196fc chore(deps-dev): bump @swc/plugin-emotion from 14.9.0 to 14.10.0 in /superset-frontend (#40368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 21:31:01 +07:00
dependabot[bot]
df8222ffcd chore(deps-dev): bump ts-jest from 29.4.10 to 29.4.11 in /superset-frontend (#40369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 21:28:40 +07:00
Đỗ Trọng Hải
64f0e88de7 chore(backend/build): upgrade Gunicorn from v22 to v25 (#38788)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-22 21:24:58 +07:00
Đỗ Trọng Hải
f4af6a2caf fix(docker): add missing service-worker.js into built container image (#39596)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-22 21:23:21 +07:00
dependabot[bot]
31087177ab chore(deps-dev): bump webpack from 5.107.0 to 5.107.1 in /docs (#40364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 21:21:22 +07:00
Shaitan
8e98ca6569 docs: expand out-of-scope vulnerability definitions (#40332) 2026-05-22 21:20:57 +07:00
dependabot[bot]
f7f6c29adf chore(deps-dev): bump webpack from 5.106.2 to 5.107.1 in /superset-frontend (#40370)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 21:15:44 +07:00
Miha Rejec
a94edfe418 fix(i18n): add Slovenian translation for 'Range type' in DateFilter (#40287)
Co-authored-by: Miha Rejec <mihar@comland.si>
2026-05-22 09:58:27 -04:00
David Kopelent
5549100601 feat(i18n): add missing Slovak translations (#40219) 2026-05-22 09:56:43 -04:00
Alexandru Soare
558ff4452b fix(preview): fix chart preview bugs (#40063) 2026-05-22 13:42:59 +03:00
Amin Ghadersohi
5966bb1c1e feat(mcp): add series_limit to generate_chart XY config (#40307)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:36:30 -04:00
chaselynisabella
ac035083d7 feat(path): support metric-based color scales & line width by metric (#39165) 2026-05-21 19:31:15 -04:00
Amin Ghadersohi
e25d708197 fix(mcp): hide write tools from users without write permissions (#40098)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 19:11:46 -04:00
dependabot[bot]
48cb3f5885 chore(deps-dev): bump baseline-browser-mapping from 2.10.29 to 2.10.31 in /superset-frontend (#40320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 13:45:02 -07:00
dependabot[bot]
dcef6f8a41 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 13:44:46 -07:00
dependabot[bot]
f09fd63495 chore(deps): bump @googleapis/sheets from 13.0.1 to 13.0.2 in /superset-frontend (#40324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 13:44:05 -07:00
dependabot[bot]
bc26006a43 chore(deps-dev): update sqlalchemy-drill requirement from <2,>=1.1.4 to >=1.1.10,<2 (#40310)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 13:42:40 -07:00
dependabot[bot]
8b483f320e chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#40325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 13:41:40 -07:00
Evan Rusackas
89c2a47433 fix(TableView): reset pagination when data reduces below current page (#34562)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-21 13:39:05 -07:00
JUST.in DO IT
b8b91574e0 fix(view query): Update style for code viewer container (#39635)
Co-authored-by: Copilot <copilot@github.com>
2026-05-21 13:37:56 -07:00
Jay Masiwal
5526464def fix(frontend): update safeStringify to surface [Circular] and DRY plugin code (#39156) 2026-05-21 13:37:05 -07:00
Evan Rusackas
73f66e4c14 fix(datasets): isolate filter state to fix concurrent /dataset race (#39685)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-21 11:12:32 -07:00
Elizabeth Thompson
f187a8e1c4 fix(reports): guard null dashboard height in Playwright screenshots (#40179) 2026-05-21 09:19:29 -07:00
Mehmet Salih Yavuz
4c3f65ef0b feat(mcp): make config optional in generate_explore_link (#39559) 2026-05-21 18:01:59 +03:00
Mehmet Salih Yavuz
53d8e5bdfa feat(mcp): include applied dashboard filters in get_chart_info (#39620)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:48:21 +03:00
Mehmet Salih Yavuz
2f95d288dd fix(mcp): eager-load dataset.metrics to prevent Excel export DetachedInstanceError (#39483)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:34:38 +03:00
Beto Dealmeida
2f5fcc21f9 fix(semantic layers): coerce filter types (#40222) 2026-05-21 09:25:27 -04:00
Mehmet Salih Yavuz
d1d07112aa feat(mcp): add find_users tool and owner filter columns for listings (#39679)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:59:09 +03:00
Alexandru Soare
e3711bec39 fix(recommandation): Fix chart recommandation (#39886) 2026-05-21 15:16:16 +03:00
Mehmet Salih Yavuz
ce9cab098f feat(mcp): chart formatting options across all supported chart types (#39887) 2026-05-21 15:00:32 +03:00
613 changed files with 159701 additions and 23153 deletions

67
.github/SECURITY.md vendored
View File

@@ -1,67 +0,0 @@
# Security Policy
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
## Reporting Vulnerabilities
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
Apache Software Foundation takes a rigorous standpoint in annihilating the security issues
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
pertaining to its features and functionality.
If you have any concern or believe you have found a vulnerability in Apache Superset,
please get in touch with the Apache Superset Security Team privately at
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
More details can be found on the ASF website at
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
**Submission Standards & AI Policy**
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
- Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
**Out of Scope Vulnerabilities**
To prioritize engineering efforts on genuine architectural risks, the following scenarios are explicitly out of scope and will not be issued a CVE:
- Attacks requiring Admin privileges: (e.g., CSS injection, template manipulation, dashboard ownership overrides, or modifying global system settings). Per the CVE vulnerability definition in CNA Operational Rules 4.1, a qualifying vulnerability must allow violation of a security policy. The Admin role is a fully trusted operational boundary defined by Apache Superset's security policy; actions within this boundary do not violate that policy and are therefore considered intended capabilities 'by design,' not vulnerabilities.
- Brute Force and Rate Limiting: Reports targeting a lack of resource exhaustion protections, generic rate-limiting, or volumetric Denial of Service (DoS) attempts.
- Theoretical attack vectors: Issues without a demonstrable, reproducible exploit path.
- Non-Exploitable Findings: Missing security headers, generic banner disclosures, or descriptive error messages that do not lead to a direct, documented exploit.
**Outcome of Reports**
Reports that are deemed out-of-scope for a CVE but represent valid security best practices or hardening opportunities may be converted into public GitHub issues. This allows the community to contribute to the general hardening of the platform even when a specific vulnerability threshold is not met.
Note that Apache Superset is not responsible for any third-party dependencies that may
have security issues. Any vulnerabilities found in third-party dependencies should be
reported to the maintainers of those projects. Results from security scans of Apache
Superset dependencies found on its official Docker image can be remediated at release time
by extending the image itself.
**Vulnerability Aggregation & CVE Attribution**
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
**Your responsible disclosure and collaboration are invaluable.**
## Extra Information
- [Apache Superset documentation](https://superset.apache.org/docs/security)
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)

View File

@@ -24,32 +24,41 @@ runs:
- name: Interpret Python Version
id: set-python-version
shell: bash
env:
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
run: |
if [ "${{ inputs.python-version }}" = "current" ]; then
echo "PYTHON_VERSION=3.11" >> $GITHUB_ENV
elif [ "${{ inputs.python-version }}" = "next" ]; then
if [ "$INPUT_PYTHON_VERSION" = "current" ]; then
RESOLVED_VERSION="3.11"
elif [ "$INPUT_PYTHON_VERSION" = "next" ]; then
# currently disabled in GHA matrixes because of library compatibility issues
echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV
elif [ "${{ inputs.python-version }}" = "previous" ]; then
echo "PYTHON_VERSION=3.10" >> $GITHUB_ENV
RESOLVED_VERSION="3.12"
elif [ "$INPUT_PYTHON_VERSION" = "previous" ]; then
RESOLVED_VERSION="3.10"
elif printf '%s' "$INPUT_PYTHON_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
RESOLVED_VERSION="$INPUT_PYTHON_VERSION"
else
echo "PYTHON_VERSION=${{ inputs.python-version }}" >> $GITHUB_ENV
echo "Invalid python-version: '$INPUT_PYTHON_VERSION'" >&2
exit 1
fi
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ env.PYTHON_VERSION }}
python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }}
- name: Install dependencies
env:
INPUT_INSTALL_SUPERSET: ${{ inputs.install-superset }}
INPUT_REQUIREMENTS_TYPE: ${{ inputs.requirements-type }}
run: |
if [ "${{ inputs.install-superset }}" = "true" ]; then
if [ "$INPUT_INSTALL_SUPERSET" = "true" ]; then
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
pip install --upgrade pip setuptools wheel uv
if [ "${{ inputs.requirements-type }}" = "dev" ]; then
if [ "$INPUT_REQUIREMENTS_TYPE" = "dev" ]; then
uv pip install --system -r requirements/development.txt
elif [ "${{ inputs.requirements-type }}" = "base" ]; then
elif [ "$INPUT_REQUIREMENTS_TYPE" = "base" ]; then
uv pip install --system -r requirements/base.txt
fi

View File

@@ -26,16 +26,25 @@ runs:
- name: Set up QEMU
if: ${{ inputs.build == 'true' }}
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
with:
# Pin the binfmt image to a specific QEMU release. The default
# (`tonistiigi/binfmt:latest`) is a moving target, and drift across
# QEMU's x86_64→aarch64 translator has been the proximate cause of
# intermittent `exit code: 132` (SIGILL) failures during the arm64
# leg of the multi-platform docker build — newer Node native modules
# emit instructions QEMU's user-mode emulation occasionally drops on
# the floor. Pinning a known-good release stabilises that path.
image: tonistiigi/binfmt:qemu-v8.1.5
- name: Set up Docker Buildx
if: ${{ inputs.build == 'true' }}
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Try to login to DockerHub
if: ${{ inputs.login-to-dockerhub == 'true' }}
continue-on-error: true
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ inputs.dockerhub-user }}
password: ${{ inputs.dockerhub-token }}

View File

@@ -10,7 +10,7 @@ runs:
steps:
- name: Setup Node Env
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
@@ -21,8 +21,9 @@ runs:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
if: ${{ inputs.from-npm == 'false' }}
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
repository: apache-superset/supersetbot
path: supersetbot

View File

@@ -1,7 +1,6 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "github-actions"
directory: "/"
ignore:
@@ -10,6 +9,8 @@ updates:
- dependency-name: anthropics/claude-code-action
schedule:
interval: "daily"
cooldown:
default-days: 7
- package-ecosystem: "npm"
ignore:
@@ -57,6 +58,8 @@ updates:
- dependabot
open-pull-requests-limit: 30
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "pip"
@@ -72,6 +75,8 @@ updates:
labels:
- pip
- dependabot
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: ".github/actions"
@@ -79,6 +84,8 @@ updates:
interval: "daily"
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/docs/"
@@ -102,6 +109,8 @@ updates:
interval: "daily"
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-websocket/"
@@ -111,6 +120,8 @@ updates:
- npm
- dependabot
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-websocket/utils/client-ws-app/"
@@ -121,6 +132,8 @@ updates:
- dependabot
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
# Now for all of our plugins and packages!
@@ -133,6 +146,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
@@ -143,6 +158,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
@@ -153,6 +170,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
@@ -166,6 +185,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
@@ -176,6 +197,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
@@ -186,6 +209,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
@@ -196,6 +221,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
@@ -206,6 +233,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-table/"
@@ -219,6 +248,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
@@ -229,6 +260,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
@@ -239,6 +272,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
@@ -249,6 +284,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
@@ -259,6 +296,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
@@ -269,6 +308,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
@@ -279,6 +320,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
@@ -289,6 +332,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
@@ -299,6 +344,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
@@ -309,6 +356,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
@@ -323,6 +372,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/generator-superset/"
@@ -333,6 +384,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
@@ -343,6 +396,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-core/"
@@ -358,6 +413,8 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-switchboard/"
@@ -368,3 +425,5 @@ updates:
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7

View File

@@ -59,6 +59,15 @@ build-assets() {
say "::endgroup::"
}
build-embedded-sdk() {
cd "$GITHUB_WORKSPACE/superset-embedded-sdk"
say "::group::Build embedded SDK bundle for E2E tests"
npm ci
npm run build
say "::endgroup::"
}
build-instrumented-assets() {
cd "$GITHUB_WORKSPACE/superset-frontend"
@@ -175,10 +184,13 @@ cypress-run-all() {
local APP_ROOT=$2
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
# Start Flask and run it in background
# --no-debugger means disable the interactive debugger on the 500 page
# so errors can print to stderr.
local flasklog="${HOME}/flask.log"
# Start the Superset backend via gunicorn (not `flask run`). The Flask
# development server is single-threaded and has no crash-recovery, so
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
# / `Missing CSRF token` cascades. Gunicorn gives us multiple workers,
# a request timeout, and worker-recycling under load.
local serverlog="${HOME}/superset-cypress.log"
local port=8081
CYPRESS_BASE_URL="http://localhost:${port}"
if [ -n "$APP_ROOT" ]; then
@@ -187,8 +199,58 @@ cypress-run-all() {
fi
export CYPRESS_BASE_URL
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
local flaskProcessId=$!
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
# gthread threads) to keep parity with production. Multi-worker
# configurations expose timing-sensitive races in the SQL Lab → Explore
# navigation flow under E2E. We diverge from the entrypoint on:
# --timeout 120: heavy dashboard import/export specs exceed the 60s
# default
# --max-requests / --max-requests-jitter: recycle the worker under
# test load to avoid leaks accumulating across the run
# superset.app:create_app(): explicit factory so we don't depend on
# FLASK_APP being exported
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \
--worker-class gthread \
--threads 20 \
--timeout 120 \
--max-requests 500 \
--max-requests-jitter 50 \
--access-logfile - \
--error-logfile - \
"superset.app:create_app()" \
>"$serverlog" 2>&1 </dev/null &
local serverPid=$!
# Ensure the backend is cleaned up and its log is emitted even when the
# test runner fails under `set -e`.
trap '
echo "::group::gunicorn log for Cypress run"
cat "'"$serverlog"'" || true
echo "::endgroup::"
kill '"$serverPid"' 2>/dev/null || true
' EXIT
# Wait for the backend to be ready before launching Cypress; otherwise
# the first spec can race the server bind and see connection errors.
local timeout=60
say "Waiting for gunicorn server to start on port $port..."
while [ $timeout -gt 0 ]; do
if curl -f "http://localhost:${port}${APP_ROOT}/health" >/dev/null 2>&1; then
say "gunicorn server is ready"
break
fi
sleep 1
timeout=$((timeout - 1))
done
if [ $timeout -eq 0 ]; then
echo "::error::gunicorn server failed to start within 60 seconds"
echo "::group::Server startup log"
cat "$serverlog"
echo "::endgroup::"
return 1
fi
USE_DASHBOARD_FLAG=''
if [ "$USE_DASHBOARD" = "true" ]; then
@@ -200,13 +262,6 @@ cypress-run-all() {
# memoryMonitorPid=$!
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID --group $PARALLEL_ID --retries 5 $USE_DASHBOARD_FLAG
# kill $memoryMonitorPid
# After job is done, print out Flask log for debugging
echo "::group::Flask log for default run"
cat "$flasklog"
echo "::endgroup::"
# make sure the program exits
kill $flaskProcessId
}
playwright-install() {
@@ -224,29 +279,55 @@ playwright-run() {
local APP_ROOT=$1
local TEST_PATH=$2
# Start Flask from the project root (same as Cypress)
# Start the Superset backend via gunicorn from the project root.
# See cypress-run-all() above for the rationale — the Flask dev server
# cannot survive the dashboard import/export tests under load.
cd "$GITHUB_WORKSPACE"
local flasklog="${HOME}/flask-playwright.log"
local serverlog="${HOME}/superset-playwright.log"
local port=8081
PLAYWRIGHT_BASE_URL="http://localhost:${port}"
# Use 127.0.0.1 explicitly: `flask run` binds IPv4 only, and Node's DNS
# resolution for `localhost` can return `::1` first (IPv6), which then
# refuses against the IPv4 listener and surfaces as
# `connect ECONNREFUSED ::1:<port>` in API helpers driven from Node
# (e.g., the embedded test app's exposed token fetcher).
PLAYWRIGHT_BASE_URL="http://127.0.0.1:${port}"
if [ -n "$APP_ROOT" ]; then
export SUPERSET_APP_ROOT=$APP_ROOT
PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}${APP_ROOT}/
fi
export PLAYWRIGHT_BASE_URL
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
local flaskProcessId=$!
# See cypress-run-all() above for the args rationale (1 worker × 20
# gthread threads matching docker/entrypoints/run-server.sh, plus a
# 120s timeout and request-recycling for heavy E2E load).
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \
--worker-class gthread \
--threads 20 \
--timeout 120 \
--max-requests 500 \
--max-requests-jitter 50 \
--access-logfile - \
--error-logfile - \
"superset.app:create_app()" \
>"$serverlog" 2>&1 </dev/null &
local serverPid=$!
# Ensure cleanup on exit
trap "kill $flaskProcessId 2>/dev/null || true" EXIT
# Ensure cleanup on exit (and emit the server log on failure)
trap '
echo "::group::gunicorn log for Playwright run"
cat "'"$serverlog"'" || true
echo "::endgroup::"
kill '"$serverPid"' 2>/dev/null || true
' EXIT
# Wait for server to be ready with health check
local timeout=60
say "Waiting for Flask server to start on port $port..."
say "Waiting for gunicorn server to start on port $port..."
while [ $timeout -gt 0 ]; do
if curl -f ${PLAYWRIGHT_BASE_URL}/health >/dev/null 2>&1; then
say "Flask server is ready"
say "gunicorn server is ready"
break
fi
sleep 1
@@ -254,9 +335,9 @@ playwright-run() {
done
if [ $timeout -eq 0 ]; then
echo "::error::Flask server failed to start within 60 seconds"
echo "::group::Flask startup log"
cat "$flasklog"
echo "::error::gunicorn server failed to start within 60 seconds"
echo "::group::Server startup log"
cat "$serverlog"
echo "::endgroup::"
return 1
fi
@@ -271,7 +352,6 @@ playwright-run() {
if ! find "playwright/tests/${TEST_PATH}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
echo "No test files found in ${TEST_PATH} - skipping test run"
say "::endgroup::"
kill $flaskProcessId
return 0
fi
echo "Running tests: ${TEST_PATH}"
@@ -288,13 +368,6 @@ playwright-run() {
fi
say "::endgroup::"
# After job is done, print out Flask log for debugging
echo "::group::Flask log for Playwright run"
cat "$flasklog"
echo "::endgroup::"
# make sure the program exits
kill $flaskProcessId
return $status
}

View File

@@ -1,43 +0,0 @@
name: Cancel Duplicates
on:
workflow_run:
workflows:
- "Miscellaneous"
types:
- requested
jobs:
cancel-duplicate-runs:
name: Cancel duplicate workflow runs
runs-on: ubuntu-24.04
permissions:
actions: write
contents: read
steps:
- name: Check number of queued tasks
id: check_queued
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPO: ${{ github.repository }}
run: |
get_count() {
echo $(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$GITHUB_REPO/actions/runs?status=$1" | \
jq ".total_count")
}
count=$(( `get_count queued` + `get_count in_progress` ))
echo "Found $count unfinished jobs."
echo "count=$count" >> $GITHUB_OUTPUT
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
if: steps.check_queued.outputs.count >= 20
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Cancel duplicate workflow runs
if: steps.check_queued.outputs.count >= 20
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
pip install click requests typing_extensions python-dateutil
python ./scripts/cancel_github_workflows.py

View File

@@ -26,6 +26,8 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check and notify
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:

View File

@@ -6,6 +6,9 @@ on:
pull_request_review_comment:
types: [created]
permissions:
contents: read
jobs:
check-permissions:
if: |
@@ -75,6 +78,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
fetch-depth: 1
- name: Run Claude PR Action

View File

@@ -32,6 +32,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check for file changes
id: check
@@ -41,7 +43,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,6 +55,6 @@ jobs:
- name: Perform CodeQL Analysis
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -28,6 +28,8 @@ jobs:
steps:
- name: "Checkout Repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: "Dependency Review"
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
continue-on-error: true
@@ -50,6 +52,8 @@ jobs:
steps:
- name: "Checkout Repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup Python
uses: ./.github/actions/setup-backend/

View File

@@ -73,20 +73,21 @@ jobs:
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_PRESET: ${{ matrix.build_preset }}
run: |
# Single platform builds in pull_request context to speed things up
if [ "${{ github.event_name }}" = "push" ]; then
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
PLATFORM_ARG="--platform linux/arm64 --platform linux/amd64"
# can only --load images in single-platform builds
PUSH_OR_LOAD="--push"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
PLATFORM_ARG="--platform linux/amd64"
PUSH_OR_LOAD="--load"
fi
supersetbot docker \
$PUSH_OR_LOAD \
--preset ${{ matrix.build_preset }} \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
@@ -95,7 +96,11 @@ 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)
run: docker pull $IMAGE_TAG
run: |
for i in 1 2 3; do
docker pull $IMAGE_TAG && break
[ $i -lt 3 ] && sleep 30
done
- name: Print docker stats
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
@@ -108,8 +113,10 @@ jobs:
- name: docker-compose sanity check
if: (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'dev'
shell: bash
env:
BUILD_PRESET: ${{ matrix.build_preset }}
run: |
export SUPERSET_BUILD_TARGET=${{ matrix.build_preset }}
export SUPERSET_BUILD_TARGET=$BUILD_PRESET
# This should reuse the CACHED image built in the previous steps
docker compose build superset-init --build-arg DEV_MODE=false --build-arg INCLUDE_CHROMIUM=false
docker compose up superset-init --exit-code-from superset-init

View File

@@ -34,6 +34,8 @@ jobs:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-embedded-sdk/.nvmrc'

View File

@@ -22,6 +22,8 @@ jobs:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: './superset-embedded-sdk/.nvmrc'

View File

@@ -1,83 +0,0 @@
name: Cleanup ephemeral envs (PR close) [DEPRECATED]
# ⚠️ DEPRECATION NOTICE ⚠️
# This workflow is deprecated and will be removed in a future version.
# The new Superset Showtime workflow handles cleanup automatically.
# See .github/workflows/showtime.yml and showtime-cleanup.yml for replacements.
# Migration guide: https://github.com/mistercrunch/superset-showtime
on:
pull_request_target:
types: [closed]
jobs:
config:
runs-on: ubuntu-24.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
steps:
- name: "Check for secrets"
id: check
shell: bash
run: |
if [ -n "${AWS_ACCESS_KEY_ID}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
env:
AWS_ACCESS_KEY_ID: ${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}
ephemeral-env-cleanup:
needs: config
if: needs.config.outputs.has-secrets
name: Cleanup ephemeral envs
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Describe ECS service
id: describe-services
run: |
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
- name: Delete ECS service
if: steps.describe-services.outputs.active == 'true'
id: delete-service
run: |
aws ecs delete-service \
--cluster superset-ci \
--service pr-${{ github.event.number }}-service \
--force
- name: Login to Amazon ECR
if: steps.describe-services.outputs.active == 'true'
id: login-ecr
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Delete ECR image tag
if: steps.describe-services.outputs.active == 'true'
id: delete-image-tag
run: |
aws ecr batch-delete-image \
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
--repository-name superset-ci \
--image-ids imageTag=pr-${{ github.event.number }}
- name: Comment (success)
if: steps.describe-services.outputs.active == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{github.token}}
script: |
github.rest.issues.createComment({
issue_number: ${{ github.event.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '⚠️ **DEPRECATED WORKFLOW** - Ephemeral environment shutdown and build artifacts deleted. Please migrate to the new Superset Showtime system for future PRs.'
})

View File

@@ -1,350 +0,0 @@
name: Ephemeral env workflow [DEPRECATED]
# ⚠️ DEPRECATION NOTICE ⚠️
# This workflow is deprecated and will be removed in a future version.
# Please use the new Superset Showtime workflow instead:
# - Use label "🎪 trigger-start" instead of "testenv-up"
# - Showtime provides better reliability and easier management
# - See .github/workflows/showtime.yml for the replacement
# - Migration guide: https://github.com/mistercrunch/superset-showtime
# Example manual trigger:
# gh workflow run ephemeral-env.yml --ref fix_ephemerals --field label_name="testenv-up" --field issue_number=666
on:
pull_request_target:
types:
- labeled
workflow_dispatch:
inputs:
label_name:
description: 'Label name to simulate label-based /testenv trigger'
required: true
default: 'testenv-up'
issue_number:
description: 'Issue or PR number'
required: true
jobs:
ephemeral-env-label:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-label
cancel-in-progress: true
name: Evaluate ephemeral env label trigger
runs-on: ubuntu-24.04
permissions:
pull-requests: write
outputs:
slash-command: ${{ steps.eval-label.outputs.result }}
feature-flags: ${{ steps.eval-feature-flags.outputs.result }}
sha: ${{ steps.get-sha.outputs.sha }}
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Check for the "testenv-up" label
id: eval-label
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
LABEL_NAME="${INPUT_LABEL_NAME}"
else
LABEL_NAME="${{ github.event.label.name }}"
fi
echo "Evaluating label: $LABEL_NAME"
if [[ "$LABEL_NAME" == "testenv-up" ]]; then
echo "result=up" >> $GITHUB_OUTPUT
else
echo "result=noop" >> $GITHUB_OUTPUT
fi
env:
INPUT_LABEL_NAME: ${{ github.event.inputs.label_name }}
- name: Get event SHA
id: get-sha
if: steps.eval-label.outputs.result == 'up'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
let prSha;
// If event is workflow_dispatch, use the issue_number from inputs
if (context.eventName === "workflow_dispatch") {
const prNumber = "${{ github.event.inputs.issue_number }}";
if (!prNumber) {
console.log("No PR number found.");
return;
}
// Fetch PR details using the provided issue_number
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
prSha = pr.head.sha;
} else {
// If it's not workflow_dispatch, use the PR head sha from the event
prSha = context.payload.pull_request.head.sha;
}
console.log(`PR SHA: ${prSha}`);
core.setOutput("sha", prSha);
- name: Looking for feature flags in PR description
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
id: eval-feature-flags
if: steps.eval-label.outputs.result == 'up'
with:
script: |
const description = context.payload.pull_request
? context.payload.pull_request.body || ''
: context.payload.inputs.pr_description || '';
const pattern = /FEATURE_(\w+)=(\w+)/g;
let results = [];
[...description.matchAll(pattern)].forEach(match => {
const config = {
name: `SUPERSET_FEATURE_${match[1]}`,
value: match[2],
};
results.push(config);
});
return results;
- name: Reply with confirmation comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
if: steps.eval-label.outputs.result == 'up'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const action = '${{ steps.eval-label.outputs.result }}';
const user = context.actor;
const runId = context.runId;
const workflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
const issueNumber = context.payload.pull_request
? context.payload.pull_request.number
: context.payload.inputs.issue_number;
if (!issueNumber) {
throw new Error("Issue number is not available.");
}
const body = `⚠️ **DEPRECATED WORKFLOW** ⚠️\n\n@${user} This workflow is deprecated! Please use the new **Superset Showtime** system instead:\n\n` +
`- Replace "testenv-up" label with "🎪 trigger-start"\n` +
`- Better reliability and easier management\n` +
`- See https://github.com/mistercrunch/superset-showtime for details\n\n` +
`Processing your ephemeral environment request [here](${workflowUrl}). Action: **${action}**.` +
` More information on [how to use or configure ephemeral environments]` +
`(https://superset.apache.org/docs/contributing/howtos/#github-ephemeral-environments)`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
ephemeral-docker-build:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-build
cancel-in-progress: true
needs: ephemeral-env-label
if: needs.ephemeral-env-label.outputs.slash-command == 'up'
name: ephemeral-docker-build
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ needs.ephemeral-env-label.outputs.sha }} : ${{steps.get-sha.outputs.sha}} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.ephemeral-env-label.outputs.sha }}
persist-credentials: false
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker
with:
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
build: "true"
install-docker-compose: "false"
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
- name: Build ephemeral env image
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
supersetbot docker \
--push \
--load \
--preset ci \
--platform linux/amd64 \
--context-ref "$RELEASE" \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Load, tag and push image to ECR
id: push-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: superset-ci
IMAGE_TAG: apache/superset:${{ needs.ephemeral-env-label.outputs.sha }}-ci
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
run: |
docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-$PR_NUMBER-ci
docker push -a $ECR_REGISTRY/$ECR_REPOSITORY
ephemeral-env-up:
needs: [ephemeral-env-label, ephemeral-docker-build]
if: needs.ephemeral-env-label.outputs.slash-command == 'up'
name: Spin up an ephemeral environment
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Check target image exists in ECR
id: check-image
continue-on-error: true
env:
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
run: |
aws ecr describe-images \
--registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
--repository-name superset-ci \
--image-ids imageTag=pr-$PR_NUMBER-ci
- name: Fail on missing container image
if: steps.check-image.outcome == 'failure'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ github.token }}
script: |
const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.';
github.rest.issues.createComment({
issue_number: ${{ github.event.inputs.issue_number || github.event.pull_request.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: errMsg
});
core.setFailed(errMsg);
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1
with:
task-definition: .github/workflows/ecs-task-definition.json
container-name: superset-ci
image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-ci
- name: Update env vars in the Amazon ECS task definition
run: |
cat <<< "$(jq '.containerDefinitions[0].environment += ${{ needs.ephemeral-env-label.outputs.feature-flags }}' < ${{ steps.task-def.outputs.task-definition }})" > ${{ steps.task-def.outputs.task-definition }}
- name: Describe ECS service
id: describe-services
run: |
echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${INPUT_ISSUE_NUMBER}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
env:
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
- name: Create ECS service
id: create-service
if: steps.describe-services.outputs.active != 'true'
env:
ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
PR_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
run: |
aws ecs create-service \
--cluster superset-ci \
--service-name pr-$PR_NUMBER-service \
--task-definition superset-ci \
--launch-type FARGATE \
--desired-count 1 \
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
- name: Deploy Amazon ECS task definition
id: deploy-task
uses: aws-actions/amazon-ecs-deploy-task-definition@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
cluster: superset-ci
wait-for-service-stability: true
wait-for-minutes: 10
- name: List tasks
id: list-tasks
run: |
echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${INPUT_ISSUE_NUMBER}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT
env:
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number || github.event.pull_request.number }}
- name: Get network interface
id: get-eni
run: |
echo "eni=$(aws ecs describe-tasks --cluster superset-ci --tasks ${{ steps.list-tasks.outputs.task }} | jq '.tasks[0].attachments[0].details | map(select(.name=="networkInterfaceId"))[0].value')" >> $GITHUB_OUTPUT
- name: Get public IP
id: get-ip
run: |
echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT
- name: Comment (success)
if: ${{ success() }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{github.token}}
script: |
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
github.rest.issues.createComment({
issue_number: issue_number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${{ github.actor }} Ephemeral environment spinning up at http://${{ steps.get-ip.outputs.ip }}:8080. Credentials are 'admin'/'admin'. Please allow several minutes for bootstrapping and startup.`
});
- name: Comment (failure)
if: ${{ failure() }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{github.token}}
script: |
const issue_number = context.payload.inputs?.issue_number || context.issue.number;
github.rest.issues.createComment({
issue_number: issue_number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@${{ github.event.inputs.user_login || github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
})

View File

@@ -6,7 +6,8 @@ on:
- "master"
- "[0-9].[0-9]*"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
branches:
- "**"
permissions:
contents: read
@@ -15,12 +16,19 @@ jobs:
validate-all-ghas:
runs-on: ubuntu-24.04
permissions:
contents: read
# Required for the zizmor action to upload its SARIF results to
# GitHub code scanning (advanced-security is enabled by default).
security-events: write
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
@@ -29,3 +37,6 @@ jobs:
- name: Run Script
run: bash .github/workflows/github-action-validator.sh
- name: Check for security issues on GHA workflows
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6

View File

@@ -9,7 +9,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/labeler@v6
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
sync-labels: true

View File

@@ -19,8 +19,10 @@ jobs:
- name: Check for latest tag
id: latest-tag
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
source ./scripts/tag_latest_release.sh $(echo ${{ github.event.release.tag_name }}) --dry-run
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
- name: Configure Git
run: |

View File

@@ -7,10 +7,13 @@ on:
permissions:
pull-requests: read
# cancel previous workflow jobs for PRs
# Let each label event run to completion. Cancelling in-progress runs leaves
# CANCELLED entries in the PR's check-suite rollup, which poisons GitHub's
# `status:success` search filter even though all real CI passed. The job is
# a tiny no-op github-script call, so the wasted compute is negligible.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
cancel-in-progress: false
jobs:
check-hold-label:

View File

@@ -71,10 +71,12 @@ jobs:
output: ' '
- name: pre-commit
env:
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
run: |
set +e # Don't exit immediately on failure
export SKIP=type-checking-frontend
pre-commit run --files ${{ steps.changed_files.outputs.files }}
pre-commit run --files $CHANGED_FILES
PRE_COMMIT_EXIT_CODE=$?
git diff --quiet --exit-code
GIT_DIFF_EXIT_CODE=$?

View File

@@ -6,6 +6,9 @@ on:
- "master"
- "[0-9].[0-9]*"
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
@@ -27,9 +30,12 @@ jobs:
if: needs.config.outputs.has-secrets
name: Bump version and publish package(s)
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# pulls all commits (needed for lerna / semantic release to correctly version)
fetch-depth: 0
- name: Get tags and filter trigger tags

View File

@@ -2,6 +2,7 @@ name: 🎪 Superset Showtime
# Ultra-simple: just sync on any PR state change
on:
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
pull_request_target:
types: [labeled, unlabeled, synchronize, closed]
@@ -102,7 +103,7 @@ jobs:
- name: Install Superset Showtime
if: steps.auth.outputs.authorized == 'true'
run: |
echo "::notice::Maintainer ${{ github.actor }} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
echo "::notice::Maintainer $GITHUB_ACTOR triggered deploy for PR ${PULL_REQUEST_NUMBER}"
pip install --upgrade superset-showtime
showtime version
@@ -173,9 +174,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
CHECK_PR_NUMBER: ${{ steps.check.outputs.pr_number }}
CHECK_TARGET_SHA: ${{ steps.check.outputs.target_sha }}
run: |
PR_NUM="${{ steps.check.outputs.pr_number }}"
TARGET_SHA="${{ steps.check.outputs.target_sha }}"
PR_NUM="$CHECK_PR_NUMBER"
TARGET_SHA="$CHECK_TARGET_SHA"
if [[ -n "$TARGET_SHA" ]]; then
python -m showtime sync $PR_NUM --sha "$TARGET_SHA"
else

View File

@@ -2,6 +2,7 @@ name: Docs Deployment
on:
# Deploy after integration tests complete on master
# zizmor: ignore[dangerous-triggers] - runs in base-branch context after a trusted upstream workflow; scoped to master
workflow_run:
workflows: ["Python-Integration"]
types: [completed]
@@ -27,6 +28,9 @@ concurrency:
group: docs-deploy-asf-site
cancel-in-progress: true
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
@@ -45,7 +49,13 @@ jobs:
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
build-deploy:
needs: config
if: needs.config.outputs.has-secrets
# For workflow_run triggers, only deploy when the triggering run originated
# from this repository (not a fork), ensuring the checked-out code and any
# local actions executed with deploy credentials are trusted.
if: >-
needs.config.outputs.has-secrets &&
(github.event_name != 'workflow_run' ||
github.event.workflow_run.head_repository.full_name == github.repository)
name: Build & Deploy
runs-on: ubuntu-24.04
steps:

View File

@@ -7,6 +7,7 @@ on:
- "superset/db_engine_specs/**"
- ".github/workflows/superset-docs-verify.yml"
types: [synchronize, opened, reopened, ready_for_review]
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes artifacts from the trusted upstream workflow
workflow_run:
workflows: ["Python-Integration"]
types: [completed]
@@ -16,6 +17,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_sha || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
linkinator:
# See docs here: https://github.com/marketplace/actions/linkinator
@@ -25,6 +29,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
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
@@ -97,7 +103,8 @@ jobs:
# Only runs if integration tests succeeded
if: >
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success'
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository
name: Build (after integration tests)
runs-on: ubuntu-24.04
defaults:

View File

@@ -42,7 +42,7 @@ jobs:
matrix:
parallel_id: [0, 1, 2, 3, 4, 5]
browser: ["chrome"]
app_root: ["", "/app/prefix"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -141,8 +141,9 @@ jobs:
- name: Set safe app root
if: failure()
id: set-safe-app-root
env:
APP_ROOT: ${{ matrix.app_root }}
run: |
APP_ROOT="${{ matrix.app_root }}"
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Artifacts
@@ -161,7 +162,7 @@ jobs:
fail-fast: false
matrix:
browser: ["chromium"]
app_root: ["", "/app/prefix"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -239,6 +240,11 @@ jobs:
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
@@ -254,8 +260,9 @@ jobs:
- name: Set safe app root
if: failure()
id: set-safe-app-root
env:
APP_ROOT: ${{ matrix.app_root }}
run: |
APP_ROOT="${{ matrix.app_root }}"
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Playwright Artifacts

View File

@@ -53,7 +53,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: steps.check.outputs.superset-extensions-cli
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
file: ./coverage.xml
flags: superset-extensions-cli

View File

@@ -16,6 +16,9 @@ concurrency:
env:
TAG: apache/superset:GHA-${{ github.run_id }}
permissions:
contents: read
jobs:
frontend-build:
runs-on: ubuntu-24.04
@@ -128,7 +131,7 @@ jobs:
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: javascript
use_oidc: true

View File

@@ -62,6 +62,8 @@ jobs:
run: echo "branch_name=helm-publish-${GITHUB_SHA:0:7}" >> $GITHUB_ENV
- name: Force recreate branch from gh-pages
env:
BRANCH_NAME: ${{ env.branch_name }}
run: |
# Ensure a clean working directory
git reset --hard
@@ -73,13 +75,13 @@ jobs:
git fetch origin gh-pages
# Check out and reset the target branch based on gh-pages
git checkout -B ${{ env.branch_name }} origin/gh-pages
git checkout -B "$BRANCH_NAME" origin/gh-pages
# Remove submodules from the branch
git submodule deinit -f --all
# Force push to the remote branch
git push origin ${{ env.branch_name }} --force
git push origin "$BRANCH_NAME" --force
# Return to the original branch
git checkout local_gha_temp
@@ -104,7 +106,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const branchName = '${{ env.branch_name }}';
const branchName = process.env.BRANCH_NAME;
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
if (!branchName) {

View File

@@ -113,6 +113,11 @@ jobs:
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
@@ -125,6 +130,21 @@ jobs:
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"
# Scope embedded-only env vars to this step. Setting them at the job
# level enabled the EMBEDDED_SUPERSET feature flag inside Flask for
# the preceding "Required Tests" and "Experimental Tests" steps too,
# which loads extra handlers and destabilizes the werkzeug dev
# server under the 2-worker Playwright load. Required Tests should
# match master's Flask configuration.
SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true"
INCLUDE_EMBEDDED: "true"
with:
run: playwright-run "${{ matrix.app_root }}" embedded
- name: Set safe app root
if: failure()
id: set-safe-app-root

View File

@@ -70,7 +70,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,mysql
verbose: true
@@ -164,7 +164,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,postgres
verbose: true
@@ -219,7 +219,7 @@ jobs:
run: |
./scripts/python_tests.sh
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,sqlite
verbose: true

View File

@@ -79,7 +79,7 @@ jobs:
run: |
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,presto
verbose: true
@@ -150,7 +150,7 @@ jobs:
pip install -e .[hive]
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,hive
verbose: true

View File

@@ -56,7 +56,7 @@ jobs:
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v5
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
flags: python,unit
verbose: true

View File

@@ -0,0 +1,88 @@
name: Translation Regression Comment
on:
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes the uploaded artifact; never checks out PR code (see note below)
workflow_run:
workflows: ["Translations"]
types: [completed]
# This workflow posts a PR comment when the Translations workflow detects a
# regression. It uses the workflow_run trigger so that it always runs in the
# base-branch context and can safely be granted write permissions, even for
# PRs from forks.
#
# IMPORTANT: This workflow must NEVER check out code from the PR branch.
# All data comes from the artifact uploaded by the Translations workflow.
permissions:
pull-requests: write
actions: read
jobs:
post-comment:
runs-on: ubuntu-24.04
# Only act when the Translations workflow failed (which means a regression
# was detected — the workflow exits 1 on regression).
if: github.event.workflow_run.conclusion == 'failure'
steps:
- name: Download regression artifact
id: download
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: translation-regression
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: /tmp/translation-regression
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs');
const prNumberFile = '/tmp/translation-regression/pr-number.txt';
const reportFile = '/tmp/translation-regression/regression-report.md';
if (!fs.existsSync(prNumberFile) || !fs.existsSync(reportFile)) {
console.log('Artifact files not found, skipping comment.');
return;
}
const prNumber = parseInt(fs.readFileSync(prNumberFile, 'utf8').trim(), 10);
if (!prNumber) {
console.log('Could not parse PR number, skipping comment.');
return;
}
const report = fs.readFileSync(reportFile, 'utf8');
const marker = '<!-- translation-regression-bot -->';
const body = `${marker}\n${report}`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
console.log(`Updated existing comment ${existing.id} on PR #${prNumber}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
console.log(`Created new comment on PR #${prNumber}`);
}

View File

@@ -20,6 +20,9 @@ concurrency:
jobs:
frontend-check-translations:
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -51,12 +54,16 @@ jobs:
babel-extract:
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
submodules: recursive
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
@@ -64,12 +71,83 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
if: steps.check.outputs.python
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
uses: ./.github/actions/setup-backend/
- name: Install msgcat
run: sudo apt update && sudo apt install gettext
- name: Install gettext tools
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Test babel extraction
if: steps.check.outputs.python
# Fetch the base ref so we can compare PR-introduced regressions
# against a fair baseline (also runs babel_update against the base
# source) — this isolates the PR's contribution from any pre-existing
# drift on the base branch.
- name: Fetch base ref and create comparison worktree
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
env:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
# For PRs use the base branch; for direct pushes compare against the previous commit.
BASE_REF="$PR_BASE_REF"
if [ -n "$BASE_REF" ]; then
git fetch --depth=1 origin "$BASE_REF"
else
git fetch --depth=2 origin "$GITHUB_REF"
fi
git worktree add /tmp/base-worktree FETCH_HEAD
# Run babel_update against BASE source + BASE translations. Any drift
# already present on the base branch (source strings that have changed
# without .po updates) shows up here as fuzzies — and will also show
# up in the PR run, so it cancels out in the comparison.
- name: Baseline — run babel_update against BASE source
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
working-directory: /tmp/base-worktree
run: ./scripts/translations/babel_update.sh
- name: Record baseline translation counts
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
run: |
python scripts/translations/check_translation_regression.py \
--count \
--translations-dir /tmp/base-worktree/superset/translations \
> /tmp/before.json
# Run babel_update against the PR source and PR translations. This keeps
# committed .po fixes in play while the base babel_update above still
# cancels out translation drift already present on the base branch.
- name: Run babel_update against PR source
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
run: ./scripts/translations/babel_update.sh
- name: Check for translation regression
id: regression
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
continue-on-error: true
run: |
python scripts/translations/check_translation_regression.py \
--compare /tmp/before.json \
--report /tmp/regression-report.md
# Save the PR number so the comment workflow can post the report without
# needing write permissions on this pull_request-triggered job.
- name: Save PR number for comment workflow
if: >-
github.event_name == 'pull_request' &&
steps.regression.outcome == 'failure'
run: echo "${{ github.event.pull_request.number }}" > /tmp/pr-number.txt
- name: Upload regression artifact
if: >-
github.event_name == 'pull_request' &&
steps.regression.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: translation-regression
path: |
/tmp/regression-report.md
/tmp/pr-number.txt
- name: Fail if regression detected
if: steps.regression.outcome == 'failure'
run: exit 1

View File

@@ -21,6 +21,9 @@ on:
options:
- 'true'
- 'false'
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
@@ -42,6 +45,8 @@ jobs:
if: needs.config.outputs.has-secrets
name: docker-release
runs-on: ubuntu-24.04
permissions:
contents: write
strategy:
matrix:
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
@@ -51,6 +56,7 @@ jobs:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
fetch-depth: 0
- name: Setup Docker Environment
@@ -62,9 +68,11 @@ jobs:
build: "true"
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 20
package-manager-cache: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
@@ -77,8 +85,9 @@ jobs:
INPUT_RELEASE: ${{ github.event.inputs.release }}
INPUT_FORCE_LATEST: ${{ github.event.inputs.force-latest }}
INPUT_GIT_REF: ${{ github.event.inputs.git-ref }}
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
RELEASE="${{ github.event.release.tag_name }}"
RELEASE="${GITHUB_EVENT_RELEASE_TAG_NAME}"
FORCE_LATEST=""
EVENT="${{github.event_name}}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
@@ -114,12 +123,15 @@ jobs:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
fetch-depth: 0
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 20
package-manager-cache: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
@@ -128,11 +140,12 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_RELEASE: ${{ github.event.inputs.release }}
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
export GITHUB_ACTOR=""
git fetch --all --tags
git checkout master
RELEASE="${{ github.event.release.tag_name }}"
RELEASE="${GITHUB_EVENT_RELEASE_TAG_NAME}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# in the case of a manually-triggered run, read release from input
RELEASE="${INPUT_RELEASE}"

View File

@@ -33,6 +33,8 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6

View File

@@ -1,22 +1,29 @@
name: Welcome New Contributor
on:
# zizmor: ignore[dangerous-triggers] - posts a welcome comment only; does not check out or execute PR-provided code
pull_request_target:
types: [opened]
jobs:
welcome:
runs-on: ubuntu-24.04
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
permissions:
pull-requests: write
steps:
- name: Welcome Message
uses: actions/first-interaction@v3
continue-on-error: true
uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0
with:
repo-token: ${{ github.token }}
pr-message: |-
repo_token: ${{ github.token }}
issue_message: |-
Congrats on opening your first issue and thank you for contributing to Superset! :tada: :heart:
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
pr_message: |-
Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart:
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
We hope to see you in our [Slack](https://apache-superset.slack.com/) community too! Not signed up? Use our [Slack App](http://bit.ly/join-superset-slack) to self-register.

2
.gitignore vendored
View File

@@ -115,6 +115,8 @@ release.json
superset/translations/**/messages.json
# these mo binary files are generated by `pybabel compile`
superset/translations/**/messages.mo
# cross-language index generated by scripts/translations/build_translation_index.py
superset/translations/translation_index.json
docker/requirements-local.txt

View File

@@ -50,7 +50,7 @@ repos:
hooks:
- id: check-docstring-first
- id: check-added-large-files
exclude: ^.*\.(geojson)$|^docs/static/img/screenshots/.*|^superset-frontend/CHANGELOG\.md$|^superset/examples/.*/data\.parquet$
exclude: ^.*\.(geojson)$|^docs/static/img/screenshots/.*|^superset-frontend/CHANGELOG\.md$|^superset/examples/.*/data\.parquet$|^superset/translations/.*\.po$
- id: check-yaml
exclude: ^helm/superset/templates/
- id: debug-statements
@@ -158,3 +158,14 @@ repos:
language: system
files: ^superset/config\.py$
pass_filenames: false
- id: zizmor
name: zizmor (GHA security audit)
entry: zizmor
language: python
additional_dependencies: [zizmor==1.25.2]
files: ^\.github/
types: [yaml]
pass_filenames: false
# Advisory until pre-existing findings are resolved; remove
# --no-exit-codes to make this hook blocking.
args: [--no-exit-codes, .github/]

View File

@@ -53,7 +53,7 @@ extension-pkg-whitelist=pyarrow
[MESSAGES CONTROL]
disable=all
enable=json-import,disallowed-sql-import,consider-using-transaction
enable=disallowed-sql-import,consider-using-transaction
[REPORTS]

View File

@@ -52,6 +52,43 @@ Common pre-commit failures:
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
## Security and Threat Model
Before evaluating any code path for security issues, read [`SECURITY.md`](SECURITY.md). It is the canonical, authoritative source for Apache Superset's security model and is referenced by both human reporters and automated scanners.
In short, the test for whether a finding is in scope is one question:
> *Does it let a principal perform an action the role and capability matrix in `SECURITY.md` does not entitle them to?*
If yes, it is in scope. If no, it is not.
The three trust boundaries are:
1. **The Admin role** is a fully trusted operational principal. Anything an Admin can do through documented configuration, API, or UI is an intended capability, not a vulnerability.
2. **The operator** owns deployment-time decisions (secrets, network exposure, feature-flag selection, connector and codec choices, notification destinations, third-party plugins). Misconfiguration at this layer is a deployment defect, not a Superset vulnerability.
3. **The codebase** is responsible for enforcing the role and capability matrix wherever it exposes functionality to a principal: API routes, command and DAO layers, UI handlers, background jobs, and any other entry point. A missing or incorrect enforcement check is in scope no matter where it lives.
The security model assumes that operator-controlled infrastructure, including the metadata database, cache backends, message brokers, secret stores, and deployment environment, remains within the operator's trust boundary. Vulnerabilities must demonstrate a security boundary violation by an attacker who does not already control those systems.
Route-level authorization in this codebase uses one of three Flask-AppBuilder decorators depending on the route type:
- `@protect()` for REST API routes (`ModelRestApi` / `BaseApi`)
- `@has_access_api` for legacy view routes
- `@has_access` for legacy HTML view routes
Object-level authorization via `security_manager.raise_for_access(...)` applies to data-bearing resources: dashboards, charts, datasets and datasources, queries, database and table access, and query contexts. Other resources (annotations, tags, CSS templates, reports, RLS rules, and similar) rely on the route-level decorator plus DAO `base_filters` for ownership scoping; the absence of `raise_for_access` on these resources is by design, not a finding. Code that omits the per-object gate on a route that returns or mutates a specific data-bearing object is in scope; code that follows the correct pattern for its resource class can still contain injection, SSRF, XSS, or other classes of finding unrelated to authorization, which are evaluated separately.
The full role and capability matrix, in-scope and out-of-scope class lists, and CVE aggregation rules are in [`SECURITY.md`](SECURITY.md). Defer to that document for any specifics.
**Requirements for findings filed by automated tooling**
Automated scanners (LLM-based code scanners, static analyzers, dependency tools) that file findings against this codebase must, in each finding, name:
1. The specific role and capability matrix row in [`SECURITY.md`](SECURITY.md) the finding believes is violated.
2. The principal the finding assumes the attacker holds (Public, Gamma, sql_lab, Alpha, Admin, Embedded guest token, or a custom role with explicit capability grants).
Findings that cannot identify both should be filed as questions, not vulnerabilities. This requirement exists to ensure every reported issue is testable against the published security model and to keep speculative or pattern-match-only reports out of the triage queue.
## Key Directories
```
@@ -128,6 +165,7 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
## Architecture Patterns
### Security & Features
- **Security model**: see the top-level [Security and Threat Model](#security-and-threat-model) section and [`SECURITY.md`](SECURITY.md)
- **RBAC**: Role-based access via Flask-AppBuilder
- **Feature flags**: Control feature rollouts
- **Row-level security**: SQL-based data access control

895
CHATBOT_SIP.md Normal file
View File

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

@@ -113,7 +113,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
# Some bash scripts needed throughout the layers
COPY --chmod=755 docker/*.sh /app/docker/
RUN pip install --no-cache-dir --upgrade uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Using uv as it's faster/simpler than pip
RUN uv venv /app/.venv
@@ -202,6 +202,8 @@ RUN mkdir -p /app/data && chown -R superset:superset /app/data
# Copy compiled things from previous stages
COPY --from=superset-node /app/superset/static/assets superset/static/assets
# Copy service.worker.js optionall as it doesn't exist when DEV_MODE=true
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
# TODO, when the next version comes out, use --exclude superset/translations
COPY superset superset

145
SECURITY.md Normal file
View File

@@ -0,0 +1,145 @@
<!--
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.
-->
# Security Policy
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
## Reporting Vulnerabilities
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
Apache Software Foundation takes a rigorous standpoint in resolving the security issues
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
pertaining to its features and functionality.
If you have any concern or believe you have found a vulnerability in Apache Superset,
please get in touch with the Apache Superset Security Team privately at
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
More details can be found on the ASF website at
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
**Submission Standards & AI Policy**
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
- Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
## Security Model
This section defines what Apache Superset considers a security issue and what it does not. It is the canonical reference for reporters, the Apache Superset Security Team, and any automated tool (LLM-based scanner, static analyzer, dependency tool) that needs to constrain its hypotheses to behaviors that genuinely violate the project's security policy.
The model is intentionally written in terms of principals, trust boundaries, and capability surface rather than in terms of specific files, functions, or libraries. New code paths inherit the model automatically.
### Trust Boundaries
Apache Superset's threat model assumes three trust boundaries.
1. *The Admin role* is a fully trusted operational principal. Anything an Admin can do through the documented user interface, REST API, or configuration system is an intended capability, not a vulnerability, even if individually powerful or destructive. The Admin role is, by policy, equivalent to operating-system-level trust over the Apache Superset application. This is unavoidable rather than aspirational: an Admin can, for example, register new database connections of arbitrary type, execute arbitrary SQL through SQL Lab, render Jinja templates that resolve to SQL or rendered HTML, and override application configuration. Granting Admin is functionally equivalent to granting shell access on the host, which is the reasoning behind treating it as a trust boundary in the sense of MITRE CNA Operational Rules 4.1.
2. *The operator* is whoever deploys, configures, and runs Apache Superset. Behaviors that depend on deployment-time decisions are the operator's responsibility, not Apache Superset's. This includes the values of secrets, the network reachability of the application and its data sources, the choice of database connectors and cache backends, the selection of feature flags, the destinations of notifications, and the trust placed in third-party plugins. Defaults that fail closed are the responsibility of the Apache Superset codebase. Defaults that fail open must be accompanied by a documented hardening requirement; applying that hardening is the operator's responsibility, while shipping an undocumented or unflagged fail-open default is a codebase issue.
3. *The Apache Superset codebase* is responsible for enforcing the role and capability matrix below across its product surface. A failure to enforce, anywhere in that surface, is in scope. The codebase's commitments are limited to the role and capability matrix and to controls Apache Superset's own documentation (this file and the linked Security documentation) explicitly positions as security boundaries; configurable hardening that operators can layer on top is treated separately under *Vulnerability Scope* below.
### Roles and Capabilities
Apache Superset ships with the following first-class principals. Detailed permission definitions live in the [Security documentation](https://superset.apache.org/docs/security).
| Principal | Read data | Write objects | Execute SQL | Manage databases | Manage users, roles, RLS |
|---|---|---|---|---|---|
| Public (anonymous) | none by default | no | no | no | no |
| Gamma | only granted datasets | own charts and dashboards on granted datasets | no by default (requires the `sql_lab` role) | no | no |
| Alpha | all data sources | own charts, dashboards, and datasets | no by default (requires the `sql_lab` role) | data upload to existing databases only | no |
| Admin | all | all | yes | yes | yes |
| Embedded guest token | data sources reachable through the embedded dashboards the token authorizes | no | no | no | no |
The `sql_lab` role is *additive*: it grants the SQL Lab permission set on top of the base role above, and is the only path by which Gamma or Alpha gain SQL execution capability. Database access is still scoped per the base role's grants. Admin includes SQL Lab access by default.
Deployments may grant or revoke individual view-menu permissions, which shifts the boundary for that deployment but does not redefine the model. Any custom role created by an operator inherits the same principle: its capabilities are whatever the operator has explicitly granted it. The Public principal follows the same rule: operators may grant the Public role read access to specific datasets or dashboards (typically for anonymous reporting use cases), which shifts the boundary for that deployment without redefining the model.
### Vulnerability Scope
The test for whether a finding is in scope is a single question:
> *Does this finding let a principal perform an action the role and capability matrix above does not entitle them to?*
If yes, it is in scope. If no, it is out of scope. The lists below apply that test to the classes Apache Superset most commonly receives reports about; they are illustrative, not exhaustive.
*In Scope*
- A user, embedded guest, or anonymous visitor reads, modifies, or deletes data outside their granted set. Includes object-level access bypass on charts, dashboards, datasets, saved queries, tags, annotations, and similar per-object endpoints, and row-level-security rule bypass.
- A user supplies input that the codebase should sanitize or parameterize but does not, causing arbitrary SQL, template code, or scripts to execute. Includes injection through Jinja templates, SQL-construction paths, and any field the codebase passes to a query engine or template engine.
- A user bypasses authentication, fixates or reuses another user's session, or reaches an authenticated endpoint without logging in.
- An embedded guest token authorizes actions outside the dashboard it was issued for, or can be forged, replayed, or escalated to a higher principal.
- Apache Superset, acting on behalf of an unprivileged user, fetches an outbound URL the user controls in a feature where Apache Superset itself, not the operator, controls the outbound destination set (server-side request forgery).
- An Apache Superset default fails open without an accompanying documented hardening requirement. The codebase is responsible for shipping fail-closed defaults or for documenting the hardening required when a default fails open; failures of that responsibility are in scope (see *Trust Boundaries*).
- A user bypasses a control Apache Superset documents specifically as a security boundary. This includes row-level security, the access checks tied to the role and capability matrix above, and any feature whose documentation positions it as security-relevant. The codebase commits to enforcing those controls; bypasses are in scope regardless of which principal triggers them.
- A user causes a script to execute in another user's browser through a field the codebase renders to that other user (cross-site scripting), or causes cross-origin leakage of authenticated session state or data.
- A user reaches a route, page, or API endpoint that requires a role they do not have.
*Out of Scope*
- 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.
- 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.
- Missing security headers, banner or version disclosure, user or object enumeration through error messages or timing, and similar low-impact information disclosure that does not enable a further concrete exploit.
- Bypasses of configurable defense-in-depth hardening that Apache Superset does not document as a security boundary. Apache Superset is not a SQL or database firewall: operator-deployable filters such as SQL function or table denylists, URI restrictions on already-authorized database connectors, and similar belt-and-braces controls are provided to let operators layer hardening on top of the role and capability matrix, not as firewall-grade guarantees the codebase commits to. Findings against such hardening are improvements, not vulnerabilities, unless the documentation positions the specific control as security-relevant.
- Hardening suggestions that improve defense in depth but do not violate the security model.
Findings in third-party dependencies fall into two cases. A finding in a transitive dependency, or in an operator-selected dependency that Apache Superset does not ship, is out of scope and should be reported to the dependency's maintainers. A finding caused by Apache Superset pinning a known-vulnerable version of a direct dependency it ships, or using a dependency in a way that creates a vulnerability the dependency itself does not have, remains in scope. Dependency findings in the official Apache Superset Docker image that fall into the first case can be remediated by extending the image at release time.
When uncertain whether a finding falls in scope, please file it through the reporting process above. The triage team will classify it and explain the reasoning if it is closed as out of scope.
**Outcome of Reports**
Reports that are deemed out of scope for a CVE but represent valid security best practices or hardening opportunities are typically converted into public GitHub issues, where the community can contribute fixes alongside the maintainers. The triage decision and reasoning are communicated back to the reporter in either case.
**Vulnerability Aggregation & CVE Attribution**
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
**Your responsible disclosure and collaboration are invaluable.**
## Extra Information
- [Apache Superset documentation](https://superset.apache.org/docs/security)
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)
**For LLM agents and automated scanners**
This file is the canonical source of truth for what Apache Superset considers a security vulnerability. Automated tooling (LLM-based code scanners, static analyzers, dependency tools) should treat the Security Model section as authoritative when classifying findings. The repository's [AGENTS.md](AGENTS.md) file contains a short pointer to this document for agents that read AGENTS.md as their entry point.

322
TICKETS.md Normal file
View File

@@ -0,0 +1,322 @@
# 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,6 +24,12 @@ assists people when migrating to a new version.
## Next
### 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.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:

View File

@@ -18,6 +18,8 @@
# Configuration for docker-compose-light.yml - disables Redis and uses minimal services
# Import all settings from the main config first
import os
from flask_caching.backends.filesystemcache import FileSystemCache
from superset_config import * # noqa: F403
@@ -36,3 +38,32 @@ THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
# Disable Celery entirely for lightweight mode
CELERY_CONFIG = None # type: ignore[assignment,misc]
# Honor SUPERSET_FEATURE_<NAME> env vars on top of any flags inherited from
# superset_config. Lets local dev/e2e enable features (e.g. EMBEDDED_SUPERSET)
# without editing shipped config files. Only the literal string "true"
# (case-insensitive) is treated as enabled — "1"/"yes"/"on" are not, matching
# the strict-string convention used elsewhere in Superset's env parsing.
FEATURE_FLAGS = {
**FEATURE_FLAGS, # noqa: F405
**{
name[len("SUPERSET_FEATURE_") :]: value.strip().lower() == "true"
for name, value in os.environ.items()
if name.startswith("SUPERSET_FEATURE_")
},
}
if os.environ.get("SUPERSET_FEATURE_EMBEDDED_SUPERSET", "").strip().lower() == "true":
# Disable Talisman so /embedded/<uuid> doesn't return X-Frame-Options:SAMEORIGIN.
# Without this, browsers refuse to render Superset inside an iframe from a
# different origin (i.e. the embedded SDK use case). Production/CI configures
# Talisman with explicit `frame-ancestors`; for the lightweight local stack we
# just turn it off.
TALISMAN_ENABLED = False
# Guest tokens (used by the embedded SDK) inherit the "Public" role's perms.
# Out of the box Public has zero perms, so embedded dashboards immediately fail
# their first call (`/api/v1/me/roles/`) with 403. Mirror Public to Gamma —
# the standard read-only viewer role — so the embedded flow can authenticate
# and load dashboard data in local dev.
PUBLIC_ROLE_LIKE = "Gamma"

View File

@@ -88,7 +88,6 @@ using our `docker compose` constructs to support production-type use-cases. For
environments, we recommend using [minikube](https://minikube.sigs.k8s.io/docs/start/) along
our [installing on k8s](https://superset.apache.org/docs/installation/running-on-kubernetes)
documentation.
configured to be secure.
:::
### Supported environment variables

View File

@@ -335,6 +335,92 @@ npm run build-translation
pybabel compile -d superset/translations
```
### Backfilling missing translations with AI
For languages with many untranslated strings, the repo includes a script that
uses Claude AI to generate draft translations for any missing entries. All
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
comment so that human reviewers know they need to be checked before merging.
#### Prerequisites
```bash
pip install -r superset/translations/requirements.txt
```
Claude Code must be installed and authenticated (`claude --version` should
work). The script calls `claude -p` internally — no separate API key is needed.
#### Step 1 — Build the translation index
The index captures every already-translated string in every language and
serves as cross-language context for the AI. Rebuild it whenever `.po` files
change significantly:
```bash
python scripts/translations/build_translation_index.py
# Writes: superset/translations/translation_index.json
```
#### Step 2 — Preview with a dry run
Check what would be translated without writing anything:
```bash
python scripts/translations/backfill_po.py --lang fr --limit 20 --dry-run
```
Output shows each string, its translation, and a context tag:
- No tag — 3+ reference languages available (high confidence)
- `[ctx:N]` — only N other languages have this string (lower confidence)
- `[ctx:0]` — no other language has this string yet; English alone used
#### Step 3 — Run the backfill
```bash
python scripts/translations/backfill_po.py --lang fr
```
Options:
| Flag | Default | Description |
|------|---------|-------------|
| `--lang LANG` | required | ISO language code (`fr`, `de`, `ja`, …) |
| `--batch-size N` | 50 | Strings per Claude request |
| `--limit N` | unlimited | Stop after N entries |
| `--min-context N` | 0 | Skip entries with fewer than N reference translations |
| `--model MODEL` | `claude-sonnet-4-6` | Claude model to use |
| `--dry-run` | off | Print without writing |
| `--no-fuzzy` | off | Don't mark entries as fuzzy |
Use `--min-context 2` to skip strings that have fewer than 2 reference
translations in other languages. Those strings are more likely to be ambiguous
(short labels, UI fragments) where the correct meaning can't be inferred
without additional context.
#### Step 4 — Review and commit
Open the target `.po` file and search for `fuzzy`. For each generated entry:
1. Verify the translation is correct for the UI context.
2. Remove the `# Machine-translated via backfill_po.py` comment and the
`#, fuzzy` flag line once you are satisfied.
3. If the translation is wrong, correct the `msgstr` before removing the flag.
4. Commit the `.po` file — do **not** commit `translation_index.json` (it is
gitignored and regenerated locally).
#### Running via npm
From `superset-frontend/`:
```bash
# Rebuild index
npm run translations:build-index
# Backfill (pass arguments after --)
npm run translations:backfill -- --lang fr --dry-run
```
## Linting
### Python

View File

@@ -25,9 +25,17 @@
command = "yarn install && yarn build"
# Output directory (relative to base)
publish = "build"
# Skip builds when no docs changes (exit 0 = skip, exit 1 = build)
# Checks for changes in docs/ and README.md (which gets pulled into docs)
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../README.md"
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
# Checks for changes in docs/ and README.md (which gets pulled into docs).
#
# $CACHED_COMMIT_REF is the last *deployed* commit. On a PR's first build it
# is empty, so the original `git diff` errored and Netlify fell back to
# building -- which is why every PR built a docs preview once even with no
# docs changes. When it is empty we instead diff the whole branch against its
# merge-base with master, so non-docs PRs are skipped from the very first
# build. Subsequent builds (and the master production build) keep the cheaper
# incremental $CACHED_COMMIT_REF diff. Any failure exits non-zero -> build.
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" "$COMMIT_REF" -- . ../README.md; else git fetch origin master --depth=100 >/dev/null 2>&1; git diff --quiet "$(git merge-base origin/master "$COMMIT_REF" 2>/dev/null || echo origin/master)" "$COMMIT_REF" -- . ../README.md; fi'
[build.environment]
# Node version matching docs/.nvmrc

View File

@@ -70,9 +70,9 @@
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"@swc/core": "^1.15.40",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.31",
"baseline-browser-mapping": "^2.10.32",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
@@ -101,7 +101,7 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@typescript-eslint/parser": "^8.60.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
@@ -109,8 +109,8 @@
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.4",
"webpack": "^5.107.0"
"typescript-eslint": "^8.60.0",
"webpack": "^5.107.2"
},
"browserslist": {
"production": [
@@ -128,7 +128,13 @@
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"swagger-client": "3.37.3"
"swagger-client": "3.37.3",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"yaml": "1.10.3",
"uuid": "11.1.1",
"serialize-javascript": "7.0.5",
"d3-color": "3.1.0"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -4033,86 +4033,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz#d84134fb80417d41128739f0b9014542e3ed9dd3"
integrity sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==
"@swc/core-darwin-arm64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz#b05d715b04c4fd47baf59288233da85a683cc0bc"
integrity sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==
"@swc/core-darwin-x64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz#0badb9834071f1c6005986571d4a96359c1d7cd0"
integrity sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==
"@swc/core-darwin-x64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz#3180daef5c1e47b435f8edd084509e0a5c0d883b"
integrity sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==
"@swc/core-linux-arm-gnueabihf@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz#b7577a825b59d98b6a9a5c991d842046efe1c34a"
integrity sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==
"@swc/core-linux-arm-gnueabihf@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz#18fcd3c70e48fdfae07c9f18751b1409ce1e5e84"
integrity sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==
"@swc/core-linux-arm64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz#304c48321494a18c67b2913c273b08674ee70d8c"
integrity sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==
"@swc/core-linux-arm64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz#26304933922f2a8e3194770e404403fc25a19c89"
integrity sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==
"@swc/core-linux-arm64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz#d116cbc04ccb4f4ee810da6bca79d4423605dbcd"
integrity sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==
"@swc/core-linux-arm64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz#3402dfba04ba7b8ea81f243e2f8fa2c336b54d03"
integrity sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==
"@swc/core-linux-ppc64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz#f5354dba36db9414305bab344c817d57b8b457c2"
integrity sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==
"@swc/core-linux-ppc64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz#b3df9065cad352328c1eeef08a28fc9fe98785aa"
integrity sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==
"@swc/core-linux-s390x-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz#016df9f4c9d7fd65b85ca9c558c5aec341f06da0"
integrity sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==
"@swc/core-linux-s390x-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz#58e5b601f641dde81b30626ef66a668701ec918f"
integrity sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==
"@swc/core-linux-x64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz#49f36558ede072e71999aa37f123367daed2a662"
integrity sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==
"@swc/core-linux-x64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz#cf057dce0c148c53f2d30152baaf60ea29e5d59c"
integrity sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==
"@swc/core-linux-x64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz#b096665f5cfeee2612325f301da5c1590b10d8f3"
integrity sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==
"@swc/core-linux-x64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz#21fb1a4d0193e9bbcd1469ecd36166d2e96e4006"
integrity sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==
"@swc/core-win32-arm64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz#f3101263a0dbaa173ec47638c9719d0b89838bd2"
integrity sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==
"@swc/core-win32-arm64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz#1dba23b2b0db86b3d6d65da2abd627cc607a1fbc"
integrity sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==
"@swc/core-win32-ia32-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz#eb981ef5613d42c9220559bdb0c8bc58cf6c3eb9"
integrity sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==
"@swc/core-win32-ia32-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz#b2da1e33165d469467b1046a2189db468da488eb"
integrity sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==
"@swc/core-win32-x64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz#a2fed9956933027ceb368857bac4bb4ee203d47c"
integrity sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==
"@swc/core-win32-x64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz#3563f7e8ce8708f5fda43eb8e0956ef11e0da320"
integrity sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==
"@swc/core@^1.15.33", "@swc/core@^1.7.39":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.33.tgz#2a6571c8aca961925f14beae52b3f43c18370fc6"
integrity sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==
"@swc/core@^1.15.40", "@swc/core@^1.7.39":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.40.tgz#941c949aa88c0d8d291f102f519f3c2c77701b90"
integrity sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.26"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.33"
"@swc/core-darwin-x64" "1.15.33"
"@swc/core-linux-arm-gnueabihf" "1.15.33"
"@swc/core-linux-arm64-gnu" "1.15.33"
"@swc/core-linux-arm64-musl" "1.15.33"
"@swc/core-linux-ppc64-gnu" "1.15.33"
"@swc/core-linux-s390x-gnu" "1.15.33"
"@swc/core-linux-x64-gnu" "1.15.33"
"@swc/core-linux-x64-musl" "1.15.33"
"@swc/core-win32-arm64-msvc" "1.15.33"
"@swc/core-win32-ia32-msvc" "1.15.33"
"@swc/core-win32-x64-msvc" "1.15.33"
"@swc/core-darwin-arm64" "1.15.40"
"@swc/core-darwin-x64" "1.15.40"
"@swc/core-linux-arm-gnueabihf" "1.15.40"
"@swc/core-linux-arm64-gnu" "1.15.40"
"@swc/core-linux-arm64-musl" "1.15.40"
"@swc/core-linux-ppc64-gnu" "1.15.40"
"@swc/core-linux-s390x-gnu" "1.15.40"
"@swc/core-linux-x64-gnu" "1.15.40"
"@swc/core-linux-x64-musl" "1.15.40"
"@swc/core-win32-arm64-msvc" "1.15.40"
"@swc/core-win32-ia32-msvc" "1.15.40"
"@swc/core-win32-x64-msvc" "1.15.40"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -4812,100 +4812,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.59.4", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707"
integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/type-utils" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/type-utils" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.59.4", "@typescript-eslint/parser@^8.59.3":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c"
integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==
"@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==
dependencies:
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/scope-manager" "8.60.0"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e"
integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==
"@typescript-eslint/project-service@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.59.4"
"@typescript-eslint/types" "^8.59.4"
"@typescript-eslint/tsconfig-utils" "^8.60.0"
"@typescript-eslint/types" "^8.60.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046"
integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==
"@typescript-eslint/scope-manager@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be"
integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==
"@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/type-utils@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed"
integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==
"@typescript-eslint/tsconfig-utils@^8.60.0":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/type-utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e"
integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==
"@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/typescript-estree@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea"
integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==
"@typescript-eslint/types@^8.60.0":
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==
dependencies:
"@typescript-eslint/project-service" "8.59.4"
"@typescript-eslint/tsconfig-utils" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/visitor-keys" "8.59.4"
"@typescript-eslint/project-service" "8.60.0"
"@typescript-eslint/tsconfig-utils" "8.60.0"
"@typescript-eslint/types" "8.60.0"
"@typescript-eslint/visitor-keys" "8.60.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e"
integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==
"@typescript-eslint/utils@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.59.4"
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@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.59.4":
version "8.59.4"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355"
integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==
"@typescript-eslint/visitor-keys@8.60.0":
version "8.60.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
dependencies:
"@typescript-eslint/types" "8.59.4"
"@typescript-eslint/types" "8.60.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -5568,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.31, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.31"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz#9c6825f052601ce6974a90dd49683b1726887b0b"
integrity sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==
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==
batch@0.6.1:
version "0.6.1"
@@ -6538,14 +6548,9 @@ d3-chord@3:
dependencies:
d3-path "1 - 3"
"d3-color@1 - 2":
version "2.0.0"
resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz"
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
"d3-color@1 - 3", d3-color@3:
"d3-color@1 - 2", "d3-color@1 - 3", d3-color@3, d3-color@3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
d3-contour@4:
@@ -7253,10 +7258,10 @@ encodeurl@~2.0.0:
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.21.4:
version "5.21.5"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz#8f80167d009d8f01267ad61035e59fe5c94ac3a6"
integrity sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==
enhanced-resolve@^5.22.0:
version "5.22.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz#c34bc3f414298496fc244b21bbe316440782da17"
integrity sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.3.3"
@@ -9676,10 +9681,10 @@ locate-path@^7.1.0:
dependencies:
p-locate "^6.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash-es@4.18.1, lodash-es@^4.17.21:
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
lodash.debounce@^4, lodash.debounce@^4.0.8:
version "4.0.8"
@@ -9701,12 +9706,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
lodash@4.17.21, lodash@4.18.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
@@ -12270,14 +12270,7 @@ pvutils@^1.1.3, pvutils@^1.1.5:
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c"
integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==
qs@^6.12.3:
version "6.14.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c"
integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==
dependencies:
side-channel "^1.1.0"
qs@~6.15.1:
qs@^6.12.3, qs@~6.15.1:
version "6.15.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9"
integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==
@@ -13374,12 +13367,10 @@ serialize-error@^8.1.0:
dependencies:
type-fest "^0.20.2"
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
serialize-javascript@7.0.5, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "7.0.5"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1"
integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==
serve-handler@^6.1.7:
version "6.1.7"
@@ -14391,15 +14382,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.59.4:
version "8.59.4"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz#834e3b53f4d1a764a985ceb8592c4a95d6a8da7c"
integrity sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==
typescript-eslint@^8.60.0:
version "8.60.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.59.4"
"@typescript-eslint/parser" "8.59.4"
"@typescript-eslint/typescript-estree" "8.59.4"
"@typescript-eslint/utils" "8.59.4"
"@typescript-eslint/eslint-plugin" "8.60.0"
"@typescript-eslint/parser" "8.60.0"
"@typescript-eslint/typescript-estree" "8.60.0"
"@typescript-eslint/utils" "8.60.0"
typescript@~6.0.3:
version "6.0.3"
@@ -14733,15 +14724,10 @@ utils-merge@1.0.1:
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@8.3.2, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
"uuid@^11.1.0 || ^12 || ^13 || ^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
uuid@11.1.1, uuid@8.3.2, "uuid@^11.1.0 || ^12 || ^13 || ^14.0.0", uuid@^8.3.2:
version "11.1.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad"
integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==
uvu@^0.5.0:
version "0.5.6"
@@ -14954,20 +14940,20 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.4.1.tgz#009d110999ebd9fb3a6fa8d32eec6f84d940e65d"
integrity sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==
webpack-sources@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.0.tgz#87bf7f5801a4e985b1f1c92b64b9620a02f76d08"
integrity sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.107.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.0.tgz#9e0d8d8baf24e76f058103f4f06ac6bb528b645a"
integrity sha512-PSxeHk/dmLYZlnTU+vL1Gej6Evg5RNtl3flhxBresfznFnzxinHMzHKloHnywM/3ouQv7/AlZCswWDIkNSggUA==
webpack@^5.107.2, webpack@^5.88.1, webpack@^5.95.0:
version "5.107.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.2.tgz#dea14dcb177b46b29de15f952f7303691ee2b596"
integrity sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==
dependencies:
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
@@ -14978,7 +14964,7 @@ webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
acorn-import-phases "^1.0.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.21.4"
enhanced-resolve "^5.22.0"
es-module-lexer "^2.1.0"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -14991,7 +14977,7 @@ webpack@^5.107.0, webpack@^5.88.1, webpack@^5.95.0:
tapable "^2.3.0"
terser-webpack-plugin "^5.5.0"
watchpack "^2.5.1"
webpack-sources "^3.4.1"
webpack-sources "^3.5.0"
webpackbar@^7.0.0:
version "7.0.0"
@@ -15146,9 +15132,9 @@ ws@^7.3.1:
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.18.0, ws@^8.2.3:
version "8.18.3"
resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz"
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
version "8.20.1"
resolved "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz"
integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==
wsl-utils@^0.1.0:
version "0.1.0"
@@ -15216,12 +15202,7 @@ yaml-ast-parser@0.0.43:
resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz"
integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
yaml@1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^1.10.0:
yaml@1.10.2, yaml@1.10.3, yaml@^1.10.0:
version "1.10.3"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3"
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==

138
extensions/chat/README.md Normal file
View File

@@ -0,0 +1,138 @@
<!--
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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,30 @@
/**
* 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 Federation entry. The host loads `./index` and invokes the factory;
* the side effect below registers the view + commands. The host's loader
* intercepts registerView calls to collect disposables for deactivation, so
* returning the master Disposable here is also captured by the test harness
* for direct assertion.
*/
import { activate } from './activate';
export const disposable = activate();

View File

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

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

@@ -0,0 +1,46 @@
/**
* 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 registry of in-flight stream AbortControllers.
*
* Lets the master Disposable abort any running stream even when the panel
* is unmounted by a route change or by re-activation of the extension.
*/
const active = new Set<AbortController>();
export const registerActiveController = (c: AbortController): void => {
active.add(c);
};
export const unregisterActiveController = (c: AbortController): void => {
active.delete(c);
};
export const abortAllActiveControllers = (): void => {
active.forEach(c => {
try {
c.abort();
} catch {
// ignore — abort() should not throw, but stay defensive.
}
});
active.clear();
};

View File

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

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

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

138
extensions/chat2/README.md Normal file
View File

@@ -0,0 +1,138 @@
<!--
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

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

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

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

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

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

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

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

@@ -0,0 +1,46 @@
/**
* 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 Alt chatbot"
style={{
width: 56,
height: 56,
borderRadius: '50%',
border: 'none',
background: '#2da44e',
color: '#fff',
fontSize: 24,
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
}}
>
?
</button>
);

View File

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

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

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

@@ -0,0 +1,30 @@
/**
* 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 Federation entry. The host loads `./index` and invokes the factory;
* the side effect below registers the view + commands. The host's loader
* intercepts registerView calls to collect disposables for deactivation, so
* returning the master Disposable here is also captured by the test harness
* for direct assertion.
*/
import { activate } from './activate';
export const disposable = activate();

View File

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

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

@@ -0,0 +1,46 @@
/**
* 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 registry of in-flight stream AbortControllers.
*
* Lets the master Disposable abort any running stream even when the panel
* is unmounted by a route change or by re-activation of the extension.
*/
const active = new Set<AbortController>();
export const registerActiveController = (c: AbortController): void => {
active.add(c);
};
export const unregisterActiveController = (c: AbortController): void => {
active.delete(c);
};
export const abortAllActiveControllers = (): void => {
active.forEach(c => {
try {
c.abort();
} catch {
// ignore — abort() should not throw, but stay defensive.
}
});
active.clear();
};

View File

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

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

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

@@ -39,7 +39,7 @@ dependencies = [
"apache-superset-core",
"backoff>=1.8.0",
"celery>=5.3.6, <6.0.0",
"click>=8.0.3",
"click>=8.4.0",
"click-option-group",
"colorama",
"flask-cors>=6.0.0, <7.0",
@@ -58,7 +58,7 @@ dependencies = [
"flask-wtf>=1.1.0, <2.0",
"geopy",
"greenlet>=3.0.3, <=3.5.0",
"gunicorn>=22.0.0; sys_platform != 'win32'",
"gunicorn>=25.3.0, <26; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# holidays>=0.45 required for security fix
"holidays>=0.45, <1",
@@ -66,12 +66,12 @@ dependencies = [
"isodate",
"jsonpath-ng>=1.6.1, <2",
"Mako>=1.2.2",
"markdown>=3.0",
"markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
"marshmallow>=3.0, <4",
"marshmallow-union>=0.1",
"msgpack>=1.0.0, <1.2",
"nh3>=0.2.11, <0.4",
"nh3>=0.3.5, <0.4",
"numpy>1.23.5, <2.3",
"packaging",
# --------------------------
@@ -101,9 +101,9 @@ dependencies = [
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=28.10.0, <29",
"sqlglot>=30.8.0, <31",
# newer pandas needs 0.9+
"tabulate>=0.9.0, <1.0",
"tabulate>=0.10.0, <1.0",
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
@@ -123,7 +123,7 @@ bigquery = [
]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [
"superset-engine-d1>=0.1.0",
"sqlalchemy-d1>=0.1.0",
@@ -135,11 +135,11 @@ databricks = [
"databricks-sqlalchemy==1.0.5",
]
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.4, <2"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
@@ -147,6 +147,7 @@ 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.
@@ -190,7 +191,7 @@ risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
sqlite = ["syntaqlite>=0.1.0"]
sqlite = ["syntaqlite>=0.1.0,<0.5.0"]
spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
@@ -220,11 +221,12 @@ development = [
"openapi-spec-validator",
"parameterized",
"pip",
"polib", # used by scripts/translations/ and their unit tests
"pre-commit",
"progress>=1.5,<2",
"psutil",
"pyfakefs",
"pyinstrument>=4.0.2,<6",
"pyinstrument>=5.1.2,<6",
"pylint",
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
"pytest-asyncio",
@@ -234,7 +236,7 @@ development = [
"ruff",
"sqloxide",
"statsd",
"syntaqlite>=0.1.0",
"syntaqlite>=0.4.2,<0.5.0",
]
[project.urls]

View File

@@ -60,7 +60,7 @@ cffi==2.0.0
# pynacl
charset-normalizer==3.4.2
# via requests
click==8.2.1
click==8.4.1
# via
# apache-superset (pyproject.toml)
# celery
@@ -166,7 +166,7 @@ greenlet==3.1.1
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==23.0.0
gunicorn==25.3.0
# via apache-superset (pyproject.toml)
h11==0.16.0
# via wsproto
@@ -176,7 +176,7 @@ holidays==0.82
# via apache-superset (pyproject.toml)
humanize==4.12.3
# via apache-superset (pyproject.toml)
idna==3.10
idna==3.15
# via
# email-validator
# requests
@@ -208,12 +208,12 @@ kombu==5.5.3
# via celery
limits==5.1.0
# via flask-limiter
mako==1.3.11
mako==1.3.12
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
# alembic
markdown==3.8.1
markdown==3.10.2
# via apache-superset (pyproject.toml)
markdown-it-py==3.0.0
# via rich
@@ -241,7 +241,7 @@ msgpack==1.0.8
# via apache-superset (pyproject.toml)
msgspec==0.19.0
# via flask-session
nh3==0.2.21
nh3==0.3.5
# via apache-superset (pyproject.toml)
numexpr==2.10.2
# via -r requirements/base.in
@@ -415,13 +415,13 @@ sqlalchemy-utils==0.42.0
# apache-superset (pyproject.toml)
# apache-superset-core
# flask-appbuilder
sqlglot==28.10.0
sqlglot==30.8.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.9.0
tabulate==0.10.0
# via apache-superset (pyproject.toml)
trio==0.30.0
# via
@@ -451,7 +451,7 @@ tzdata==2025.2
# pandas
url-normalize==2.2.1
# via requests-cache
urllib3==2.6.3
urllib3==2.7.0
# via
# -r requirements/base.in
# requests

View File

@@ -52,7 +52,7 @@ attrs==25.3.0
# referencing
# requests-cache
# trio
authlib==1.6.9
authlib==1.6.12
# via fastmcp
babel==2.17.0
# via
@@ -130,7 +130,7 @@ charset-normalizer==3.4.2
# via
# -c requirements/base-constraint.txt
# requests
click==8.2.1
click==8.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -183,6 +183,7 @@ cryptography==46.0.7
# -c requirements/base-constraint.txt
# apache-superset
# authlib
# joserfc
# paramiko
# pyjwt
# pyopenssl
@@ -219,7 +220,7 @@ docstring-parser==0.17.0
# via cyclopts
docutils==0.22.2
# via rich-rst
duckdb==1.4.2
duckdb==1.5.3
# via
# apache-superset
# duckdb-engine
@@ -317,7 +318,7 @@ flask-wtf==1.2.2
# -c requirements/base-constraint.txt
# apache-superset
# flask-appbuilder
fonttools==4.55.0
fonttools==4.60.2
# via matplotlib
freezegun==1.5.1
# via apache-superset
@@ -346,6 +347,7 @@ google-auth==2.43.0
# google-api-core
# google-auth-oauthlib
# google-cloud-bigquery
# google-cloud-bigquery-storage
# google-cloud-core
# pandas-gbq
# pydata-google-auth
@@ -360,7 +362,7 @@ google-cloud-bigquery==3.27.0
# apache-superset
# pandas-gbq
# sqlalchemy-bigquery
google-cloud-bigquery-storage==2.19.1
google-cloud-bigquery-storage==2.26.0
# via pandas-gbq
google-cloud-core==2.4.1
# via google-cloud-bigquery
@@ -388,7 +390,7 @@ grpcio==1.71.0
# grpcio-status
grpcio-status==1.60.1
# via google-api-core
gunicorn==23.0.0
gunicorn==25.3.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -421,7 +423,7 @@ humanize==4.12.3
# apache-superset
identify==2.5.36
# via pre-commit
idna==3.10
idna==3.15
# via
# -c requirements/base-constraint.txt
# anyio
@@ -470,6 +472,8 @@ jmespath==1.1.0
# via
# boto3
# botocore
joserfc==1.6.8
# via apache-superset
jsonpath-ng==1.7.0
# via
# -c requirements/base-constraint.txt
@@ -506,12 +510,12 @@ limits==5.1.0
# via
# -c requirements/base-constraint.txt
# flask-limiter
mako==1.3.11
mako==1.3.12
# via
# -c requirements/base-constraint.txt
# alembic
# apache-superset
markdown==3.8.1
markdown==3.10.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -565,7 +569,7 @@ msgspec==0.19.0
# flask-session
mysqlclient==2.2.6
# via apache-superset
nh3==0.2.21
nh3==0.3.5
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -677,6 +681,8 @@ ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0
# via apache-superset
polyline==2.0.2
# via
# -c requirements/base-constraint.txt
@@ -699,7 +705,7 @@ proto-plus==1.25.0
# via
# google-api-core
# google-cloud-bigquery-storage
protobuf==4.25.8
protobuf==5.29.6
# via
# google-api-core
# google-cloud-bigquery-storage
@@ -765,7 +771,7 @@ pygments==2.20.0
# rich
pyhive==0.7.0
# via apache-superset
pyinstrument==4.4.0
pyinstrument==5.1.2
# via apache-superset
pyjwt==2.12.0
# via
@@ -835,9 +841,9 @@ python-dotenv==1.2.2
# apache-superset
# fastmcp
# pydantic-settings
python-ldap==3.4.4
python-ldap==3.4.5
# via apache-superset
python-multipart==0.0.20
python-multipart==0.0.29
# via mcp
pytz==2025.2
# via
@@ -985,7 +991,7 @@ sqlalchemy-utils==0.42.0
# apache-superset
# apache-superset-core
# flask-appbuilder
sqlglot==28.10.0
sqlglot==30.8.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1002,9 +1008,9 @@ starlette==0.49.1
# via mcp
statsd==4.0.1
# via apache-superset
syntaqlite==0.1.0
syntaqlite==0.4.2
# via apache-superset
tabulate==0.9.0
tabulate==0.10.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1069,7 +1075,7 @@ url-normalize==2.2.1
# via
# -c requirements/base-constraint.txt
# requests-cache
urllib3==2.6.3
urllib3==2.7.0
# via
# -c requirements/base-constraint.txt
# botocore
@@ -1087,7 +1093,7 @@ vine==5.1.0
# amqp
# celery
# kombu
virtualenv==20.29.2
virtualenv==20.36.1
# via pre-commit
watchdog==6.0.0
# via

View File

@@ -31,7 +31,9 @@ PATTERNS = {
r"^superset/",
r"^scripts/",
r"^setup\.py",
r"^pyproject\.toml$",
r"^requirements/.+\.txt",
r"^pyproject\.toml",
r"^.pylintrc",
],
"frontend": [
@@ -155,7 +157,7 @@ def main(event_type: str, sha: str, repo: str) -> None:
def get_git_sha() -> str:
return os.getenv("GITHUB_SHA") or subprocess.check_output( # noqa: S603
["git", "rev-parse", "HEAD"] # noqa: S607
["git", "rev-parse", "HEAD"] # noqa: S603, S607
).strip().decode("utf-8")

View File

@@ -55,7 +55,7 @@ if [ ${#js_ts_files[@]} -gt 0 ]; then
echo "$output" >&2
exit 1
}
[ -n "$output" ] && echo "$output"
if [ -n "$output" ]; then echo "$output"; fi
else
echo "No JavaScript/TypeScript files to lint"
fi

View File

@@ -0,0 +1,686 @@
# 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.
"""Backfill missing translations in a .po file using Claude AI.
For each untranslated (empty msgstr) entry in the target language, the script
sends the English source string along with all available translations in other
languages to Claude as context, then writes the AI-generated translation back
into the .po file marked as #, fuzzy for human review.
Usage:
# Build the translation index first (one-time or when .po files change)
python scripts/translations/build_translation_index.py
# Backfill French translations
python scripts/translations/backfill_po.py --lang fr
# Dry run (print what would be translated without writing)
python scripts/translations/backfill_po.py --lang de --dry-run
# Limit to 100 entries and use a specific model
python scripts/translations/backfill_po.py --lang es --limit 100 \
--model claude-opus-4-6
Options:
--lang LANG ISO language code to backfill (required)
--batch-size N Number of strings per Claude request (default: 50)
--limit N Stop after translating N entries (default: unlimited)
--min-context N Skip entries with fewer than N existing translations across
reference languages (default: 0 — translate everything)
--model MODEL Claude model ID (default: claude-sonnet-4-6)
--index PATH Path to translation_index.json (default: auto-detect)
--dry-run Print translations without writing to .po file
--no-fuzzy Do not mark generated translations as fuzzy (default: mark fuzzy)
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
try:
import polib # type: ignore[import-untyped]
except ImportError:
print("polib is required. Run: pip install polib", file=sys.stderr)
sys.exit(1)
TRANSLATIONS_DIR = Path(__file__).parent.parent.parent / "superset" / "translations"
DEFAULT_INDEX = TRANSLATIONS_DIR / "translation_index.json"
DEFAULT_MODEL = "claude-sonnet-4-6"
DEFAULT_BATCH_SIZE = 50
_ASF_LICENSE_HEADER = """\
# 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.
"""
# Language names for the prompt, keyed by ISO code
LANGUAGE_NAMES: dict[str, str] = {
"ar": "Arabic",
"ca": "Catalan",
"de": "German",
"es": "Spanish",
"fa": "Persian (Farsi)",
"fr": "French",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"mi": "Māori",
"nl": "Dutch",
"pl": "Polish",
"pt": "Portuguese",
"pt_BR": "Brazilian Portuguese",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"tr": "Turkish",
"uk": "Ukrainian",
"zh": "Chinese (Simplified)",
"zh_TW": "Chinese (Traditional)",
}
def _ensure_license_header(po_path: Path, *, dry_run: bool = False) -> None:
"""Prepend the ASF license header to the .po file if it is missing."""
content = po_path.read_text(encoding="utf-8")
if "Licensed to the Apache Software Foundation" not in content:
if dry_run:
print(
f"[dry-run] Would add ASF license header to {po_path}", file=sys.stderr
)
else:
po_path.write_text(_ASF_LICENSE_HEADER + content, encoding="utf-8")
print(f"Added ASF license header to {po_path}", file=sys.stderr)
def _lang_name(code: str) -> str:
"""Return a human-readable language name for an ISO language code."""
return LANGUAGE_NAMES.get(code, code)
def _plural_key(msgid: str, msgid_plural: str) -> str:
"""Build the translation index key used for pluralized entries."""
return f"{msgid}\x00{msgid_plural}"
def _is_missing(entry: polib.POEntry) -> bool:
"""Return True for entries that need a translation."""
if entry.obsolete:
return False
if entry.msgid_plural:
return not any(v for v in entry.msgstr_plural.values())
return not entry.msgstr
def _context_langs(
item: dict[str, Any], index: dict[str, Any], target_lang: str
) -> list[str]:
"""Return sorted list of language codes that have translations for this entry."""
key = item["index_key"]
if key not in index:
return []
return sorted(
lang for lang, val in index[key].items() if lang != target_lang and val
)
def _context_count(
item: dict[str, Any], index: dict[str, Any], target_lang: str
) -> int:
"""Return the number of other-language translations available for this entry."""
return len(_context_langs(item, index, target_lang))
def _render_item(
i: int,
item: dict[str, Any],
index: dict[str, Any],
target_lang: str,
reference_langs_sorted: list[str],
) -> list[str]:
"""Render one batch entry as prompt lines."""
lines: list[str] = []
ctx = _context_count(item, index, target_lang)
if ctx == 0:
lines.append(
f"--- [{i}] (no reference translations — translate conservatively) ---"
)
else:
plural = "s" if ctx != 1 else ""
lines.append(f"--- [{i}] ({ctx} reference translation{plural}) ---")
lines.append(f"English: {json.dumps(item['msgid'], ensure_ascii=False)}")
if item.get("msgid_plural"):
plural_json = json.dumps(item["msgid_plural"], ensure_ascii=False)
lines.append(f"English plural: {plural_json}")
key = item["index_key"]
if key in index and reference_langs_sorted:
for lang in reference_langs_sorted:
val = index[key].get(lang)
if val is None:
continue
if isinstance(val, dict):
forms = "; ".join(
f"[{k}] {json.dumps(v, ensure_ascii=False)}" for k, v in val.items()
)
lines.append(f"{_lang_name(lang)}: {forms}")
else:
lines.append(
f"{_lang_name(lang)}: {json.dumps(val, ensure_ascii=False)}"
)
lines.append("")
return lines
def build_prompt(
target_lang: str,
batch: list[dict[str, Any]],
index: dict[str, Any],
) -> str:
"""Build the Claude prompt for a batch of entries."""
lang_name = _lang_name(target_lang)
# Collect which other languages actually have translations for this batch
reference_langs: set[str] = set()
for item in batch:
key = item["index_key"]
if key in index:
reference_langs.update(
lang for lang, val in index[key].items() if lang != target_lang and val
)
reference_langs_sorted = sorted(reference_langs)
lines: list[str] = [
"You are a professional translator specializing in software UI strings.",
f"Translate the following English strings into {lang_name} ({target_lang}).",
"",
"Rules:",
"- Preserve all format placeholders exactly: %(name)s, {name}, %s, %d, etc.",
"- Preserve HTML tags if present.",
"- Keep the same tone and register as the reference translations.",
"- For plural forms, provide translations for all plural forms"
" required by the language.",
"- Return ONLY a JSON object mapping each numeric index (as a string)"
" to its translation.",
"- Do not add any explanation, preamble, or markdown fences.",
"",
"Important: Many strings are short fragments or single words that are"
" ambiguous in English (e.g. 'Scale' could mean a measurement scale,"
" to scale an image, or fish scales). Use the translations in other"
" languages as your primary signal for which meaning is intended —"
" they collectively disambiguate the intended sense. When no"
" other-language translations are available for an entry, translate"
" conservatively based on the most common meaning in a data"
" visualization UI context.",
"",
]
if reference_langs_sorted:
lines.append(
f"Reference translations are provided per string where available "
f"({', '.join(_lang_name(lc) for lc in reference_langs_sorted)})."
)
lines.append("")
lines.append("Strings to translate:")
lines.append("")
for i, item in enumerate(batch):
lines.extend(_render_item(i, item, index, target_lang, reference_langs_sorted))
# Add guidance on plural form counts per language whenever ANY entry in
# the batch is plural — batches mix singular and plural in .po order, so
# gating on the first entry would silently drop the guidance whenever
# the plural entries happen to land after a singular one.
if any(item.get("msgid_plural") for item in batch):
lines.append(
"Note: provide ALL plural forms required by the target language "
"(e.g. French needs 2, Russian needs 3, Arabic needs 6)."
)
lines.append("")
lines.append(
'Expected output format: {"0": "<translation>", "1": "<translation>", ...}'
)
lines.append("(keys are the numeric indices of the strings above)")
return "\n".join(lines)
def parse_response(text: str, batch_size: int) -> dict[int, str]:
"""Parse the JSON object from Claude's response."""
# Strip any accidental markdown fences
text = re.sub(r"^```[^\n]*\n", "", text.strip())
text = re.sub(r"\n```$", "", text)
try:
raw = json.loads(text)
except json.JSONDecodeError as exc:
raise ValueError(
f"Could not parse response as JSON: {exc}\n\nResponse:\n{text}"
) from exc
# _process_batches only catches ValueError/RuntimeError, so a non-object
# response (list, scalar, null) must surface as ValueError rather than
# bubbling up an AttributeError from .items() and aborting the whole run.
if not isinstance(raw, dict):
raise ValueError(
f"Expected a JSON object mapping indices to translations, "
f"got {type(raw).__name__}.\n\nResponse:\n{text}"
)
# Preserve dict/list values as JSON strings so plural responses (where
# v is a dict of plural forms) can be re-parsed downstream by
# _apply_translation's json.loads. str(v) on a dict produces Python
# repr ({'0': 'x'}) which is not valid JSON.
return {
int(k): (
json.dumps(v, ensure_ascii=False) if isinstance(v, (dict, list)) else str(v)
)
for k, v in raw.items()
if str(k).isdigit()
}
def translate_batch(
model: str,
target_lang: str,
batch: list[dict[str, Any]],
index: dict[str, Any],
) -> dict[int, str]:
"""Send a batch of strings to Claude via `claude -p`.
Returns a dict mapping batch index to translated string.
"""
claude_bin = shutil.which("claude")
if not claude_bin:
raise RuntimeError(
"claude CLI not found. Install Claude Code or add it to PATH."
)
prompt = build_prompt(target_lang, batch, index)
# Pipe the prompt over stdin rather than passing it as argv: a single batch
# with many reference languages can grow into the tens of KB and approach
# ARG_MAX on some platforms.
# claude_bin is resolved via shutil.which — not user-controlled input
result = subprocess.run( # noqa: S603
[claude_bin, "--model", model, "-p"],
input=prompt,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
f"claude exited with code {result.returncode}:\n{result.stderr}"
)
return parse_response(result.stdout.strip(), len(batch))
def _apply_plural_translation(entry: polib.POEntry, translation: str) -> None:
"""Distribute a model response across the entry's plural forms.
Model may return a JSON dict ({"0": "form0", "1": "form1"}), a JSON list
(["form0", "form1"], also valid since plural forms are ordered), a JSON
scalar (a single translation that fills every form), or a plain non-JSON
string (older models that ignore the JSON instruction).
"""
try:
plural_value = json.loads(translation)
except (json.JSONDecodeError, ValueError):
for k in entry.msgstr_plural:
entry.msgstr_plural[k] = translation
return
if isinstance(plural_value, dict):
entry.msgstr_plural = {int(k): str(v) for k, v in plural_value.items()}
return
if isinstance(plural_value, list) and plural_value:
# Distribute list items across plural form indices in order; if the
# model returned fewer forms than the language requires, repeat the
# last form rather than leaving slots blank.
forms = [str(v) for v in plural_value]
for k in sorted(entry.msgstr_plural):
entry.msgstr_plural[k] = forms[k] if k < len(forms) else forms[-1]
return
# Scalar (or empty list) — broadcast to every form.
fill = str(plural_value) if plural_value not in (None, []) else translation
for k in entry.msgstr_plural:
entry.msgstr_plural[k] = fill
def _apply_translation(
entry: polib.POEntry,
translation: str,
item: dict[str, Any],
model: str,
mark_fuzzy: bool,
) -> None:
"""Write a translation string into a POEntry and add attribution."""
if entry.msgid_plural:
_apply_plural_translation(entry, translation)
else:
entry.msgstr = translation
if mark_fuzzy and "fuzzy" not in entry.flags:
entry.flags.append("fuzzy")
refs = item["context_langs"]
refs_tag = f" [refs: {', '.join(refs)}]" if refs else " [no refs]"
attribution = f"Machine-translated via backfill_po.py ({model}){refs_tag}"
if entry.tcomment:
if attribution not in entry.tcomment:
entry.tcomment = f"{entry.tcomment}\n{attribution}"
else:
entry.tcomment = attribution
def _build_batch_items(
entries: list[polib.POEntry],
index: dict[str, Any],
lang: str,
) -> list[dict[str, Any]]:
"""Convert a list of POEntries into the dict format used by translate_batch."""
items: list[dict[str, Any]] = []
for entry in entries:
if entry.msgid_plural:
item: dict[str, Any] = {
"msgid": entry.msgid,
"msgid_plural": entry.msgid_plural,
"index_key": _plural_key(entry.msgid, entry.msgid_plural),
"is_plural": True,
}
else:
item = {
"msgid": entry.msgid,
"index_key": entry.msgid,
"is_plural": False,
}
item["context_langs"] = _context_langs(item, index, lang)
item["context_count"] = len(item["context_langs"])
items.append(item)
return items
def _process_batches(
missing: list[polib.POEntry],
index: dict[str, Any],
lang: str,
batch_size: int,
model: str,
dry_run: bool,
mark_fuzzy: bool,
cat: polib.POFile | None = None,
po_path: Path | None = None,
) -> tuple[int, int]:
"""Translate missing entries in batches. Returns (translated, failed) counts.
When ``cat`` and ``po_path`` are provided and ``dry_run`` is False, the
catalog is saved to disk after each batch that produced at least one
successful translation. This means a crash mid-run only loses the in-flight
batch rather than every batch translated so far.
"""
translated_count = 0
failed_count = 0
for batch_start in range(0, len(missing), batch_size):
batch_entries = missing[batch_start : batch_start + batch_size]
batch_items = _build_batch_items(batch_entries, index, lang)
end = min(batch_start + batch_size, len(missing))
print(
f" Translating entries {batch_start + 1}{end} of {len(missing)}",
file=sys.stderr,
)
try:
translations = translate_batch(model, lang, batch_items, index)
except (ValueError, RuntimeError) as exc:
print(f" ERROR in batch starting at {batch_start}: {exc}", file=sys.stderr)
failed_count += len(batch_entries)
continue
batch_applied = 0
for i, entry in enumerate(batch_entries):
translation = translations.get(i)
if translation is None:
print(
f" WARNING: no translation returned for index {i} "
f"(msgid: {entry.msgid[:60]!r})",
file=sys.stderr,
)
failed_count += 1
continue
if dry_run:
ctx = batch_items[i]["context_count"]
ctx_tag = f" [ctx:{ctx}]" if ctx < 3 else ""
print(
f" [{lang}]{ctx_tag} {entry.msgid[:60]!r}{translation[:60]!r}"
)
else:
_apply_translation(
entry, translation, batch_items[i], model, mark_fuzzy
)
batch_applied += 1
translated_count += 1
if (
not dry_run
and batch_applied > 0
and cat is not None
and po_path is not None
):
cat.save()
print(
f" Saved {po_path} ({batch_applied} entry(ies) in this batch).",
file=sys.stderr,
)
return translated_count, failed_count
def backfill(
lang: str,
*,
batch_size: int = DEFAULT_BATCH_SIZE,
limit: int | None = None,
min_context: int = 0,
model: str = DEFAULT_MODEL,
index_path: Path = DEFAULT_INDEX,
dry_run: bool = False,
mark_fuzzy: bool = True,
) -> None:
"""Backfill missing translations in the target language's .po file."""
# Defense against path traversal: ``lang`` lands in a filesystem path
# without further sanitization, so reject anything that isn't an
# ISO 639-1/639-2 code with an optional ISO 3166 region (e.g. ``pt_BR``).
if not re.fullmatch(r"[a-z]{2,3}(_[A-Z]{2})?", lang):
print(
f"Invalid language code: {lang!r} "
"(expected ISO 639 code, optionally with _<REGION>, e.g. 'fr' or 'pt_BR')",
file=sys.stderr,
)
sys.exit(1)
po_path = TRANSLATIONS_DIR / lang / "LC_MESSAGES" / "messages.po"
if not po_path.exists():
print(f"No .po file found for language '{lang}': {po_path}", file=sys.stderr)
sys.exit(1)
if not index_path.exists():
print(
f"Translation index not found at {index_path}.\n"
"Run: python scripts/translations/build_translation_index.py",
file=sys.stderr,
)
sys.exit(1)
print("Loading translation index …", file=sys.stderr)
with open(index_path, encoding="utf-8") as f:
index: dict[str, Any] = json.load(f)
_ensure_license_header(po_path, dry_run=dry_run)
print(f"Loading {po_path}", file=sys.stderr)
cat = polib.pofile(str(po_path))
missing: list[polib.POEntry] = [e for e in cat if e.msgid and _is_missing(e)]
print(f"Found {len(missing)} untranslated entries for '{lang}'.", file=sys.stderr)
if min_context > 0:
before = len(missing)
missing = [
e
for e in missing
if _context_count(
{
"index_key": (
_plural_key(e.msgid, e.msgid_plural)
if e.msgid_plural
else e.msgid
)
},
index,
lang,
)
>= min_context
]
skipped = before - len(missing)
print(
f"Skipping {skipped} entries with fewer than {min_context} reference "
f"translation(s) (use --min-context 0 to include them).",
file=sys.stderr,
)
if limit is not None:
missing = missing[:limit]
print(f"Limiting to {limit} entries.", file=sys.stderr)
if not missing:
print("Nothing to do.", file=sys.stderr)
return
translated_count, failed_count = _process_batches(
missing,
index,
lang,
batch_size,
model,
dry_run,
mark_fuzzy,
cat=cat,
po_path=po_path,
)
print(
f"\nDone. Translated: {translated_count}, Failed/skipped: {failed_count}.",
file=sys.stderr,
)
if not dry_run and translated_count > 0:
print(
f"Translations written to {po_path} (marked #, fuzzy for review).",
file=sys.stderr,
)
def main() -> None:
"""Parse CLI arguments and run translation backfill."""
parser = argparse.ArgumentParser(
description="Backfill missing .po translations using Claude AI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--lang", required=True, help="ISO language code (e.g. fr, de, ja)"
)
parser.add_argument(
"--batch-size",
type=int,
default=DEFAULT_BATCH_SIZE,
help=f"Strings per Claude request (default: {DEFAULT_BATCH_SIZE})",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Maximum number of entries to translate (default: unlimited)",
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
help=f"Claude model ID (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--index",
type=Path,
default=DEFAULT_INDEX,
help=f"Path to translation_index.json (default: {DEFAULT_INDEX})",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print translations without modifying the .po file",
)
parser.add_argument(
"--min-context",
type=int,
default=0,
metavar="N",
help=(
"Skip entries with fewer than N reference translations in other languages "
"(default: 0 = translate everything). Strings with low context are more "
"likely to be ambiguous single words or fragments — set to e.g. 2 to only "
"translate strings that have been confirmed in at least 2 other languages."
),
)
parser.add_argument(
"--no-fuzzy",
dest="mark_fuzzy",
action="store_false",
default=True,
help=(
"Do not mark generated translations as #, fuzzy. "
"WARNING: fuzzy entries are excluded from compiled .mo files. "
"Removing this flag causes AI-generated translations to be served "
"to end users without human review — only use after you have "
"manually verified the .po file."
),
)
args = parser.parse_args()
backfill(
lang=args.lang,
batch_size=args.batch_size,
limit=args.limit,
min_context=args.min_context,
model=args.model,
index_path=args.index,
dry_run=args.dry_run,
mark_fuzzy=args.mark_fuzzy,
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,153 @@
# 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.
"""Build a cross-language translation index from all .po files.
Outputs a JSON file structured as:
{
"<msgid>": {
"<lang>": "<translated string or null>",
...
},
...
}
For plural entries the key is "<msgid>\x00<msgid_plural>" and the value
is a dict mapping lang -> {0: "...", 1: "..."} (or null if untranslated).
Usage:
python scripts/translations/build_translation_index.py
python scripts/translations/build_translation_index.py \
--translations-dir superset/translations \
--output /tmp/translation_index.json
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
try:
import polib # type: ignore[import-untyped]
except ImportError:
print("polib is required. Install with: pip install polib", file=sys.stderr)
sys.exit(1)
TRANSLATIONS_DIR = Path(__file__).parent.parent.parent / "superset" / "translations"
DEFAULT_OUTPUT = (
Path(__file__).parent.parent.parent
/ "superset"
/ "translations"
/ "translation_index.json"
)
def _is_translated(entry: polib.POEntry) -> bool:
"""Return True if the entry has a non-empty, non-fuzzy translation."""
if "fuzzy" in entry.flags:
return False
if entry.msgid_plural:
return any(v for v in entry.msgstr_plural.values())
return bool(entry.msgstr)
def _plural_key(entry: polib.POEntry) -> str:
"""Build the combined key used for plural translation entries."""
return f"{entry.msgid}\x00{entry.msgid_plural}"
def build_index(translations_dir: Path) -> dict[str, Any]:
"""Read all .po files and build a combined translation index."""
index: dict[str, dict[str, Any]] = {}
langs = sorted(
d
for d in os.listdir(translations_dir)
if (translations_dir / d / "LC_MESSAGES" / "messages.po").exists()
and d != "en" # en has empty msgstr by convention (source = target)
)
for lang in langs:
po_path = translations_dir / lang / "LC_MESSAGES" / "messages.po"
cat = polib.pofile(str(po_path))
for entry in cat:
if not entry.msgid:
continue # skip header entry
if entry.msgid_plural:
key = _plural_key(entry)
if key not in index:
index[key] = {}
# Fuzzy entries are unreviewed (often machine-generated drafts),
# so excluding them prevents feeding unverified translations
# back into the AI backfill prompt as trusted context.
index[key][lang] = (
dict(entry.msgstr_plural) if _is_translated(entry) else None
)
else:
key = entry.msgid
if key not in index:
index[key] = {}
index[key][lang] = entry.msgstr if _is_translated(entry) else None
# Ensure every entry has a slot for every language (null if missing)
for key in index:
for lang in langs:
index[key].setdefault(lang, None)
return index
def main() -> None:
"""Parse arguments, build the translation index, and write it to disk."""
parser = argparse.ArgumentParser(
description="Build cross-language translation index"
)
parser.add_argument(
"--translations-dir",
type=Path,
default=TRANSLATIONS_DIR,
help="Path to the translations directory (default: superset/translations)",
)
parser.add_argument(
"--output",
"-o",
type=Path,
default=DEFAULT_OUTPUT,
help=(
"Output JSON file path"
" (default: superset/translations/translation_index.json)"
),
)
args = parser.parse_args()
print(f"Reading .po files from {args.translations_dir}", file=sys.stderr)
index = build_index(args.translations_dir)
print(f"Indexed {len(index)} message IDs.", file=sys.stderr)
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(index, f, ensure_ascii=False, indent=2)
print(f"Written to {args.output}", file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# 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.
"""
Check that source-code changes don't cause translation regressions.
Usage
-----
Count non-fuzzy translated 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:
python check_translation_regression.py --compare /path/to/before.json
Optionally write a markdown report to a file (used by CI to post a PR comment):
python check_translation_regression.py --compare before.json --report report.md
Use a translations directory other than the repo default (used by CI to count
against a separate base-branch worktree):
python check_translation_regression.py --count \\
--translations-dir /tmp/base-worktree/superset/translations
Typical CI workflow
-------------------
1. Create a base-branch worktree alongside the PR worktree
2. Run babel_update.sh in the base worktree (extract from BASE source)
3. Record baseline: python ... --count --translations-dir BASE_TREE > before.json
4. Run babel_update.sh in the PR worktree (extract from PR source and keep
any committed PR .po updates)
5. Compare: python ... --compare before.json [--report report.md]
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.
"""
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Optional
DEFAULT_TRANSLATIONS_DIR = (
Path(__file__).resolve().parent.parent.parent / "superset" / "translations"
)
# English .po files use empty msgstr by convention (source language == target),
# so they always show 0 translated entries and should not be checked.
SKIP_LANGS = {"en"}
def count_translated(po_file: Path) -> int:
"""Return the number of non-fuzzy translated messages in a .po file.
Raises:
subprocess.CalledProcessError: if ``msgfmt`` fails (e.g. malformed
.po file). The regression check exists to surface translation
problems, so a silent zero would defeat its purpose — let the
caller see a malformed file as a hard failure.
"""
import shutil # noqa: PLC0415
msgfmt = shutil.which("msgfmt") or "msgfmt"
result = subprocess.run( # noqa: S603
[msgfmt, "--statistics", "-o", "/dev/null", str(po_file)],
capture_output=True,
text=True,
check=True,
)
# stderr: "123 translated messages, 4 fuzzy translations, 56 untranslated messages."
match = re.search(r"(\d+) translated message", result.stderr)
if not match:
raise RuntimeError(
f"Could not parse msgfmt --statistics output for {po_file}: "
f"{result.stderr!r}"
)
return int(match.group(1))
def get_counts(translations_dir: Path) -> dict[str, int]:
counts: 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)
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.
print(
f"WARNING: skipping {lang}{po_file} could not be counted: {exc}",
file=sys.stderr,
)
return counts
def build_regression_report(regressions: list[tuple[str, int, int]]) -> str:
"""Build a markdown report for posting as a PR comment."""
rows = "\n".join(
f"| `{lang}` | {b} | {a} | -{b - a} |" 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"{rows}\n\n"
"### How to fix\n\n"
"**1. Install dependencies** (if not already set up):\n\n"
"```bash\n"
"pip install -r superset/translations/requirements.txt\n"
"sudo apt-get install gettext # or: brew install gettext\n"
"```\n\n"
"**2. Re-extract strings and sync `.po` files:**\n\n"
"```bash\n"
"./scripts/translations/babel_update.sh\n"
"```\n\n"
"This rewrites `superset/translations/messages.pot` from the current "
"source files and merges the changes into every `.po` file. Strings "
"whose `msgid` changed will be marked `#, fuzzy`.\n\n"
f"**3. Resolve the fuzzy entries** in the affected language files "
f"({affected}):\n\n"
"```bash\n"
"grep -n '#, fuzzy' superset/translations/<lang>/LC_MESSAGES/messages.po\n"
"```\n\n"
"For each fuzzy entry, either rewrite the `msgstr` to match the new "
"string and remove the `#, fuzzy` line, or clear the `msgstr` to "
'`""` if you cannot provide a translation.\n\n'
"**4. Commit your changes to the `.po` files.**\n"
)
def cmd_count(translations_dir: Path) -> None:
counts = get_counts(translations_dir)
print(json.dumps(counts, indent=2))
def cmd_compare(
before_path: str,
translations_dir: Path,
report_path: Optional[str] = None,
) -> None:
with open(before_path) as f:
before: dict[str, int] = json.load(f)
after = get_counts(translations_dir)
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))
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(
"\nStrings renamed or deleted by this PR invalidated existing translations."
)
print(
"Update the affected .po files to restore the lost entries before merging."
)
if report_path:
Path(report_path).write_text(
build_regression_report(regressions), encoding="utf-8"
)
sys.exit(1)
# 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})")
def main() -> None:
parser = argparse.ArgumentParser(
description="Check for translation regressions in .po files."
)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument(
"--count",
action="store_true",
help="Output translation counts per language as JSON.",
)
action.add_argument(
"--compare",
metavar="BEFORE_JSON",
help="Compare current counts against a baseline JSON file.",
)
parser.add_argument(
"--report",
metavar="REPORT_MD",
help="When --compare detects regressions, write a markdown report here.",
)
parser.add_argument(
"--translations-dir",
type=Path,
default=DEFAULT_TRANSLATIONS_DIR,
help=(
"Path to the translations directory containing per-language "
"LC_MESSAGES/messages.po files (default: <repo>/superset/translations)."
),
)
args = parser.parse_args()
if args.count:
cmd_count(args.translations_dir)
else:
cmd_compare(args.compare, args.translations_dir, args.report)
if __name__ == "__main__":
main()

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