Compare commits

..

130 Commits

Author SHA1 Message Date
rusackas
15a6e5ba12 test: strengthen refresh_frequency preservation coverage per review
- dao_tests.py: seed cross_filters_enabled=False alongside refresh_frequency
  so the preserves-unsent-fields test actually exercises this PR's change
  (refresh_frequency alone was already preserved by #42354).
- PropertiesModal.test.tsx: add a second JSON-editor test using an explicit
  0 while the Refresh dropdown holds a non-zero value, so a regression from
  `??` to `||` on index.tsx:353 would be caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 12:55:00 -07:00
rusackas
6962044df5 fix: address bot review findings on refresh_frequency JSON sync
- Validate the JSON editor's refresh_frequency (not just the dropdown
  state) so a value edited directly in Advanced JSON still enforces
  SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT.
- Don't overwrite the Advanced JSON editor with a one-field object when
  the dropdown changes while the editor holds invalid/in-progress JSON.
- Keep shared_label_colors, map_label_colors, and color_scheme_domain
  resetting to their default when absent from the payload, since the
  Properties modal never sends them and preserving stale values could
  leave outdated color mappings after a color scheme change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 07:54:44 -07:00
Evan
16e61fb856 test: mock SupersetClient.put so the save path reaches onSubmit
The refresh_frequency preservation test clicks Save with onlyApply: false,
which PUTs to the dashboard API before calling onSubmit. Without a mocked
put the request rejects and onSubmit is never invoked, failing the test in
CI. Mirrors the mock used by the existing onlyApply:false submit test.
Adds an aria-label to the mocked textarea to satisfy jsx-a11y.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 02:57:44 -07:00
Evan Rusackas
47fa9705e4 fix(dashboard): keep refresh_frequency set via the Advanced JSON editor
The Dashboard Properties modal seeds a separate refreshFrequency state from the
Refresh dropdown and, on save, unconditionally overwrote
json_metadata.refresh_frequency with it, so a value typed directly into the
Advanced JSON editor was clobbered by the (stale) dropdown value and reset to 0.
This mirrors how color_scheme is already handled: the JSON editor value now
takes precedence on save, and a dropdown change syncs back into the JSON so the
two sources can't diverge.

Also hardens DashboardDAO.set_dash_metadata so metadata fields absent from the
incoming payload are preserved instead of reset to their defaults (the pattern
dosu flagged on the issue), preventing partial updates from wiping unrelated
fields.

Fixes #42116

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 02:57:41 -07:00
Mehmet Salih Yavuz
ecc7f726a4 fix: guard potential null derefs and remove dead branches (#42358)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-07-28 11:43:46 +03:00
PRATHAMESH HUKKERI
26b6f7bb5d fix(dashboard): preserve refresh_frequency when absent from save data (#42354)
Co-authored-by: Prathamesh Hukkeri <prathamesh04@users.noreply.github.com>
2026-07-27 23:13:11 -07:00
Phuc Hung Nguyen
a10c3f0b1d test(result_set): add regression test for empty result set column metadata (#35962)
Co-authored-by: Phuc Hung Nguyen <phucnguyen@geotab.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 23:08:59 -07:00
PRATHAMESH HUKKERI
3095d7b07f fix(metadata-bar): add max-width to text for proper ellipsis truncation (#42237)
Co-authored-by: Prathamesh Hukkeri <prathamesh04@users.noreply.github.com>
2026-07-27 20:38:29 -07:00
Gaurav Dubey
a8e2a340f1 fix(select): exclude null-valued options from "Select all" count (#42220)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-27 20:24:44 -07:00
Pawan
c0117f78a9 feat(dashboard): respect GranularExportControls for download permission (#41799) 2026-07-27 20:00:03 -07:00
Mike Bridge
a73e2485de feat(versioning): version-history retention cleanup job (#41075)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 19:59:11 -07:00
Jean-Philippe Monette
79f3fed1f9 fix(country-map): carve the Great Lakes out of USA map states (#29503)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-27 19:58:32 -07:00
Amogh Atreya
c792752a58 fix(db_engine_specs): preserve DATE semantics when applying Postgres time grains (#42254) (#42329)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-27 17:31:28 -07:00
Elizabeth Thompson
eb914b8ae3 fix(datasets): log datetime format-detection DB query failures at WARNING (#42388)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 15:09:28 -07:00
Elizabeth Thompson
276b7e2d67 fix(importers): catch YAMLError instead of narrower ParserError in dataset v0 importer (#42442) 2026-07-27 15:04:52 -07:00
Elizabeth Thompson
3e7ae85cc2 fix(importers): catch YAMLError instead of narrower ParserError in load_yaml (#42426) 2026-07-27 15:03:37 -07:00
jobbywl
12d7179c21 fix(hive): select schema via USE prequery instead of rewriting the URI database (#36603)
Co-authored-by: Job <job@samax.nu>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:16 -07:00
Joe Li
1d752a0ced fix(embedded): load guest charts with missing or stale query_context (#42150)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:14:55 -07:00
Joe Li
a44afd8105 chore: drop legacy /superset prefix from remaining hardcoded URLs (#42156)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 14:13:35 -07:00
Nitish Agarwal
74924ae73a fix(db_engine_specs): use CAST(DATE({col}) AS DATETIME) in MySQL HOUR time grain (#38617) 2026-07-27 12:52:25 -07:00
dependabot[bot]
748e1e80f0 chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0 (#42452)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:45:26 -07:00
dependabot[bot]
4300e9fb93 chore(deps): bump hot-shots from 17.0.0 to 17.0.1 in /superset-websocket (#42445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:24:56 -07:00
dependabot[bot]
3540f3f957 chore(deps): bump the rjsf group in /superset-frontend with 3 updates (#42446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:24:53 -07:00
dependabot[bot]
faf2cc0f04 chore(deps): bump @fontsource/ibm-plex-mono from 5.2.7 to 5.3.0 in /docs (#42447)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:24:49 -07:00
dependabot[bot]
45c2b801f7 chore(deps): bump @fontsource/ibm-plex-mono from 5.2.7 to 5.3.0 in /superset-frontend (#42458)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:24:45 -07:00
Đỗ Trọng Hải
03c97764d1 chore(ci): remove nyc usage for merging coverage results as Codecov natively supports the action (#42431)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-27 10:28:26 -07:00
Evan Rusackas
95e5e25e1a feat(helm): mitigate init-db Job hang under Istio sidecar injection (closes #25798) (#41396)
Co-authored-by: Devin AI <devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:09:30 -07:00
dependabot[bot]
dece457792 chore(deps): bump @swc/core from 1.15.43 to 1.15.46 in /docs (#42448)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:52:52 -07:00
dependabot[bot]
3666b9a934 chore(deps): bump swagger-ui-react from 5.32.8 to 5.32.9 in /docs (#42449)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:55 -07:00
dependabot[bot]
c09ebc2087 chore(deps): bump @fontsource/inter from 5.2.8 to 5.3.0 in /docs (#42450)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:50 -07:00
dependabot[bot]
23e5cc24d5 chore(deps): bump @fontsource/fira-code from 5.2.7 to 5.3.0 in /docs (#42451)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:45 -07:00
dependabot[bot]
fe78726935 chore(deps-dev): bump stylelint from 17.14.0 to 17.14.1 in /superset-frontend (#42453)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:38 -07:00
dependabot[bot]
b3c95d138a chore(deps): bump chrono-node from 2.10.0 to 2.10.1 in /superset-frontend (#42454)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:32 -07:00
dependabot[bot]
5898463ced chore(deps-dev): bump @swc/core from 1.15.43 to 1.15.46 in /superset-frontend (#42455)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:26 -07:00
dependabot[bot]
0d2a82c2b2 chore(deps): bump react-arborist from 3.13.2 to 3.15.0 in /superset-frontend (#42456)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:21 -07:00
dependabot[bot]
67face3e3d chore(deps-dev): bump lightningcss from 1.32.0 to 1.33.0 in /superset-frontend (#42457)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:16 -07:00
dependabot[bot]
fdea53df9e chore(deps-dev): bump source-map from 0.7.6 to 0.8.0 in /superset-frontend (#42459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:08 -07:00
dependabot[bot]
5dea601a09 chore(deps): bump @fontsource/inter from 5.2.8 to 5.3.0 in /superset-frontend (#42460)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:51:02 -07:00
dependabot[bot]
943bbb92e4 chore(deps): bump @fontsource/fira-code from 5.2.7 to 5.3.0 in /superset-frontend (#42461)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 07:50:57 -07:00
Enzo Martellucci
e338c77dd7 fix(dashboard-tabs): disable drag on input fields during tab reorder (#36889) 2026-07-27 16:06:41 +02:00
Sepuri Sai Krishna
6856d0fc67 fix(select): rank case-insensitive matches consistently in dropdown search (#42408) 2026-07-26 19:29:30 -07:00
Hans Yu
8f8331f656 chore: session enforce sqlalchemy 2.0 (#42365)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:28:42 -07:00
Evan Rusackas
e54eccd5fb chore(a11y): enable jsx-a11y/prefer-tag-over-role as error (#42078)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:38:55 -07:00
Evan Rusackas
710037d3d2 chore(importers): log field names instead of full config on validation failure (#42399)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:36:34 -07:00
Elizabeth Thompson
90040fc1f6 fix(mcp_service): downgrade client-disconnect transport noise to WARNING (SC-115264) (#42441)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-26 15:02:15 -07:00
Evan Rusackas
ba0900d2b3 fix(semantic-layers): show a friendly warning when schema enrichment falls back (#42400)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 02:57:17 -07:00
Elizabeth Thompson
9a025267bd fix(reports): positive readiness check for non-tiled screenshots (#42253)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mafi <matt.fitzgerald@gmail.com>
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
2026-07-26 01:14:06 -04:00
dependabot[bot]
e3d3262e97 chore(deps-dev): bump grpcio from 1.81.1 to 1.83.0 (#42414)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-07-25 18:47:24 -07:00
Evan Rusackas
6c2ccc44bd chore(table): drop dead th.ant-column-cell selector (#42407)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:31:07 -07:00
dependabot[bot]
e1ffa53d9b chore(deps): bump pyparsing from 3.2.3 to 3.3.2 (#42420)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 21:53:51 +07:00
dependabot[bot]
4443357f33 chore(deps): bump flask-caching from 2.3.1 to 2.4.1 (#42413)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 21:49:15 +07:00
dependabot[bot]
dd08c4f36c chore(deps-dev): update flightsql-dbapi requirement from <0.3,>=0.2.0 to >=0.2.2,<0.3 (#42416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 21:36:04 +07:00
dependabot[bot]
4f3a1a5058 chore(deps-dev): update snowflake-sqlalchemy requirement from <2,>=1.10.2 to >=1.11.0,<2 (#42418)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 21:35:44 +07:00
dependabot[bot]
9d6fbdceff chore(deps-dev): update ibm-db-sa requirement from <=0.4.4,>0.3.8 to <=0.4.4,>=0.4.4 (#42419)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 20:15:32 +07:00
dependabot[bot]
8864b7a0a3 chore(deps-dev): update oracledb requirement from <5,>=2.0.0 to >=4.0.2,<5 (#42421)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 20:04:34 +07:00
dependabot[bot]
d4652460ee chore(deps-dev): bump google-cloud-bigquery from 3.42.1 to 3.42.2 (#42422)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-25 20:04:07 +07:00
Gordon Cowie
8c37d627aa fix: allow clearing database/schema in DatasourceModal (#39918) 2026-07-24 16:17:25 -07:00
dependabot[bot]
5775bdbe5e chore(deps-dev): bump postcss from 8.5.15 to 8.5.23 in /superset-embedded-sdk (#42394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:14:38 -07:00
Evan Rusackas
c65bedf2d7 fix(extensions): make LOCAL_EXTENSIONS hot reload reliable in Docker (#40084)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-24 15:30:04 -07:00
dependabot[bot]
2a07f7967d chore(deps): bump brace-expansion, @cypress/code-coverage and eslint in /superset-frontend/cypress-base (#42275)
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: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-24 14:24:22 -07:00
Joe Li
b82ff27f9c fix(sqllab): render each query error once on async fallback (#42318)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-24 14:13:35 -07:00
dependabot[bot]
83ea9a1025 chore(deps): bump js-yaml from 5.2.1 to 5.2.2 in /docs (#42395)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:33:36 -07:00
dependabot[bot]
e9f61c1e66 chore(deps): bump postcss from 8.5.15 to 8.5.23 in /superset-frontend (#42396)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:33:22 -07:00
dependabot[bot]
857a194c66 chore(deps): bump postcss from 8.5.16 to 8.5.23 in /docs (#42397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:33:09 -07:00
Joe Li
5ffdac1871 fix(mcp): align pooled screenshot override signature (#42384) 2026-07-24 13:32:31 -07:00
Joe Li
52552c8278 test(dashboard): migrate dashboard force-refresh control to Playwright (#41433)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:32:09 -07:00
Joe Li
134cae07ae fix(ui): update stale Ant Design v6 internal class selectors (#42146)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:31:45 -07:00
Evan Rusackas
f3fa1c7d4f fix(reports): write a single execution log row per report run (#29857) (#41966)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-24 13:30:31 -07:00
Evan Rusackas
99841fb504 fix(async): include datasets and rev claims in guest channel HMAC (#41487)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:29:58 -07:00
Evan Rusackas
3a53c4ebc3 chore(deps): raise SQLAlchemy lower bound to >=1.4.43 (#41123)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:29:30 -07:00
Joe Li
f7d0ec3a2c chore: add @rusackas to migration code owners (#42398) 2026-07-24 13:18:33 -07:00
Bhargava Vadlamani
7eceebce1c feat(dbspec): Support apache datafusion dbspec (#42374) 2026-07-24 11:56:30 -07:00
Amin Ghadersohi
3bdf134aaa fix(logging): stop noisy LocalProxy-not-mapped warning for guest users (#42306)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 13:06:20 -04:00
ViktorGo86
65528a5b76 docs(map-tiles): add Yandex Maps Tiles API configuration (#42375) 2026-07-24 22:19:20 +07:00
Alejandro Solares
0792f98910 fix(security): bump pillow from 12.2.0 to 12.3.0 (#42362) 2026-07-24 21:49:27 +07:00
Alejandro Solares
67c3fea0ce fix(security): bump pyasn1 from 0.6.3 to 0.6.4 (#42363) 2026-07-24 21:46:21 +07:00
dependabot[bot]
d2ea5e9a1a chore(deps-dev): bump brace-expansion from 1.1.15 to 1.1.16 in /superset-embedded-sdk (#42369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:23:48 +07:00
dependabot[bot]
0b9a9c5d03 chore(deps): bump body-parser from 1.20.5 to 1.20.6 in /docs (#42370)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:23:31 +07:00
dependabot[bot]
16de95f024 chore(deps): bump immer from 11.1.11 to 11.1.15 in /superset-frontend (#42378)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:23:14 +07:00
dependabot[bot]
5b5e5cf82d chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 (#42376)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:23:00 +07:00
dependabot[bot]
542416ac6e chore(deps): bump @deck.gl/mapbox from 9.3.6 to 9.3.7 in /superset-frontend in the deckgl group (#42377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:21:20 +07:00
SkinnyPigeon
4f017de532 docs(gunicorn): correct dead links in values.yaml (#42385) 2026-07-24 21:20:46 +07:00
suvankardas216
a1142b0f91 fix(dataset): disable duplicate button when name is empty (#42217)
Co-authored-by: AS-MAC-1123 <as-mac-1123@AS-MAC-1123.local>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:24:37 -07:00
Jean Massucatto
4ee500e47b fix(explore): render Jinja before validating legacy chart filters (#41996) 2026-07-23 22:05:14 -07:00
Ujjwal Jain
3ff5dbfe81 fix(native-filters): use FILTER_STATE_CACHE_CONFIG timeout for dynamic filter option queries (#38910) 2026-07-23 21:01:54 -07:00
dependabot[bot]
cfb04a6e79 chore(deps-dev): bump databricks-sql-connector from 4.2.6 to 4.3.0 (#42347)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-23 20:57:58 -07:00
dependabot[bot]
49f4e84b48 chore(deps): bump flask-compress from 1.17 to 1.24 (#42346)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 19:21:29 -07:00
dependabot[bot]
921f75d544 chore(deps): bump pillow from 12.2.0 to 12.3.0 (#42348)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 19:21:17 -07:00
Ramachandran A G
bcf0361a91 feat(KustoKQL): Add support for NULL / IS NOT NULL operator (#37890)
Co-authored-by: ag-ramachandran <ramacg@microsoft.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 18:28:37 -07:00
jenwitteng
5776aff50a fix(charts): handle async (202) chart-data responses in StatefulChart (#42157)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-23 18:01:05 -07:00
Hans Yu
14c96761e5 chore: SQLAlchemy User cascade backref warnings are irrelevant (#42360) 2026-07-23 17:16:38 -07:00
Abdul Rehman
d41f0febae fix(api): add example to get_export_ids_schema so Swagger "Try it out" pre-fills a valid array (#42265) 2026-07-23 17:15:24 -07:00
jesperct
c43effa4a3 fix(explore): show the beginning date on time-series x-axis line charts (#42046) 2026-07-23 17:14:30 -07:00
Evan Rusackas
206fe7ab12 fix(embedded): stop rejecting guest chart data built from control-specific params keys (#42295)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-23 13:31:04 -07:00
dependabot[bot]
1049826bc1 chore(deps): bump sqlalchemy-continuum from 1.6.0 to 1.7.0 (#42351)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 12:56:11 -07:00
dependabot[bot]
6317113ee8 chore(deps): bump pydantic from 2.11.7 to 2.13.4 (#42350)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 12:56:03 -07:00
dependabot[bot]
fdb9c152f0 chore(deps): bump nh3 from 0.3.5 to 0.3.6 (#42349)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-23 12:55:56 -07:00
Amin Ghadersohi
2317d9cf91 fix(mcp): truncate query-tool responses instead of hard-failing (#42244) 2026-07-23 14:38:49 -04:00
yousoph
df8bbb548a fix(native-filters): keep filter value input caret at inline start (#42323)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:26:56 -07:00
dependabot[bot]
6b02a423ef chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#42355)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 00:39:08 +07:00
dependabot[bot]
f2848d951c chore(deps-dev): update taos-ws-py requirement from >=0.6.9 to >=0.7.0 (#42344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 10:27:56 -07:00
JUST.in DO IT
cb319c9b81 fix(mcp): trust dataset is_dttm flag when applying time_grain to VARCHAR temporal columns (#42288) 2026-07-23 10:00:22 -07:00
dependabot[bot]
cc8edd2231 chore(deps): bump ag-grid from 36.0.0 to 36.0.1 in /superset-frontend (#42338)
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>
2026-07-23 22:21:37 +07:00
Yash Shrivastava
c5f6d33d21 docs: add pattern to the list of organisations using superset (#42341) 2026-07-23 22:11:17 +07:00
Evan Rusackas
2e72080a16 chore(deps): bump echarts to 6.1.0 with locale and containLabel guards (#42315) (#42321)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-23 21:48:11 +07:00
dependabot[bot]
6654318491 chore(deps): bump caniuse-lite from 1.0.30001805 to 1.0.30001806 in /docs (#42333)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 21:46:27 +07:00
dependabot[bot]
ab553c37ab chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.17 to 0.10.18 in /superset-frontend (#42337)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 21:37:30 +07:00
dependabot[bot]
873da606dd chore(deps): bump github/codeql-action/init from 4.37.0 to 4.37.1 (#42334)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 21:36:39 +07:00
dependabot[bot]
1dd3103c24 chore(deps): bump github/codeql-action/analyze from 4.37.0 to 4.37.1 (#42331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 21:36:16 +07:00
dependabot[bot]
2926d6f6a9 chore(deps): bump actions/labeler from 6.2.0 to 7.0.0 (#42332)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-23 21:34:54 +07:00
Evan Rusackas
67924ad35e fix(dashboard): offer Exit edit mode when there is nothing to discard (#42208)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 18:43:49 -07:00
David
ef459c21e8 fix(helm): add MCP HTTPRoute configuration (#42219) 2026-07-22 17:08:16 -07:00
Amitesh Gupta
3ed8889a01 fix: Added PostgreSQL 17.X to the supported database versions table in (#42280) 2026-07-22 16:24:13 -07:00
yousoph
17bd286ae9 fix(forecast): resolve time grain robustly for Prophet forecasting (#42145)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:08:57 -07:00
yousoph
32937f24d2 fix(heatmap): correct tooltip axis value lookup and percentage calculations and add tests (#41864)
Signed-off-by: yousoph <sophieyou12@gmail.com>
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-22 15:34:10 -07:00
Elizabeth Thompson
e075133a83 fix(pandas_postprocessing): avoid FutureWarning for max/min in boxplot MINMAX (#42272) 2026-07-22 15:02:14 -07:00
Joe Li
8d034ef65c fix(plugin-chart-echarts): import the -obj locale build so time axes render (#42317)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:53:59 -07:00
Joe Li
641713f623 fix: Revert "chore(deps): bump echarts from 5.6.0 to 6.1.0 in /superset-frontend" (#42314)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:40:56 -07:00
Joe Li
40d9e7d048 fix(ag-grid-table): avoid ambiguous build query import (#42313) 2026-07-22 11:30:45 -07:00
Hans Yu
19159d58c8 refactor: engine enforce SQLAlchemy 2.0 (#42277) 2026-07-22 11:11:48 -07:00
Grégoire
894c0191d6 fix(superset_app_root): when used with oauth (#38033)
Signed-off-by: Grégoire Bellon-Gervais <gregoire.bellon-gervais@docaposte.fr>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:34:43 -07:00
Joe Li
49b4243129 test(dashboard): migrate dashboard load smoke test to Playwright (#41432)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 10:32:07 -07:00
dependabot[bot]
ac41f61d4a chore(deps): bump nanoid from 5.0.9 to 6.0.0 in /superset-frontend (#42230)
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: Joe Li <joe@preset.io>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 10:28:51 -07:00
dependabot[bot]
f50b7b7154 chore(deps): bump actions/setup-go from 6.5.0 to 7.0.0 (#42303)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 10:28:11 -07:00
Zack
be5744c431 fix(chart): updates counties of kenya map (#38019)
Co-authored-by: Zack Adams <zack@Zacks-Laptop.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-07-22 10:17:29 -07:00
SkinnyPigeon
8c5d465a65 feat(datasets): add RLS filter indicator badge to dataset list and explore view (#38807)
Co-authored-by: Evan <evan@preset.io>
2026-07-22 10:07:52 -07:00
Evan Rusackas
c5935b6904 feat(table/pivot-table): correct non-additive totals/subtotals via DB rollup [SIP-216] (#41184)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-22 10:06:05 -07:00
Evan Rusackas
10ff470702 fix(ag-grid-table): respect row limit with server pagination (#41346)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:03:33 -07:00
Amin Ghadersohi
1c0f259e38 refactor(mcp): dedupe list-tool schemas and delete dead middleware (#41923) 2026-07-22 12:29:59 -04:00
Alexandru Soare
b8f3b768d4 fix(sqllab): Fall back to empty editor tab when linked query no longer exists (#42241) 2026-07-22 15:30:42 +03:00
629 changed files with 22214 additions and 10016 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,6 @@
# Notify all committers of DB migration changes, per SIP-59
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe @rusackas
# Notify some committers of changes in the components

View File

@@ -70,6 +70,10 @@ updates:
- "@storybook/*"
- "storybook"
- "eslint-plugin-storybook"
ag-grid:
patterns:
- "ag-grid-react"
- "ag-grid-community"
open-pull-requests-limit: 30
versioning-strategy: increase
cooldown:

View File

@@ -31,7 +31,7 @@ jobs:
checks: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
ref: master
@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-supersetbot/
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check and notify

View File

@@ -26,7 +26,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -58,13 +58,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -75,6 +75,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
category: "/language:${{matrix.language}}"

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Dependency Review"
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

View File

@@ -30,7 +30,7 @@ jobs:
docker: ${{ steps.check.outputs.docker }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -71,7 +71,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -184,7 +184,7 @@ jobs:
timeout-minutes: 30
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Free up disk space

View File

@@ -23,7 +23,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Note: registry-url is intentionally omitted. When set, actions/setup-node

View File

@@ -21,7 +21,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0

View File

@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -27,7 +27,7 @@ jobs:
security-events: write
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

View File

@@ -16,7 +16,7 @@ jobs:
issues: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-26.04
steps:
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with:
sync-labels: true

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -27,7 +27,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -33,7 +33,7 @@ jobs:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -47,7 +47,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
- name: Install helm-docs
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2

View File

@@ -33,7 +33,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# pulls all commits (needed for lerna / semantic release to correctly version)

View File

@@ -102,7 +102,7 @@ jobs:
fail-fast: false
steps:
- name: "Checkout release tag: ${{ needs.config.outputs.latest-release }}"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.config.outputs.latest-release }}
fetch-depth: 0

View File

@@ -153,7 +153,7 @@ jobs:
- name: Checkout PR code (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false

View File

@@ -41,7 +41,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
persist-credentials: false

View File

@@ -28,7 +28,7 @@ jobs:
name: Link Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Do not bump this linkinator-action version without opening
@@ -73,7 +73,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -112,7 +112,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

View File

@@ -38,7 +38,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -96,21 +96,21 @@ jobs:
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -206,21 +206,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -31,7 +31,7 @@ jobs:
working-directory: superset-extensions-cli
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -27,7 +27,7 @@ jobs:
should-run: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
@@ -110,7 +110,7 @@ jobs:
id-token: write
steps:
- name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
@@ -122,25 +122,13 @@ jobs:
pattern: coverage-artifacts-*
path: coverage/
- name: Reorganize test result reports
run: |
find coverage/
for i in {1..8}; do
mv coverage/coverage-artifacts-${i}/coverage-final.json coverage/coverage-shard-${i}.json
done
shell: bash
- name: Merge Code Coverage
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: javascript
use_oidc: true
verbose: true
disable_search: true
files: merged-output/coverage-summary.json
directory: coverage
slug: apache/superset
lint-frontend:

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true

View File

@@ -34,7 +34,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -83,21 +83,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -29,7 +29,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -72,7 +72,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -157,7 +157,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -207,7 +207,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -72,7 +72,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -127,7 +127,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -30,7 +30,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for file changes
@@ -61,7 +61,7 @@ jobs:
SQLALCHEMY_WARN_20: "1"
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
@@ -61,7 +61,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
timeout-minutes: 20
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js

View File

@@ -38,7 +38,7 @@ jobs:
});
- name: "Checkout ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

View File

@@ -27,7 +27,7 @@ jobs:
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
- name: Checkout source code
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true

View File

@@ -60,7 +60,7 @@ jobs:
fail-fast: false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
@@ -126,7 +126,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -32,7 +32,7 @@ jobs:
name: Generate Reports
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

1
.gitignore vendored
View File

@@ -57,6 +57,7 @@ local_config.py
/superset_text.yml
superset.egg-info/
superset/bin/supersetc
superset/extensions/.reload_trigger
tmp
rat-results.txt
superset/app/

View File

@@ -144,7 +144,7 @@ repos:
git fetch --no-recurse-submodules origin "$TARGET_BRANCH" 2>/dev/null || true
fi
BASE=$(git merge-base origin/"$TARGET_BRANCH" HEAD 2>/dev/null) || BASE="HEAD"
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' || true)
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' | grep -v '^superset/migrations/' || true)
if [ -n "$files" ]; then
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint --reports=no $files
else

View File

@@ -168,6 +168,11 @@ categories:
url: https://www.now.vn/
contributors: ["@davidkohcw"]
- name: Pattern
url: https://www.pattern.com
contributors: ["@alephys26", "@hladush", "@sanketjadhavSF"]
logo: pattern_logo.png
- name: Qunar
url: https://www.qunar.com/
contributors: ["@flametest"]

View File

@@ -288,6 +288,22 @@ The pivot table chart's `First` and `Last` aggregations now return the first and
The `error` and `response` parameters of the `retryDelay` and `retryOn` callbacks in `FetchRetryOptions` (exported from `@superset-ui/core`) are now typed `Error | null` and `Response | null` to match the actual call-site signature provided by `fetch-retry`. Because these parameter types are contravariant, consumers who typed their callbacks with the non-nullable `(attempt: number, error: Error, response: Response) => number` will get a TypeScript compile error. Widen your callback signatures to accept `Error | null` / `Response | null`.
### Pivot Table totals are now computed by the database (per-metric "Aggregation function" control removed)
Pivot Table subtotals and grand totals are now computed by the database at each
rollup level instead of re-aggregating the already-aggregated cell values on the
client. This fixes long-standing incorrect totals for non-additive metrics
(ratios such as `SUM(a)/SUM(b)`, `COUNT_DISTINCT`, `AVG`, percentiles, etc.),
which previously summed the displayed cell values.
As a result the per-table **"Aggregation function"** control (which let you pick
how totals were aggregated client-side, e.g. Sum/Average/Count) has been
removed: totals now always reflect the metric's own definition evaluated at the
total's granularity. For additive metrics (`SUM`/`COUNT`/`MIN`/`MAX`) the result
is unchanged. Saved charts that set `aggregateFunction` will ignore it; no
migration is required. If you previously relied on a plain sum-of-cells total
for a non-additive metric, that specific behavior is no longer available.
### `thumbnail_url` removed from dashboard list API response
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
@@ -342,6 +358,16 @@ A read-only companion to the version-history endpoints: each entity type gains a
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
### Version-history retention (pruning)
Entity version history (the `version_transaction` / `*_version` shadow tables that back version capture) is aged out by a nightly Celery beat task, `version_history.prune_old_versions` (`superset.tasks.version_history_retention`).
| Key | Default | Purpose |
|---|---|---|
| `SUPERSET_VERSION_HISTORY_RETENTION_DAYS` | `30` | Version rows whose owning `version_transaction.issued_at` is older than this many days are pruned. Each entity's live row (`end_transaction_id IS NULL`) is always preserved, as are the live rows of its children and associations; closed historical rows (including the baseline) age out. Set to `0` or a negative value to disable pruning. |
The task ships in the default `CeleryConfig.beat_schedule`; a deployment that overrides `CELERY_CONFIG` without inheriting the default will log a startup warning that the prune task is absent (so it never silently stops running). Retention only prunes whatever history exists — capture itself is gated separately by `ENABLE_VERSIONING_CAPTURE` (ships off).
### Webhook alerts/reports block private/internal hosts by default
Webhook alert/report dispatch (`WebhookNotification.send`) now validates the target URL's host against the same private/internal-IP block applied to dataset import URLs. If the resolved host is in a loopback, link-local, private (RFC-1918), shared-CGNAT, or multicast range, the webhook is rejected with `NotificationParamException`.
@@ -636,6 +662,10 @@ Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to se
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
### Kenya Country Map: Updated Administrative Divisions
The Kenya country map has been updated to reflect the 47 counties established under Kenya's 2010 constitution, replacing the outdated 8-province boundaries from the Natural Earth dataset. County keys now use ISO 3166-2:KE codes (`KE-01` through `KE-47`), replacing the former province codes (`KE-110`, `KE-200`, ..., `KE-800`). Dashboards that join on the old province codes will need to re-key their datasets to use the new county codes.
### MCP Tool Observability
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:

View File

@@ -34,6 +34,7 @@ x-superset-volumes: &superset-volumes
- superset_home:/app/superset_home
- ./tests:/app/tests
- superset_data:/app/data
- ./local_extensions:/app/local_extensions
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`

View File

@@ -98,7 +98,9 @@ case "${1}" in
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 \
--extra-files "/app/superset/extensions/.reload_trigger" \
--exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*:*/superset/__init__.py"
;;
app-gunicorn)
echo "Starting web app..."

View File

@@ -149,10 +149,10 @@ database engine on a separate host or container.
Superset supports the following database engines/versions:
| Database Engine | Supported Versions |
| ----------------------------------------- | ---------------------------------------- |
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X |
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
| Database Engine | Supported Versions |
| ----------------------------------------- | ---------------------------------------------- |
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X, 17.X |
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
Use the following database drivers and connection strings:

View File

@@ -25,6 +25,33 @@ DECKGL_BASE_MAP = [
]
```
[Yandex Tiles API](https://yandex.ru/maps-api/docs/tiles-api/request.html) can be used as a custom tile provider. The URL must include the `apikey` from the [Yandex Developer Dashboard](https://developer.tech.yandex.ru/services), tile coordinates (`x`, `y`, and `z`), and the required map layer parameters.
```python
DECKGL_BASE_MAP = [
[
"tile://https://tiles.api-maps.yandex.ru/v1/tiles/"
"?apikey=YOUR_YANDEX_API_KEY"
"&lang=ru_RU"
"&x={x}&y={y}&z={z}"
"&l=map"
"&projection=web_mercator"
"&maptype=map",
"Yandex Maps - Web Mercator",
],
[
"tile://https://tiles.api-maps.yandex.ru/v1/tiles/"
"?apikey=YOUR_YANDEX_API_KEY"
"&lang=ru_RU"
"&x={x}&y={y}&z={z}"
"&l=map"
"&projection=wgs84_mercator"
"&maptype=map",
"Yandex Maps - WGS84 Mercator",
],
]
```
Default values are:
```python
DECKGL_BASE_MAP = [
@@ -56,6 +83,7 @@ CORS_OPTIONS: dict[Any, Any] = {
"https://tile.openstreetmap.org",
"https://tile.osm.ch",
"https://your_personal_url/{z}/{x}/{y}.png",
"https://tiles.api-maps.yandex.ru",
]
}
@@ -72,6 +100,7 @@ TALISMAN_CONFIG = {
"https://tile.openstreetmap.org",
"https://tile.osm.ch",
"https://your_personal_url/{z}/{x}/{y}.png",
"https://tiles.api-maps.yandex.ru",
],
...
}

View File

@@ -53,20 +53,20 @@
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.0",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.43",
"@swc/core": "^1.15.46",
"antd": "^6.5.1",
"baseline-browser-mapping": "^2.10.43",
"caniuse-lite": "^1.0.30001805",
"caniuse-lite": "^1.0.30001806",
"docusaurus-plugin-openapi-docs": "^5.1.2",
"docusaurus-theme-openapi-docs": "^5.1.2",
"js-yaml": "^5.2.0",
"js-yaml": "^5.2.2",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
@@ -78,7 +78,7 @@
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.0",
"swagger-ui-react": "^5.32.8",
"swagger-ui-react": "^5.32.9",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
"unist-util-visit": "^5.1.0"

View File

@@ -1,16 +1,16 @@
{
"generated": "2026-04-17T17:08:35.132Z",
"statistics": {
"totalDatabases": 80,
"withDocumentation": 80,
"withConnectionString": 80,
"withDrivers": 28,
"totalDatabases": 81,
"withDocumentation": 81,
"withConnectionString": 81,
"withDrivers": 29,
"withAuthMethods": 5,
"supportsJoins": 74,
"supportsSubqueries": 77,
"supportsJoins": 75,
"supportsSubqueries": 78,
"supportsDynamicSchema": 29,
"supportsCatalog": 20,
"averageScore": 54,
"averageScore": 53,
"maxScore": 201,
"byCategory": {
"Cloud - AWS": [
@@ -20,6 +20,7 @@
],
"Query Engines": [
"Amazon Athena",
"Apache DataFusion",
"Apache Drill",
"Apache Hive",
"Apache Impala",
@@ -94,6 +95,7 @@
"Vertica"
],
"Apache Projects": [
"Apache DataFusion",
"Apache Doris",
"Apache Drill",
"Apache Druid",
@@ -108,6 +110,7 @@
"Databricks Interactive Cluster"
],
"Open Source": [
"Apache DataFusion",
"Apache Doris",
"Apache Drill",
"Apache Druid",
@@ -9698,6 +9701,97 @@
"supports_dynamic_schema": true,
"supports_catalog": true,
"supports_dynamic_catalog": true
},
"Apache DataFusion": {
"time_grains": {
"SECOND": true,
"FIVE_SECONDS": true,
"THIRTY_SECONDS": true,
"MINUTE": true,
"FIVE_MINUTES": true,
"TEN_MINUTES": true,
"FIFTEEN_MINUTES": true,
"THIRTY_MINUTES": true,
"HALF_HOUR": false,
"HOUR": true,
"SIX_HOURS": false,
"DAY": true,
"WEEK": true,
"WEEK_STARTING_SUNDAY": false,
"WEEK_STARTING_MONDAY": false,
"WEEK_ENDING_SATURDAY": false,
"WEEK_ENDING_SUNDAY": false,
"MONTH": true,
"QUARTER": true,
"QUARTER_YEAR": false,
"YEAR": true
},
"module": "superset.db_engine_specs.datafusion",
"limit_method": 1,
"limit_clause": true,
"joins": true,
"subqueries": true,
"alias_in_select": true,
"alias_in_orderby": true,
"time_groupby_inline": false,
"alias_to_source_column": false,
"order_by_not_in_select": true,
"expressions_in_orderby": false,
"cte_in_subquery": true,
"max_column_name": null,
"sql_comments": true,
"escaped_colons": true,
"masked_encrypted_extra": false,
"column_type_mapping": false,
"function_names": false,
"user_impersonation": false,
"file_upload": false,
"get_extra_table_metadata": false,
"dbapi_exception_mapping": false,
"custom_errors": false,
"dynamic_schema": false,
"catalog": false,
"dynamic_catalog": false,
"ssh_tunneling": true,
"query_cancelation": false,
"get_metrics": false,
"where_latest_partition": false,
"expand_data": false,
"query_cost_estimation": false,
"sql_validation": false,
"score": 24,
"max_score": 201,
"documentation": {
"description": "DataFusion is a highly performant query engine",
"logo": "datafusion.png",
"homepage_url": "https://datafusion.apache.org/",
"categories": [
"Query Engines",
"Open Source",
"Apache Projects"
],
"pypi_packages": [
"flightsql-dbapi"
],
"connection_string": "datafusion://host:port",
"drivers": [
{
"name": "Arrow Flight SQL (Recommended)",
"pypi_package": "flightsql-dbapi",
"connection_string": "datafusion://host:port",
"is_recommended": true
}
],
"category": "Apache Projects"
},
"engine": "datafusion",
"engine_name": "Apache DataFusion",
"engine_aliases": [],
"default_driver": "flightsql",
"supports_file_upload": false,
"supports_dynamic_schema": false,
"supports_catalog": false,
"supports_dynamic_catalog": false
}
}
}

BIN
docs/static/img/databases/datafusion.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/static/img/logos/pattern_logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -2529,20 +2529,20 @@
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe"
integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==
"@fontsource/fira-code@^5.2.7":
version "5.2.7"
resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.2.7.tgz#9ecbd909d53e7196a5d895b601747fe34491fc6a"
integrity sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==
"@fontsource/fira-code@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.3.0.tgz#487475cedfa1f7ba7650810ddce00b6c7b1b7a25"
integrity sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==
"@fontsource/ibm-plex-mono@^5.2.7":
version "5.2.7"
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz#ef5b6f052115fdf6666208a5f8a0f13fcd7ba1fd"
integrity sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==
"@fontsource/ibm-plex-mono@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz#1879699d104602d5331e28e103f33c3b1b766b17"
integrity sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==
"@fontsource/inter@^5.2.8":
version "5.2.8"
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.2.8.tgz#10c95d877d972c7de5bd4592309d42fb6a5e1a5b"
integrity sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==
"@fontsource/inter@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.3.0.tgz#866278dc2357659f8be1755c8b67f29d3697b440"
integrity sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
version "9.3.0"
@@ -4697,86 +4697,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz#386294f8427dde2df1a70dd0a5826d67af70e996"
integrity sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==
"@swc/core-darwin-arm64@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.46.tgz#393903c7eda790dbd89abd8fa0afdd9041543e5f"
integrity sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==
"@swc/core-darwin-x64@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz#c4823529c424e2ae25b7eb786438474741521fcb"
integrity sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==
"@swc/core-darwin-x64@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.46.tgz#ddf16787e320636621180df480a3490fd9a868ca"
integrity sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==
"@swc/core-linux-arm-gnueabihf@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz#c0a0ed17cffc5d4af192935667f12f05feeb39f9"
integrity sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==
"@swc/core-linux-arm-gnueabihf@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.46.tgz#7bee01b7311c43b913771ef9c7012931871de73b"
integrity sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==
"@swc/core-linux-arm64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz#1eb2d9c5eeee5bb9d00599b475ddc31dc2870d22"
integrity sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==
"@swc/core-linux-arm64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.46.tgz#964596d757d18f04a02873d85a3660416c09c187"
integrity sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==
"@swc/core-linux-arm64-musl@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz#ea6b5c38088f3921a57922d3931b2d74fd23a9fd"
integrity sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==
"@swc/core-linux-arm64-musl@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.46.tgz#213d3ece772689a8166ed51064836346c6ce1c2a"
integrity sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==
"@swc/core-linux-ppc64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz#538fac30bbd5f1e678bb7bac9ccc62246a6f6d7a"
integrity sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==
"@swc/core-linux-ppc64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.46.tgz#4d2ec554103c6bef60cc1e294f374ea5a5edaf78"
integrity sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==
"@swc/core-linux-s390x-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz#ee564b45f3f578b1fc82136c4dab163189316641"
integrity sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==
"@swc/core-linux-s390x-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.46.tgz#097a19792ec22e2f51f6bfac02da1e0b3f5e5bb1"
integrity sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==
"@swc/core-linux-x64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz#e6e3bfea76921c7f5e16d50a126615f2e04ce1c8"
integrity sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==
"@swc/core-linux-x64-gnu@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.46.tgz#39c1ca215f9ca643a4aa3ca6250cc38ba5f5c673"
integrity sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==
"@swc/core-linux-x64-musl@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz#539f6f2721c0cc32e5db5cf0d453c82045f6662d"
integrity sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==
"@swc/core-linux-x64-musl@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.46.tgz#323a720bc965fffeedacdc3167b46a291553b5e0"
integrity sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==
"@swc/core-win32-arm64-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz#b7bb6b611d484ac19d0ee21469e7012d646c28b5"
integrity sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==
"@swc/core-win32-arm64-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.46.tgz#9c2cfd2a59be74671a018097b8914f8cfbcc698d"
integrity sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==
"@swc/core-win32-ia32-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz#e5b25722a7d27bb0c9a9bdee7863f29c8674364e"
integrity sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==
"@swc/core-win32-ia32-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.46.tgz#bd7bd009a47b0f9826212e7ed36385d32fe193d8"
integrity sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==
"@swc/core-win32-x64-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz#d28842621201c345383d468d40c09648b6cd6e68"
integrity sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==
"@swc/core-win32-x64-msvc@1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.46.tgz#8371845a5bdb330cf05b009f602bb8c4636c6beb"
integrity sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==
"@swc/core@^1.15.40", "@swc/core@^1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.43.tgz#653e6573968fd5c74163b9885ea0a933012c9f22"
integrity sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==
"@swc/core@^1.15.40", "@swc/core@^1.15.46":
version "1.15.46"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.46.tgz#8acc0f68ee55010fdc876adf2a8faf0b097c681b"
integrity sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.27"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.43"
"@swc/core-darwin-x64" "1.15.43"
"@swc/core-linux-arm-gnueabihf" "1.15.43"
"@swc/core-linux-arm64-gnu" "1.15.43"
"@swc/core-linux-arm64-musl" "1.15.43"
"@swc/core-linux-ppc64-gnu" "1.15.43"
"@swc/core-linux-s390x-gnu" "1.15.43"
"@swc/core-linux-x64-gnu" "1.15.43"
"@swc/core-linux-x64-musl" "1.15.43"
"@swc/core-win32-arm64-msvc" "1.15.43"
"@swc/core-win32-ia32-msvc" "1.15.43"
"@swc/core-win32-x64-msvc" "1.15.43"
"@swc/core-darwin-arm64" "1.15.46"
"@swc/core-darwin-x64" "1.15.46"
"@swc/core-linux-arm-gnueabihf" "1.15.46"
"@swc/core-linux-arm64-gnu" "1.15.46"
"@swc/core-linux-arm64-musl" "1.15.46"
"@swc/core-linux-ppc64-gnu" "1.15.46"
"@swc/core-linux-s390x-gnu" "1.15.46"
"@swc/core-linux-x64-gnu" "1.15.46"
"@swc/core-linux-x64-musl" "1.15.46"
"@swc/core-win32-arm64-msvc" "1.15.46"
"@swc/core-win32-ia32-msvc" "1.15.46"
"@swc/core-win32-x64-msvc" "1.15.46"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -6394,9 +6394,9 @@ binary-extensions@^2.0.0:
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
body-parser@~1.20.5:
version "1.20.5"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.5.tgz#303c8c34423d1d6fa799bc764e93c1e4dc6ebf64"
integrity sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==
version "1.20.6"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76"
integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==
dependencies:
bytes "~3.1.2"
content-type "~1.0.5"
@@ -6602,10 +6602,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001805:
version "1.0.30001805"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001806:
version "1.0.30001806"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e"
integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==
ccount@^2.0.0:
version "2.0.1"
@@ -7007,7 +7007,7 @@ copy-text-to-clipboard@^3.2.0:
resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz#99bc79db3f2d355ec33a08d573aff6804491ddb9"
integrity sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==
copy-to-clipboard@^3.3.1:
copy-to-clipboard@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
@@ -10155,10 +10155,10 @@ js-yaml@=4.2.0:
dependencies:
argparse "^2.0.1"
js-yaml@^5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.2.1.tgz#bb3f2e87295ebfe6ef0a75e17c62834daeff9ce2"
integrity sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==
js-yaml@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.2.2.tgz#497bfe63f0b0db11c7bbc5ce8bc568e836c8b08c"
integrity sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==
dependencies:
argparse "^2.0.1"
@@ -11681,10 +11681,10 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nanoid@^3.3.12:
version "3.3.15"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316"
integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==
nanoid@^3.3.16:
version "3.3.16"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
natural-compare@^1.4.0:
version "1.4.0"
@@ -12974,11 +12974,11 @@ postcss-zindex@^6.0.2:
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.33, postcss@^8.5.4:
version "8.5.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28"
integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==
version "8.5.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594"
integrity sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==
dependencies:
nanoid "^3.3.12"
nanoid "^3.3.16"
picocolors "^1.1.1"
source-map-js "^1.2.1"
@@ -13243,12 +13243,12 @@ rc@1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-copy-to-clipboard@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c"
integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==
react-copy-to-clipboard@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.1.tgz#76adb8be03616e99692fcf3f762365ed3fb5ff16"
integrity sha512-s+HrzLyJBxrpGTYXF15dTgMjAJpEPZT/Yp6NytAtZMRngejxt6Pt5WrfFxLAcsqUDU6sY1Jz6tyHwIicE1U2Xg==
dependencies:
copy-to-clipboard "^3.3.1"
copy-to-clipboard "^3.3.3"
prop-types "^15.8.1"
react-debounce-input@=3.3.0:
@@ -14925,10 +14925,10 @@ swagger-client@^3.37.4:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.32.8:
version "5.32.8"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.8.tgz#0608b45cf552f33fcc9b3fc5e07740c9a854861f"
integrity sha512-Cstx4Tq8fT5l2TBxHxts8pG+ks0qKSkuO1pwUwgrQQiZ241Mqs+KUODLVIonsYXL/gqX143rkcipUa4d0Rid7w==
swagger-ui-react@^5.32.9:
version "5.32.9"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.32.9.tgz#d0e8521091b270e35f8d87771fba49eede425b38"
integrity sha512-zs6PG4G6XEWG2evKHSwx32nWXIlk48a9Hw9YS9hlt55O5W0FM1cpltMrM/AnU+mohdnV7oEx0ap2TaG3fbfLIA==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -14946,7 +14946,7 @@ swagger-ui-react@^5.32.8:
prop-types "^15.8.1"
randexp "^0.5.3"
randombytes "^2.1.0"
react-copy-to-clipboard "5.1.0"
react-copy-to-clipboard "5.1.1"
react-debounce-input "=3.3.0"
react-immutable-proptypes "2.2.0"
react-immutable-pure-component "^2.2.0"

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.22.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.22.4 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.22.1](https://img.shields.io/badge/Version-0.22.1-informational?style=flat-square)
![Version: 0.22.4](https://img.shields.io/badge/Version-0.22.4-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -131,7 +131,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| httproute.hostnames | list | `[]` | Hostnames that match against the HTTP Host header (templated) |
| httproute.labels | object | `{}` | Additional labels to add to the HTTPRoute |
| httproute.parentRefs | list | `[]` | Gateways this HTTPRoute attaches to |
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. |
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and `supersetMcp.httproute.enabled` are true, an extra rule routing `supersetMcp.httproute.path` to the `-mcp` service is also appended. |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
| image.tag | string | `nil` | |
@@ -159,6 +159,10 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| init.extraInitContainers | list | `[]` | Extra init containers appended after init job initContainers |
| init.initContainers | list | a container waiting for postgres | List of initContainers |
| init.initscript | string | unused; kept for backwards-compatibility only | DEPRECATED: this field is no longer used by the chart. The init script is rendered entirely from the internal `superset.initScript` template (which runs `superset db upgrade`, `superset init`, admin creation, and examples). Any customization placed here is silently ignored. See UPGRADING.md. |
| init.istio | object | `{"disableSidecarInjection":false,"quitEndpoint":"http://localhost:15020/quitquitquit","terminateSidecarOnExit":false}` | Configuration for compatibility with the Istio service mesh. Without these mitigations, when the init-db Job runs in a namespace with automatic Istio sidecar injection enabled, the injected envoy-proxy container keeps running after the init container has exited, preventing the Job from ever reaching the Completed state. See https://github.com/apache/superset/issues/25798 |
| init.istio.disableSidecarInjection | bool | `false` | When true, adds the `sidecar.istio.io/inject: "false"` label to the init job pod template, opting it out of Istio automatic sidecar injection. This is the recommended fix when the cluster's mesh policy allows opting out per pod. |
| init.istio.quitEndpoint | string | `"http://localhost:15020/quitquitquit"` | Endpoint that the trap POSTs to in order to terminate the sidecar. The default matches the standard Istio pilot-agent admin port. |
| init.istio.terminateSidecarOnExit | bool | `false` | When true, the rendered `superset_init.sh` script (from the internal `superset.initScript` template) registers an `EXIT` trap that POSTs to the Istio pilot-agent's `/quitquitquit` endpoint after the init logic finishes, gracefully terminating an injected envoy-proxy sidecar so the Job can complete. Enable this when sidecar injection cannot be disabled per pod (e.g. when enforced by a cluster-wide Istio policy). Note: requires `curl` to be available in the init container image (it is included in the default `apache/superset` image) and only takes effect when `init.command` sources the rendered `superset_init.sh`. |
| init.jobAnnotations."helm.sh/hook" | string | `"post-install,post-upgrade"` | |
| init.jobAnnotations."helm.sh/hook-delete-policy" | string | `"before-hook-creation"` | |
| init.loadExamples | bool | `false` | |
@@ -266,6 +270,8 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetMcp.extraContainers | list | `[]` | Launch additional containers into supersetMcp pods |
| supersetMcp.extraInitContainers | list | `[]` | Extra init containers appended after supersetMcp initContainers |
| supersetMcp.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetMcp.httproute.enabled | bool | `false` | If true, the MCP server will be exposed via the HTTPRoute /mcp subpath |
| supersetMcp.httproute.path | string | `"/mcp"` | |
| supersetMcp.ingress.enabled | bool | `false` | If true, the MCP server will be exposed via the ingress /mcp subpath |
| supersetMcp.ingress.path | string | `"/mcp"` | |
| supersetMcp.ingress.pathType | string | `"Prefix"` | |

View File

@@ -685,6 +685,14 @@ TALISMAN_CONFIG = {
{{- define "superset.initScript" -}}
#!/bin/sh
set -eu
{{- if dig "istio" "terminateSidecarOnExit" false .Values.init }}
# Notify the Istio pilot-agent sidecar to exit when this script completes
# (whether successfully or via `set -e`), so that the Job can reach the
# Completed state instead of hanging on a still-running envoy-proxy.
# See https://github.com/apache/superset/issues/25798
ISTIO_QUIT_ENDPOINT={{ dig "istio" "quitEndpoint" "http://localhost:15020/quitquitquit" .Values.init | replace "'" "'\\''" | squote }}
trap 'rc=$?; curl -fsS -m 5 -X POST "$ISTIO_QUIT_ENDPOINT" >/dev/null 2>&1 || echo "WARNING: failed to notify Istio sidecar at $ISTIO_QUIT_ENDPOINT to quit; the Job may hang if sidecar injection is active" >&2; exit $rc' EXIT
{{- end }}
echo "Upgrading DB schema..."
superset db upgrade
echo "Initializing roles and permissions..."

View File

@@ -80,4 +80,16 @@ spec:
type: PathPrefix
value: {{ .Values.supersetWebsockets.ingress.path }}
{{- end }}
{{- if and .Values.supersetMcp.enabled .Values.supersetMcp.httproute.enabled }}
- backendRefs:
- group: ''
kind: Service
name: {{ $fullName }}-mcp
port: {{ .Values.supersetMcp.service.port }}
weight: 1
matches:
- path:
type: PathPrefix
value: {{ .Values.supersetMcp.httproute.path }}
{{- end }}
{{- end }}

View File

@@ -35,6 +35,7 @@ spec:
{{- if .Values.init.podAnnotations }}
annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }}
{{- end }}
{{- $istioDisableInject := dig "istio" "disableSidecarInjection" false .Values.init }}
labels:
{{- include "superset.componentSelectorLabels" (dict "component" "init" "root" .) | nindent 8 }}
job: {{ template "superset.fullname" . }}-init-db
@@ -44,6 +45,9 @@ spec:
{{- if .Values.init.podLabels }}
{{- toYaml .Values.init.podLabels | nindent 8 }}
{{- end }}
{{- if $istioDisableInject }}
sidecar.istio.io/inject: "false"
{{- end }}
spec:
{{- if .Values.init.additionalPodSpec }}
{{- tpl (toYaml .Values.init.additionalPodSpec) . | nindent 6 }}

View File

@@ -0,0 +1,92 @@
#
# 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.
#
suite: HTTPRoute
templates:
- httproute.yaml
tests:
- it: routes the general Superset path
set:
httproute.enabled: true
asserts:
- lengthEqual:
path: spec.rules
count: 1
- equal:
path: spec.rules[0].backendRefs[0].name
value: RELEASE-NAME-superset
- equal:
path: spec.rules[0].backendRefs[0].port
value: 8088
- equal:
path: spec.rules[0].matches[0].path.value
value: /
- it: routes the WebSocket path when WebSockets are enabled
set:
httproute.enabled: true
supersetWebsockets.enabled: true
asserts:
- lengthEqual:
path: spec.rules
count: 2
- equal:
path: spec.rules[1].backendRefs[0].name
value: RELEASE-NAME-superset-ws
- equal:
path: spec.rules[1].backendRefs[0].port
value: 8080
- equal:
path: spec.rules[1].matches[0].path.value
value: /ws
- it: does not route the WebSocket path when WebSockets are disabled
set:
httproute.enabled: true
supersetWebsockets.enabled: false
asserts:
- lengthEqual:
path: spec.rules
count: 1
- it: routes the MCP path when MCP HTTPRoute is enabled
set:
httproute.enabled: true
supersetMcp.enabled: true
supersetMcp.httproute.enabled: true
asserts:
- lengthEqual:
path: spec.rules
count: 2
- equal:
path: spec.rules[1].backendRefs[0].name
value: RELEASE-NAME-superset-mcp
- equal:
path: spec.rules[1].backendRefs[0].port
value: 5008
- equal:
path: spec.rules[1].matches[0].path.value
value: /mcp
- it: does not route MCP when MCP HTTPRoute is disabled
set:
httproute.enabled: true
supersetMcp.enabled: true
supersetMcp.httproute.enabled: false
asserts:
- lengthEqual:
path: spec.rules
count: 1

183
helm/superset/tests/test-istio.sh Executable file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
#
# 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.
#
# Renders the chart with `helm template` for several `init.istio.*` value
# combinations and asserts that the relevant manifests contain (or omit)
# the expected fields. Intended to be run from the chart directory or via
# `bash helm/superset/tests/test-istio.sh` from the repo root.
#
# Covers the fix for:
# https://github.com/apache/superset/issues/25798
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHART_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
pass=0
fail=0
assert_contains() {
local label="$1"
local needle="$2"
local haystack="$3"
if grep -qF -- "${needle}" <<<"${haystack}"; then
echo " PASS: ${label}"
pass=$((pass + 1))
else
echo " FAIL: ${label}"
echo " expected to contain: ${needle}"
fail=$((fail + 1))
fi
}
assert_not_contains() {
local label="$1"
local needle="$2"
local haystack="$3"
if ! grep -qF -- "${needle}" <<<"${haystack}"; then
echo " PASS: ${label}"
pass=$((pass + 1))
else
echo " FAIL: ${label}"
echo " expected NOT to contain: ${needle}"
fail=$((fail + 1))
fi
}
render() {
helm template release "${CHART_DIR}" "$@"
}
extract_init_job() {
awk '
/^# Source: superset\/templates\/init-job\.yaml/ { capture = 1 }
capture && /^---$/ { capture = 0 }
capture { print }
' <<<"$1"
}
extract_config_secret() {
awk '
/^# Source: superset\/templates\/secret-superset-config\.yaml/ { capture = 1 }
capture && /^---$/ { capture = 0 }
capture { print }
' <<<"$1"
}
echo "==> defaults: no istio mitigations applied"
out_default="$(render)"
init_job_default="$(extract_init_job "${out_default}")"
config_default="$(extract_config_secret "${out_default}")"
assert_not_contains "default does not set sidecar.istio.io/inject label" \
'sidecar.istio.io/inject' "${init_job_default}"
assert_not_contains "default initscript does not register quitquitquit trap" \
'quitquitquit' "${config_default}"
echo "==> init.istio.disableSidecarInjection=true"
out_disable="$(render --set init.istio.disableSidecarInjection=true)"
init_job_disable="$(extract_init_job "${out_disable}")"
config_disable="$(extract_config_secret "${out_disable}")"
assert_contains "init job sets sidecar.istio.io/inject: \"false\"" \
'sidecar.istio.io/inject: "false"' "${init_job_disable}"
assert_not_contains "disableSidecarInjection alone does not add quitquitquit trap" \
'quitquitquit' "${config_disable}"
echo "==> init.istio.terminateSidecarOnExit=true"
out_terminate="$(render --set init.istio.terminateSidecarOnExit=true)"
init_job_terminate="$(extract_init_job "${out_terminate}")"
config_terminate="$(extract_config_secret "${out_terminate}")"
assert_not_contains "terminateSidecarOnExit alone does not add inject label" \
'sidecar.istio.io/inject' "${init_job_terminate}"
assert_contains "initscript registers EXIT trap that calls quitquitquit" \
"trap 'rc=\$?; curl -fsS -m 5 -X POST" "${config_terminate}"
assert_contains "initscript trap targets the configured quit endpoint" \
'http://localhost:15020/quitquitquit' "${config_terminate}"
assert_contains "initscript trap binds to the EXIT signal" \
"' EXIT" "${config_terminate}"
echo "==> init.istio.quitEndpoint override"
out_endpoint="$(render --set init.istio.terminateSidecarOnExit=true \
--set init.istio.quitEndpoint=http://127.0.0.1:15020/quitquitquit)"
config_endpoint="$(extract_config_secret "${out_endpoint}")"
assert_contains "trap honours custom quitEndpoint" \
'http://127.0.0.1:15020/quitquitquit' "${config_endpoint}"
echo "==> both options combined"
out_both="$(render --set init.istio.disableSidecarInjection=true \
--set init.istio.terminateSidecarOnExit=true)"
init_job_both="$(extract_init_job "${out_both}")"
config_both="$(extract_config_secret "${out_both}")"
assert_contains "combined: inject label present" \
'sidecar.istio.io/inject: "false"' "${init_job_both}"
assert_contains "combined: trap present" \
'quitquitquit' "${config_both}"
echo "==> existing init.podLabels are preserved alongside istio label"
out_labels="$(render --set init.istio.disableSidecarInjection=true \
--set init.podLabels.team=data-platform)"
init_job_labels="$(extract_init_job "${out_labels}")"
assert_contains "user-supplied podLabel still rendered" \
'team: data-platform' "${init_job_labels}"
assert_contains "istio inject label rendered alongside" \
'sidecar.istio.io/inject: "false"' "${init_job_labels}"
echo "==> init.istio explicitly overridden to null"
out_null_istio="$(render --set init.istio=null)"
init_job_null_istio="$(extract_init_job "${out_null_istio}")"
config_null_istio="$(extract_config_secret "${out_null_istio}")"
assert_not_contains "null init.istio does not set inject label" \
'sidecar.istio.io/inject' "${init_job_null_istio}"
assert_not_contains "null init.istio does not register quitquitquit trap" \
'quitquitquit' "${config_null_istio}"
echo "==> EXIT trap propagates the script's exit code, not the notification's"
# The trap's own curl call is best-effort (failures are logged with
# `|| echo ... >&2`, not swallowed with `|| true`) and must not mask a
# failed migration. Extract the two rendered lines and actually run them,
# with curl pointed at a closed local port so the notification itself fails,
# to make sure the wrapped script's real exit code still comes through.
quit_endpoint_line="$(grep -F 'ISTIO_QUIT_ENDPOINT=' <<<"${config_terminate}" || true)"
trap_line="$(grep -F "trap 'rc=\$?; curl" <<<"${config_terminate}" || true)"
if [[ -z "${quit_endpoint_line}" || -z "${trap_line}" ]]; then
echo " FAIL: script exit code (42) survives a failing quitquitquit notification"
echo " could not locate the rendered ISTIO_QUIT_ENDPOINT/trap lines to exercise"
fail=$((fail + 1))
else
set +e
(
eval "${quit_endpoint_line}"
ISTIO_QUIT_ENDPOINT="http://127.0.0.1:1/quitquitquit"
eval "${trap_line}"
exit 42
)
trap_test_rc=$?
set -e
if [[ "${trap_test_rc}" -eq 42 ]]; then
echo " PASS: script exit code (42) survives a failing quitquitquit notification"
pass=$((pass + 1))
else
echo " FAIL: script exit code (42) survives a failing quitquitquit notification"
echo " got exit code: ${trap_test_rc}"
fail=$((fail + 1))
fi
fi
echo
echo "passed: ${pass}, failed: ${fail}"
if [[ ${fail} -gt 0 ]]; then
exit 1
fi

View File

@@ -98,13 +98,13 @@ envFromSecrets: []
# -- Extra environment variables that will be passed into pods
extraEnv: {}
# Different gunicorn settings, refer to the gunicorn documentation
# https://docs.gunicorn.org/en/stable/settings.html#
# https://gunicorn.org/reference/settings/
# These variables are used as Flags at the gunicorn startup
# https://github.com/apache/superset/blob/master/docker/run-server.sh#L22
# https://github.com/apache/superset/blob/master/docker/entrypoints/run-server.sh#L22
# Extend timeout to allow long running queries.
# GUNICORN_TIMEOUT: 300
# Increase the gunicorn worker amount, can improve performance drastically
# See: https://docs.gunicorn.org/en/stable/design.html#how-many-workers
# See: https://gunicorn.org/design/#how-many-workers
# SERVER_WORKER_AMOUNT: 4
# WORKER_MAX_REQUESTS: 0
# WORKER_MAX_REQUESTS_JITTER: 0
@@ -277,7 +277,9 @@ httproute:
# per rule to leave room for traffic splitting (defaults to 1). When
# `supersetWebsockets.enabled` is true, an extra rule routing
# `supersetWebsockets.ingress.path` to the `-ws` service is appended
# automatically, mirroring the ingress behavior.
# automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and
# `supersetMcp.httproute.enabled` are true, an extra rule routing
# `supersetMcp.httproute.path` to the `-mcp` service is also appended.
rules:
- matches:
- path:
@@ -987,7 +989,6 @@ supersetWebsockets:
# -- Set priorityClassName for supersetWebsockets pods
priorityClassName: ~
supersetMcp:
# -- Enables the Superset MCP Server. To expose it via the shared ingress at /mcp,
# also set supersetMcp.ingress.enabled=true.
@@ -1015,6 +1016,10 @@ supersetMcp:
enabled: false
path: /mcp
pathType: Prefix
httproute:
# -- If true, the MCP server will be exposed via the HTTPRoute /mcp subpath
enabled: false
path: /mcp
service:
type: ClusterIP
annotations: {}
@@ -1236,6 +1241,31 @@ init:
topologySpreadConstraints: []
# -- Set priorityClassName for init job pods
priorityClassName: ~
# -- Configuration for compatibility with the Istio service mesh.
# Without these mitigations, when the init-db Job runs in a namespace with
# automatic Istio sidecar injection enabled, the injected envoy-proxy
# container keeps running after the init container has exited, preventing
# the Job from ever reaching the Completed state.
# See https://github.com/apache/superset/issues/25798
istio:
# -- When true, adds the `sidecar.istio.io/inject: "false"` label to the
# init job pod template, opting it out of Istio automatic sidecar
# injection. This is the recommended fix when the cluster's mesh policy
# allows opting out per pod.
disableSidecarInjection: false
# -- When true, the rendered `superset_init.sh` script (from the internal
# `superset.initScript` template) registers an `EXIT` trap that POSTs to
# the Istio pilot-agent's `/quitquitquit` endpoint after the init logic
# finishes, gracefully terminating an injected envoy-proxy sidecar so
# the Job can complete. Enable this when sidecar injection cannot be
# disabled per pod (e.g. when enforced by a cluster-wide Istio policy).
# Note: requires `curl` to be available in the init container image
# (it is included in the default `apache/superset` image) and only
# takes effect when `init.command` sources the rendered `superset_init.sh`.
terminateSidecarOnExit: false
# -- Endpoint that the trap POSTs to in order to terminate the sidecar.
# The default matches the standard Istio pilot-agent admin port.
quitEndpoint: "http://localhost:15020/quitquitquit"
# -- Configuration values for the postgresql dependency.
# ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql

View File

@@ -54,7 +54,7 @@ dependencies = [
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.2, <6.0.0",
"flask-caching>=2.1.0, <3",
"flask-caching>=2.4.1, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",
@@ -92,7 +92,7 @@ dependencies = [
"Pillow>=11.0.0, <13",
"polyline>=2.0.0, <3.0",
"pydantic>=2.8.0",
"pyparsing>=3.0.6, <4",
"pyparsing>=3.3.2, <4",
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash",
@@ -106,7 +106,7 @@ dependencies = [
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy>=1.4.43, <2", # 1.4.43 adds the python-oracledb (oracle+oracledb) dialect
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.12.0, <31",
@@ -127,7 +127,7 @@ aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
"sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.42.1",
"google-cloud-bigquery>=3.42.2",
]
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
@@ -139,10 +139,11 @@ d1 = [
]
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
databricks = [
"databricks-sql-connector==4.2.6",
"databricks-sql-connector>=4.2.6, <4.4.0",
"databricks-sqlalchemy==1.0.5",
]
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
@@ -188,7 +189,7 @@ ocient = [
"shapely",
"geojson",
]
oracle = ["oracledb>=2.0.0, <5"]
oracle = ["oracledb>=4.0.2, <5"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.61.0, <2"]
@@ -200,7 +201,7 @@ redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
spark = [
"pyhive[hive_pure_sasl]>=0.7",
@@ -209,7 +210,7 @@ spark = [
]
tdengine = [
"taospy>=2.8.9",
"taos-ws-py>=0.6.9"
"taos-ws-py>=0.7.0"
]
teradata = ["teradatasql>=20.0.0.62"]
thumbnails = [] # deprecated, will be removed in 7.0
@@ -226,7 +227,7 @@ development = [
"docker",
"flask-testing",
"freezegun",
"grpcio>=1.81.1",
"grpcio>=1.82.1",
"openapi-spec-validator",
"parameterized",
"pip",

View File

@@ -36,11 +36,9 @@ filterwarnings =
error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning
error:The legacy calling style of select\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:The "whens" argument to case:sqlalchemy.exc.RemovedIn20Warning
# error:"User" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -28,6 +28,8 @@ babel==2.17.0
# via flask-babel
backoff==2.2.1
# via apache-superset (pyproject.toml)
backports-zstd==1.6.0
# via flask-compress
bcrypt==4.3.0
# via paramiko
billiard==4.2.1
@@ -128,9 +130,9 @@ flask-appbuilder==5.2.2
# apache-superset-core
flask-babel==3.1.0
# via flask-appbuilder
flask-caching==2.3.1
flask-caching==2.4.1
# via apache-superset (pyproject.toml)
flask-compress==1.17
flask-compress==1.24
# via apache-superset (pyproject.toml)
flask-cors==6.0.5
# via apache-superset (pyproject.toml)
@@ -244,7 +246,7 @@ msgpack==1.2.1
# via apache-superset (pyproject.toml)
msgspec==0.19.0
# via flask-session
nh3==0.3.5
nh3==0.3.6
# via apache-superset (pyproject.toml)
numexpr==2.10.2
# via -r requirements/base.in
@@ -285,7 +287,7 @@ parsedatetime==2.6
# via apache-superset (pyproject.toml)
pgsanity==0.2.9
# via apache-superset (pyproject.toml)
pillow==12.2.0
pillow==12.3.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
@@ -300,17 +302,17 @@ pyarrow==24.0.0
# -r requirements/base.in
# apache-superset (pyproject.toml)
# apache-superset-core
pyasn1==0.6.3
pyasn1==0.6.4
# via pyasn1-modules
pyasn1-modules==0.4.2
# via google-auth
pycparser==2.22
# via cffi
pydantic==2.11.7
pydantic==2.13.4
# via
# apache-superset (pyproject.toml)
# apache-superset-core
pydantic-core==2.33.2
pydantic-core==2.46.4
# via pydantic
pygeohash==3.2.2
# via apache-superset (pyproject.toml)
@@ -327,7 +329,7 @@ pyopenssl==26.3.0
# via
# -r requirements/base.in
# shillelagh
pyparsing==3.2.3
pyparsing==3.3.2
# via apache-superset (pyproject.toml)
pysocks==1.7.1
# via urllib3
@@ -408,7 +410,7 @@ sqlalchemy==1.4.54
# shillelagh
# sqlalchemy-continuum
# sqlalchemy-utils
sqlalchemy-continuum==1.6.0
sqlalchemy-continuum==1.7.0
# via apache-superset (pyproject.toml)
sqlalchemy-utils==0.42.1
# via
@@ -443,7 +445,7 @@ typing-extensions==4.16.0
# selenium
# shillelagh
# typing-inspection
typing-inspection==0.4.1
typing-inspection==0.4.2
# via pydantic
tzdata==2025.2
# via
@@ -496,5 +498,3 @@ xlsxwriter==3.2.9
# via
# apache-superset (pyproject.toml)
# pandas
zstandard==0.23.0
# via flask-compress

View File

@@ -64,6 +64,10 @@ backoff==2.2.1
# apache-superset
backports-tarfile==1.2.0
# via jaraco-context
backports-zstd==1.6.0
# via
# -c requirements/base-constraint.txt
# flask-compress
bcrypt==4.3.0
# via
# -c requirements/base-constraint.txt
@@ -272,11 +276,11 @@ flask-babel==3.1.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
flask-caching==2.3.1
flask-caching==2.4.1
# via
# -c requirements/base-constraint.txt
# apache-superset
flask-compress==1.17
flask-compress==1.24
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -337,7 +341,7 @@ geopy==2.4.1
# apache-superset
gevent==26.4.0
# via apache-superset
google-api-core==2.23.0
google-api-core==2.33.0
# via
# google-cloud-bigquery
# google-cloud-core
@@ -358,7 +362,7 @@ google-auth-oauthlib==1.2.1
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.42.1
google-cloud-bigquery==3.42.2
# via
# apache-superset
# pandas-gbq
@@ -382,7 +386,7 @@ greenlet==3.5.3
# sqlalchemy
griffelib==2.0.2
# via fastmcp-slim
grpcio==1.81.1
grpcio==1.83.0
# via
# apache-superset
# google-api-core
@@ -570,7 +574,7 @@ msgspec==0.19.0
# flask-session
mysqlclient==2.2.8
# via apache-superset
nh3==0.3.5
nh3==0.3.6
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -665,7 +669,7 @@ pgsanity==0.2.9
# via
# -c requirements/base-constraint.txt
# apache-superset
pillow==12.2.0
pillow==12.3.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -724,7 +728,7 @@ pyarrow==24.0.0
# apache-superset-core
# db-dtypes
# pandas-gbq
pyasn1==0.6.3
pyasn1==0.6.4
# via
# -c requirements/base-constraint.txt
# pyasn1-modules
@@ -738,7 +742,7 @@ pycparser==2.22
# via
# -c requirements/base-constraint.txt
# cffi
pydantic==2.11.7
pydantic==2.13.4
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -747,7 +751,7 @@ pydantic==2.11.7
# mcp
# openapi-pydantic
# pydantic-settings
pydantic-core==2.33.2
pydantic-core==2.46.4
# via
# -c requirements/base-constraint.txt
# pydantic
@@ -791,7 +795,7 @@ pyopenssl==26.3.0
# -c requirements/base-constraint.txt
# google-auth
# shillelagh
pyparsing==3.2.3
pyparsing==3.3.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -987,7 +991,7 @@ sqlalchemy==1.4.54
# sqlalchemy-utils
sqlalchemy-bigquery==1.17.0
# via apache-superset
sqlalchemy-continuum==1.6.0
sqlalchemy-continuum==1.7.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1066,7 +1070,7 @@ typing-extensions==4.16.0
# shillelagh
# starlette
# typing-inspection
typing-inspection==0.4.1
typing-inspection==0.4.2
# via
# -c requirements/base-constraint.txt
# mcp
@@ -1168,7 +1172,4 @@ zope-event==5.0
zope-interface==5.4.0
# via gevent
zstandard==0.23.0
# via
# -c requirements/base-constraint.txt
# flask-compress
# trino
# via trino

View File

@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
# where the current model is out-of-sync with the existing table after a
# downgrade
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
engine = create_engine(sqlalchemy_uri)
engine = create_engine(sqlalchemy_uri, future=True)
Base = automap_base() # noqa: N806
Base.prepare(engine, reflect=True)
seen = set()

View File

@@ -2662,9 +2662,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3937,9 +3937,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -4114,9 +4114,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"dev": true,
"funding": [
{
@@ -4134,7 +4134,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},

View File

@@ -97,7 +97,7 @@ export const databasesPage = {
infoAlert: '.ant-alert',
serviceAccountInput: '[name="credentials_info"]',
connectionStep: {
modal: '.ant-modal-content',
modal: '.ant-modal-container',
modalBody: '.ant-modal-body',
stepTitle: '.css-7x6kk > h4',
helperBottom: '.helper-bottom',
@@ -261,7 +261,7 @@ export const datasetsList = {
},
},
deleteDatasetModal: {
modal: '.ant-modal-content',
modal: '.ant-modal-container',
deleteInput: dataTestLocator('delete-modal-input'),
deleteButton: dataTestLocator('modal-confirm-button'),
text: '.css-kxmt87',
@@ -474,7 +474,7 @@ export const exploreView = {
},
chartAreaItem: '.nv-legend-text',
viewQueryModal: {
container: '.ant-modal-content',
container: '.ant-modal-container',
closeButton: 'button.ant-modal-close',
},
embedCodeModal: {
@@ -482,7 +482,7 @@ export const exploreView = {
textfield: dataTestLocator('embed-code-textarea'),
},
saveModal: {
modal: '.ant-modal-content',
modal: '.ant-modal-container',
chartNameInput: dataTestLocator('new-chart-name'),
dashboardNameInput: '.ant-select-input',
addToDashboardInput: dataTestLocator(
@@ -578,7 +578,7 @@ export const exploreView = {
},
},
editDatasetModal: {
container: '.ant-modal-content',
container: '.ant-modal-container',
datasetTabsContainer: dataTestLocator('edit-dataset-tabs'),
saveButton: dataTestLocator('datasource-modal-save'),
metricsTab: {
@@ -617,12 +617,12 @@ export const dashboardView = {
closeButton: dataTestLocator('close-button'),
},
saveModal: {
modal: '.ant-modal-content',
modal: '.ant-modal-container',
dashboardNameInput: '.ant-input',
saveButton: dataTestLocator('modal-save-dashboard-button'),
},
dashboardProperties: {
modal: '.ant-modal-content',
modal: '.ant-modal-container',
dashboardTitleInput: dataTestLocator('dashboard-title-input'),
modalButton: '[type="button"]',
},

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"chai": "^4.5.0",
"cy-verify-downloads": "^0.2.5",
"cypress-fail-on-console-error": "^4.0.3",
"nanoid": "^5.0.9",
"nanoid": "^6.0.0",
"querystringify": "^2.2.0",
"react-dom": "^16.13.0",
"rison": "^0.1.1"

View File

@@ -215,6 +215,7 @@
"jsx-a11y/no-noninteractive-tabindex": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/prefer-tag-over-role": "error",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/scope": "error",

File diff suppressed because it is too large Load Diff

View File

@@ -115,9 +115,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"@googleapis/sheets": "^13.0.2",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
@@ -130,9 +130,9 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^6.6.2",
"@rjsf/core": "^6.7.0",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.6.2",
"@rjsf/validator-ajv8": "^6.7.0",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -166,10 +166,10 @@
"@visx/scale": "^4.0.0",
"@visx/tooltip": "^4.0.0",
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"ag-grid-community": "36.0.1",
"ag-grid-react": "36.0.1",
"antd": "^6.5.1",
"chrono-node": "^2.10.0",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
"d3-scale": "^4.0.2",
@@ -187,7 +187,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.9.0",
"immer": "^11.1.11",
"immer": "^11.1.15",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -201,12 +201,12 @@
"memoize-one": "^6.0.0",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.16",
"nanoid": "^6.0.0",
"ol": "^10.9.0",
"query-string": "9.4.1",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.13.2",
"react-arborist": "^3.15.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.4.0",
"react-dnd": "^11.1.3",
@@ -263,16 +263,16 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.17",
"@formatjs/intl-durationformat": "^0.10.18",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.0",
"@storybook/addon-links": "10.5.0",
"@storybook/react-webpack5": "10.5.0",
"@storybook/addon-docs": "10.5.2",
"@storybook/addon-links": "10.5.2",
"@storybook/react-webpack5": "10.5.2",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.43",
"@swc/core": "^1.15.46",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
@@ -325,7 +325,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.5.0",
"eslint-plugin-storybook": "10.5.2",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -340,7 +340,7 @@
"js-yaml-loader": "^1.2.2",
"jsdom": "^29.1.1",
"lerna": "^9.0.4",
"lightningcss": "^1.32.0",
"lightningcss": "^1.33.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
@@ -354,12 +354,12 @@
"react-refresh": "^0.18.0",
"react-resizable": "^4.0.2",
"redux-mock-store": "^1.5.4",
"source-map": "^0.7.6",
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.0",
"storybook": "10.5.2",
"style-loader": "^4.0.0",
"stylelint": "^17.14.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.11",
"tscw-config": "^1.1.2",

View File

@@ -108,7 +108,7 @@
"@emotion/styled": "^11.14.1",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"nanoid": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",

View File

@@ -16,9 +16,21 @@
* specific language governing permissions and limitationsxw
* under the License.
*/
import { PostProcessingProphet, getXAxisLabel } from '@superset-ui/core';
import {
PostProcessingProphet,
TimeGranularity,
getXAxisColumn,
getXAxisLabel,
isAdhocColumn,
} from '@superset-ui/core';
import { PostProcessingFactory } from './types';
// Fallback grain used only when no time grain can be resolved from the form
// data, query object, or x-axis column. Matches the `time_grain_sqla` control
// default in sharedControls so forecasting stays functional rather than failing
// with an opaque backend error.
const DEFAULT_TIME_GRAIN = TimeGranularity.DAY;
/* eslint-disable @typescript-eslint/no-unused-vars */
export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
formData,
@@ -26,10 +38,25 @@ export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
) => {
const xAxisLabel = getXAxisLabel(formData);
if (formData.forecastEnabled && xAxisLabel) {
// The effective time grain can live in several places depending on how the
// chart was configured. Prefer, in order:
// 1. the grain popover on an adhoc x-axis column (generic x-axis),
// 2. the grain resolved onto the query object's extras (picks up
// dashboard-applied grains and the panel control),
// 3. the `time_grain_sqla` panel control on the form data directly.
// Fall back to a daily grain so a saved/dashboard chart with the grain
// cleared to "None" still forecasts instead of raising a backend error.
const xAxisColumn = getXAxisColumn(formData);
const timeGrain =
(isAdhocColumn(xAxisColumn) &&
(xAxisColumn.timeGrain as TimeGranularity)) ||
queryObject.extras?.time_grain_sqla ||
formData.time_grain_sqla ||
DEFAULT_TIME_GRAIN;
return {
operation: 'prophet',
options: {
time_grain: formData.time_grain_sqla,
time_grain: timeGrain,
periods: parseInt(formData.forecastPeriods, 10),
confidence_interval: parseFloat(formData.forecastInterval),
yearly_seasonality: formData.forecastSeasonalityYearly,

View File

@@ -43,6 +43,11 @@ const queryObject: QueryObject = {
granularity: 'P1Y',
};
// A chart whose Time Grain control was cleared to "None": form_data has no
// `time_grain_sqla` key at all (SC-113749).
const formDataWithoutGrain: SqlaFormData = { ...formData };
delete formDataWithoutGrain.time_grain_sqla;
test('should skip prophetOperator', () => {
expect(prophetOperator(formData, queryObject)).toEqual(undefined);
});
@@ -137,3 +142,103 @@ test('should do prophetOperator over adhoc column', () => {
},
});
});
test('should fall back to a daily grain when no time grain is resolvable', () => {
// Regression for SC-113749: a saved/dashboard chart with the Time Grain
// control cleared to "None" has no `time_grain_sqla` in form_data. Prior to
// the fix this emitted `time_grain: undefined`, which `JSON.stringify` drops,
// causing the backend `prophet()` call to raise a raw `TypeError`.
expect(
prophetOperator(
{
...formDataWithoutGrain,
granularity_sqla: 'time_column',
forecastEnabled: true,
forecastPeriods: '3',
forecastInterval: '5',
forecastSeasonalityYearly: true,
forecastSeasonalityWeekly: false,
forecastSeasonalityDaily: false,
},
{ ...queryObject, extras: {} },
),
).toEqual({
operation: 'prophet',
options: {
time_grain: 'P1D',
periods: 3.0,
confidence_interval: 5.0,
yearly_seasonality: true,
weekly_seasonality: false,
daily_seasonality: false,
index: DTTM_ALIAS,
},
});
});
test('should resolve the time grain from the adhoc x-axis column', () => {
// With the generic x-axis, the grain lives on the column's popover
// (`timeGrain`) rather than the `time_grain_sqla` panel control.
expect(
prophetOperator(
{
...formDataWithoutGrain,
x_axis: {
label: 'ds',
expressionType: 'SQL',
sqlExpression: 'ds',
timeGrain: 'P1M',
},
forecastEnabled: true,
forecastPeriods: '3',
forecastInterval: '5',
forecastSeasonalityYearly: true,
forecastSeasonalityWeekly: false,
forecastSeasonalityDaily: false,
},
{ ...queryObject, extras: {} },
),
).toEqual({
operation: 'prophet',
options: {
time_grain: 'P1M',
periods: 3.0,
confidence_interval: 5.0,
yearly_seasonality: true,
weekly_seasonality: false,
daily_seasonality: false,
index: 'ds',
},
});
});
test('should resolve the time grain from the query object extras', () => {
// Dashboard-applied grains (e.g. via native filters) land in
// `queryObject.extras.time_grain_sqla` even when form_data has none.
expect(
prophetOperator(
{
...formDataWithoutGrain,
granularity_sqla: 'time_column',
forecastEnabled: true,
forecastPeriods: '3',
forecastInterval: '5',
forecastSeasonalityYearly: true,
forecastSeasonalityWeekly: false,
forecastSeasonalityDaily: false,
},
{ ...queryObject, extras: { time_grain_sqla: 'P1W' } },
),
).toEqual({
operation: 'prophet',
options: {
time_grain: 'P1W',
periods: 3.0,
confidence_interval: 5.0,
yearly_seasonality: true,
weekly_seasonality: false,
daily_seasonality: false,
index: DTTM_ALIAS,
},
});
});

View File

@@ -31,8 +31,8 @@
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^4.0.0",
"ace-builds": "^1.44.0",
"ag-grid-community": "36.0.0",
"ag-grid-react": "36.0.0",
"ag-grid-community": "36.0.1",
"ag-grid-react": "36.0.1",
"brace": "^0.11.1",
"classnames": "^2.5.1",
"core-js": "^3.49.0",
@@ -101,7 +101,7 @@
"@types/react-window": "^1.8.8",
"@types/tinycolor2": "*",
"antd": "^6.0.0",
"nanoid": "^5.0.9",
"nanoid": "*",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render, waitFor, configure } from '@testing-library/react';
import { render, waitFor, configure, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import StatefulChart from './StatefulChart';
import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton';
@@ -67,19 +67,19 @@ beforeEach(() => {
jest.clearAllMocks();
// Setup default registry mocks
(getChartMetadataRegistry as any).mockReturnValue({
jest.mocked(getChartMetadataRegistry).mockReturnValue({
get: jest.fn().mockReturnValue({
useLegacyApi: false,
}),
});
} as unknown as ReturnType<typeof getChartMetadataRegistry>);
(getChartBuildQueryRegistry as any).mockReturnValue({
jest.mocked(getChartBuildQueryRegistry).mockReturnValue({
get: jest.fn().mockResolvedValue(null),
});
} as unknown as ReturnType<typeof getChartBuildQueryRegistry>);
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(null),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
// Mock ChartClient constructor
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
@@ -113,9 +113,9 @@ test('should refetch data when non-renderTrigger control changes', async () => {
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -165,9 +165,9 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender, getByTestId } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -201,9 +201,9 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
test('should refetch when control panel config is not available', async () => {
// No control panel config available
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(null),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -245,9 +245,9 @@ test('should refetch when viz_type changes', async () => {
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -299,9 +299,9 @@ test('should handle mixed renderTrigger and non-renderTrigger changes', async ()
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -352,9 +352,9 @@ test('should handle controls with complex structure', async () => {
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender, getByTestId } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -404,11 +404,11 @@ test('should not refetch when formData has not changed', async () => {
test('should handle errors gracefully when accessing registry', async () => {
// Mock registry to throw an error
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockImplementation(() => {
throw new Error('Registry error');
}),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -492,9 +492,9 @@ test('should NOT refetch data when string-based renderTrigger control (zoomable)
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const formDataWithZoom = {
...mockFormData,
@@ -542,9 +542,9 @@ test('should NOT refetch data when other string-based renderTrigger controls cha
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender, getByTestId } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -585,9 +585,9 @@ test('should refetch when string control is NOT in RENDER_TRIGGER_SHARED_CONTROL
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const { rerender } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
@@ -631,9 +631,9 @@ test('should handle mixed string and object controls correctly', async () => {
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const formDataWithControls = {
...mockFormData,
@@ -687,9 +687,9 @@ test('should refetch when mixing renderTrigger string control with non-renderTri
],
};
(getChartControlPanelRegistry as any).mockReturnValue({
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
});
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const formDataWithZoom = {
...mockFormData,
@@ -720,6 +720,435 @@ test('should refetch when mixing renderTrigger string control with non-renderTri
});
});
test('resolves async (202) responses via the injected handleAsyncChartData hook', async () => {
const asyncJob = {
channel_id: 'c1',
job_id: 'j1',
status: 'running',
result_url: '/api/v1/chart/data/abc',
};
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: asyncJob,
});
const handleAsyncChartData = jest
.fn()
.mockResolvedValue([{ data: 'async result' }]);
const { getByTestId } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Delegates the raw response + job metadata (and useLegacyApi + abort signal)
expect(handleAsyncChartData).toHaveBeenCalledWith(
{ status: 202 },
asyncJob,
false,
expect.any(AbortSignal),
);
// Chart renders once the async data resolves
await waitFor(() => {
expect(getByTestId('super-chart')).toBeInTheDocument();
});
});
test('errors on async (202) response when no async handler is provided', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j1', channel_id: 'c1', status: 'running' },
});
const onError = jest.fn();
const { findByText } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
onError={onError}
/>,
);
// Fails loudly instead of rendering the job metadata as empty data
expect(await findByText(/async handler/i)).toBeInTheDocument();
await waitFor(() => {
expect(onError).toHaveBeenCalledTimes(1);
});
});
test('renders synchronous (200) responses that include a response object', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 200 } as Response,
json: [{ result: [{ data: 'sync result' }] }],
});
const { getByTestId } = render(
<StatefulChart formData={mockFormData} chartType="test_chart" />,
);
await waitFor(() => {
expect(getByTestId('super-chart')).toBeInTheDocument();
});
// Synchronous path: no async handler needed, single request
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
});
test('wraps the legacy async body as { result: [body] } for the async handler', async () => {
const legacyBody = { job_id: 'j1', channel_id: 'c1', status: 'running' };
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: legacyBody,
});
// Force the legacy API path for this viz type
jest.mocked(getChartMetadataRegistry).mockReturnValue({
get: jest.fn().mockReturnValue({ useLegacyApi: true }),
} as unknown as ReturnType<typeof getChartMetadataRegistry>);
const handleAsyncChartData = jest
.fn()
.mockResolvedValue([{ data: 'legacy result' }]);
const { getByTestId } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Legacy body must be wrapped to match the V1 response signature
expect(handleAsyncChartData).toHaveBeenCalledWith(
{ status: 202 },
{ result: [legacyBody] },
true,
expect.any(AbortSignal),
);
await waitFor(() => {
expect(getByTestId('super-chart')).toBeInTheDocument();
});
});
test('does not apply a superseded async response over a newer one', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
let resolveFirst: (data: unknown) => void = () => {};
let resolveSecond: (data: unknown) => void = () => {};
const handleAsyncChartData = jest
.fn()
.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveFirst = resolve;
}),
)
.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveSecond = resolve;
}),
);
const onLoad = jest.fn();
const { rerender } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
onLoad={onLoad}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// A newer request supersedes the first (viz_type change forces a refetch)
const newFormData = { ...mockFormData, viz_type: 'different_chart' };
rerender(
<StatefulChart
formData={newFormData}
chartType="different_chart"
hooks={{ handleAsyncChartData }}
onLoad={onLoad}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
});
// Resolve the newer request first, then the stale one
await act(async () => {
resolveSecond([{ data: 'B' }]);
});
await act(async () => {
resolveFirst([{ data: 'A' }]);
});
await waitFor(() => {
expect(onLoad).toHaveBeenCalledWith([{ data: 'B' }]);
});
// The stale (superseded) response must not overwrite the newer one
expect(onLoad).not.toHaveBeenCalledWith([{ data: 'A' }]);
expect(onLoad).toHaveBeenCalledTimes(1);
});
test('preserves the detailed message from an async (array) rejection', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
const handleAsyncChartData = jest
.fn()
.mockRejectedValue([{ error: 'Async query failed: table not found' }]);
const onError = jest.fn();
const { findByText } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
onError={onError}
/>,
);
// The detailed message survives instead of collapsing to the generic one
expect(await findByText(/table not found/i)).toBeInTheDocument();
await waitFor(() => {
expect(onError).toHaveBeenCalledTimes(1);
expect(onError.mock.calls[0][0].message).toContain('table not found');
});
});
test('refetches with the latest formData rather than the initial props', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 200 } as Response,
json: [{ result: [{ data: 'x' }] }],
});
const { rerender } = render(
<StatefulChart
formData={{ ...mockFormData, metrics: ['metric_v1'] }}
chartType="test_chart"
/>,
);
await waitFor(() => {
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
});
// Change a data-affecting control -> triggers a refetch
rerender(
<StatefulChart
formData={{ ...mockFormData, metrics: ['metric_v2'] }}
chartType="test_chart"
/>,
);
await waitFor(() => {
expect(mockChartClient.client.post).toHaveBeenCalledTimes(2);
});
// The second request must carry the updated formData, not the initial props
const secondRequestConfig = mockChartClient.client.post.mock.calls[1][0];
expect(JSON.stringify(secondRequestConfig)).toContain('metric_v2');
expect(JSON.stringify(secondRequestConfig)).not.toContain('metric_v1');
});
test('does not revert a render-only change when a slow async request resolves', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
// color_scheme is a renderTrigger control -> its change does not refetch
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue({
controlPanelSections: [
{
controlSetRows: [
[{ name: 'color_scheme', config: { renderTrigger: true } }],
],
},
],
}),
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
let resolveAsync: (data: unknown) => void = () => {};
const handleAsyncChartData = jest.fn(
() =>
new Promise(resolve => {
resolveAsync = resolve;
}),
);
const { rerender, getByTestId } = render(
<StatefulChart
formData={{ ...mockFormData, color_scheme: 'scheme_one' }}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Render-only change while the async request is still pending (no refetch)
rerender(
<StatefulChart
formData={{ ...mockFormData, color_scheme: 'scheme_two' }}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
// The stale request resolves; it must not revert color_scheme back
await act(async () => {
resolveAsync([{ data: 'd' }]);
});
await waitFor(() => {
expect(getByTestId('super-chart')).toHaveTextContent('scheme_two');
});
expect(getByTestId('super-chart')).not.toHaveTextContent('scheme_one');
});
test('passes an abort signal to the async handler and aborts it on unmount', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
// Typed with a rest param so mock.calls is indexable (the 4th arg is the signal)
const handleAsyncChartData = jest.fn(
(..._args: unknown[]) => new Promise<never>(() => {}), // never resolves
);
const { unmount } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
const signal = handleAsyncChartData.mock.calls[0][3] as AbortSignal;
expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);
// Unmounting aborts the signal so a signal-aware handler can stop polling
unmount();
expect(signal.aborted).toBe(true);
});
test('suppresses stale error state from a superseded request', async () => {
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
let rejectFirst: (err: unknown) => void = () => {};
const handleAsyncChartData = jest
.fn()
.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectFirst = reject;
}),
)
.mockImplementationOnce(() => new Promise(() => {})); // newer request stays pending
const onError = jest.fn();
const { rerender } = render(
<StatefulChart
formData={mockFormData}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
onError={onError}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Supersede the first request (aborts its controller)
rerender(
<StatefulChart
formData={{ ...mockFormData, viz_type: 'different_chart' }}
chartType="different_chart"
hooks={{ handleAsyncChartData }}
onError={onError}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
});
// The stale request now fails; its error must not surface
await act(async () => {
rejectFirst(new Error('stale failure'));
});
expect(onError).not.toHaveBeenCalled();
});
test('does not publish stale data when switching from chartId to formData mode', async () => {
mockChartClient.loadFormData.mockResolvedValue({ ...mockFormData });
mockChartClient.client.post.mockResolvedValue({
response: { status: 202 } as Response,
json: { job_id: 'j', channel_id: 'c' },
});
let resolveFirst: (data: unknown) => void = () => {};
const handleAsyncChartData = jest
.fn()
.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveFirst = resolve;
}),
)
.mockImplementationOnce(() => new Promise(() => {}));
const onLoad = jest.fn();
// Start in chartId mode
const { rerender } = render(
<StatefulChart
chartId={1}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
onLoad={onLoad}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
});
// Switch to direct-formData mode
rerender(
<StatefulChart
formData={{ ...mockFormData, metrics: ['m'] }}
chartType="test_chart"
hooks={{ handleAsyncChartData }}
onLoad={onLoad}
/>,
);
await waitFor(() => {
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
});
// The stale chartId-mode request resolves; its data must not be published
await act(async () => {
resolveFirst([{ data: 'stale' }]);
});
expect(onLoad).not.toHaveBeenCalledWith([{ data: 'stale' }]);
});
test('should display error message when HTTP request fails with Response object', async () => {
const errorBody = JSON.stringify({ message: 'Error: division by zero' });
const mockResponse = new Response(errorBody, {

View File

@@ -18,17 +18,19 @@
*/
import { useState, useEffect, useRef, useCallback } from 'react';
import { isEqual } from 'lodash';
import { ParentSize } from '@visx/responsive';
import { t } from '@apache-superset/core/translation';
import {
QueryFormData,
QueryData,
JsonObject,
SupersetClientInterface,
buildQueryContext,
RequestConfig,
getClientErrorObject,
ensureIsArray,
} from '../..';
import type { HandlerFunction } from '../types/Base';
import { Loading } from '../../components/Loading';
import ChartClient from '../clients/ChartClient';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
@@ -189,6 +191,12 @@ export default function StatefulChart(props: StatefulChartProps) {
const chartClientRef = useRef<ChartClient>();
const abortControllerRef = useRef<AbortController>();
// fetchData is memoized with an empty dep list, so it would otherwise close
// over the first render's props. Keep the latest props in a ref so refetches
// (triggered by updated filters/formData/overrides) use current values.
const propsRef = useRef(props);
propsRef.current = props;
// Initialize chart client
if (!chartClientRef.current) {
chartClientRef.current = new ChartClient({ client: props.client });
@@ -199,20 +207,48 @@ export default function StatefulChart(props: StatefulChartProps) {
chartId,
formData: propsFormData,
formDataOverrides,
onError,
onLoad,
chartType,
force,
timeout,
} = props;
hooks,
} = propsRef.current;
// Cancel any in-flight requests
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller
abortControllerRef.current = new AbortController();
// Create new abort controller (kept in a local so we can detect when this
// request has been superseded by a newer one, even across async awaits).
const controller = new AbortController();
abortControllerRef.current = controller;
// A request is superseded if it was aborted, or if the props changed in a
// data-affecting way since it began - including switching between chartId
// and direct-formData modes. Props are captured during render but the abort
// happens in a passive effect, so the abort signal alone can let a stale
// success or error slip through in the render->effect gap. This mirrors the
// effect's own refetch decision; render-only changes are intentionally not
// treated as superseding.
const isSuperseded = () => {
if (controller.signal.aborted) {
return true;
}
const latest = propsRef.current;
const vizTypeForCompare = latest.formData?.viz_type || latest.chartType;
return (
latest.chartId !== chartId ||
// Deep compare overrides: callers commonly pass a fresh object with the
// same contents each render, which should not count as superseding.
!isEqual(latest.formDataOverrides, formDataOverrides) ||
latest.force !== force ||
Boolean(propsFormData) !== Boolean(latest.formData) ||
(!!propsFormData &&
!!latest.formData &&
latest.formData !== propsFormData &&
shouldRefetchData(propsFormData, latest.formData, vizTypeForCompare))
);
};
setStatus('loading');
setError(undefined);
@@ -224,7 +260,7 @@ export default function StatefulChart(props: StatefulChartProps) {
// Load formData from chartId
finalFormData = await chartClientRef.current!.loadFormData(
{ sliceId: chartId },
{ signal: abortControllerRef.current.signal } as RequestConfig,
{ signal: controller.signal } as RequestConfig,
);
} else if (propsFormData) {
// Use provided formData
@@ -267,7 +303,7 @@ export default function StatefulChart(props: StatefulChartProps) {
const requestConfig: RequestConfig = {
endpoint,
signal: abortControllerRef.current.signal,
signal: controller.signal,
...(timeout && { timeout: timeout * 1000 }),
};
@@ -285,39 +321,136 @@ export default function StatefulChart(props: StatefulChartProps) {
};
}
const response = await chartClientRef.current!.client.post(requestConfig);
let responseData = Array.isArray(response.json)
? response.json
: [response.json];
const clientResponse =
await chartClientRef.current!.client.post(requestConfig);
// Handle the nested result structure from the new API
if (!useLegacyApi && responseData[0]?.result) {
responseData = responseData[0].result;
}
setStatus('loaded');
setData(responseData);
setFormData(finalFormData);
if (onLoad) {
onLoad(responseData);
}
} catch (err) {
// Ignore abort errors
if ((err as Error).name === 'AbortError') {
// A newer request may have started while the POST was in flight; discard
// this stale response so it can't overwrite the newer chart data.
if (isSuperseded()) {
return;
}
const parsedError = await getClientErrorObject(
err as Parameters<typeof getClientErrorObject>[0],
);
const errorMessage =
parsedError.error || parsedError.message || 'An error occurred';
const rawResponse = clientResponse.response as Response | undefined;
let responseData: QueryData[];
if (rawResponse?.status === 202) {
// With GLOBAL_ASYNC_QUERIES the query is dispatched to a Celery worker
// and the 202 body is job metadata (channel_id, job_id, result_url),
// not chart data. Delegate to the injected handler, which polls the
// async event channel and resolves the cached results. Without a
// handler we fail loudly rather than rendering the job metadata as if
// it were an (empty) result set.
if (!hooks?.handleAsyncChartData) {
throw new Error(
'Received an async chart data response (HTTP 202) but no async ' +
'handler was provided, so results cannot be retrieved. Wire up ' +
'the async handler or disable GLOBAL_ASYNC_QUERIES for this chart.',
);
}
// The async handler (handleChartDataResponse) expects the V1 chart data
// response signature. The legacy endpoint returns a flat body, so wrap
// it as { result: [body] } exactly like legacyChartDataRequest does for
// the standard chart path; the V1 body is already correctly shaped.
const asyncPayload = useLegacyApi
? ({ result: [clientResponse.json] } as JsonObject)
: (clientResponse.json as JsonObject);
responseData = ensureIsArray(
await hooks.handleAsyncChartData(
rawResponse,
asyncPayload,
useLegacyApi,
controller.signal,
),
);
// Async results can resolve well after a newer request began polling.
if (isSuperseded()) {
return;
}
} else {
const rows = (
Array.isArray(clientResponse.json)
? clientResponse.json
: [clientResponse.json]
) as JsonObject[];
// Handle the nested result structure from the new API
responseData = (
!useLegacyApi && rows[0]?.result ? rows[0].result : rows
) as QueryData[];
}
// Don't pair this request's data with newer props or fire a stale onLoad
// if it has been superseded (see isSuperseded).
if (isSuperseded()) {
return;
}
const latestProps = propsRef.current;
setStatus('loaded');
setData(responseData);
// Render the resolved data with the latest formData so a render-only
// change made while the request was in flight isn't reverted.
setFormData(
latestProps.formData
? {
...latestProps.formData,
...latestProps.formDataOverrides,
viz_type: finalFormData.viz_type,
}
: finalFormData,
);
// Read onLoad from the latest props (like setFormData above) so a stale
// callback captured at request start isn't invoked.
if (latestProps.onLoad) {
latestProps.onLoad(responseData);
}
} catch (err) {
// Ignore aborted requests, whether they threw AbortError or were
// superseded by a newer request (including the render->effect gap).
if ((err as Error)?.name === 'AbortError' || isSuperseded()) {
return;
}
// waitForAsyncData rejects with an array of already-parsed client-error
// objects; unwrap the first element so its detailed message survives.
const rawError = Array.isArray(err) ? err[0] : err;
let errorMessage: string | undefined;
if (
rawError &&
typeof rawError === 'object' &&
!(rawError instanceof Error) &&
!(rawError instanceof Response) &&
typeof (rawError as { error?: unknown }).error === 'string'
) {
// Already a parsed client-error object (e.g. from the async handler);
// getClientErrorObject would discard its `error` field, so read it here.
const parsed = rawError as { error?: string; message?: string };
errorMessage = parsed.error || parsed.message;
} else {
const parsedError = await getClientErrorObject(
rawError as Parameters<typeof getClientErrorObject>[0],
);
errorMessage = parsedError.error || parsedError.message;
}
const errorObj = new Error(errorMessage || 'An error occurred');
// The request may have been superseded while its error response was being
// parsed above (or in the render->effect gap before its abort ran); don't
// set stale error state or call onError in that case.
if (isSuperseded()) {
return;
}
const errorObj = new Error(errorMessage);
setStatus('error');
setError(errorObj);
// Read onError from the latest props so a stale callback captured at
// request start isn't invoked.
const { onError } = propsRef.current;
if (onError) {
onError(errorObj);
}
@@ -481,7 +614,7 @@ export default function StatefulChart(props: StatefulChartProps) {
enableNoResults={enableNoResults}
noResults={NoDataComponent && <NoDataComponent />}
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
onRenderFailure={onRenderFailure}
hooks={hooks}
/>
);

View File

@@ -66,6 +66,18 @@ type Hooks = {
setTooltip?: HandlerFunction;
/* handle legend scroll changes */
onLegendScroll?: HandlerFunction;
/**
* Resolve an async chart-data response (HTTP 202 from GLOBAL_ASYNC_QUERIES).
* Injected by the app so components in this package (e.g. Matrixify's
* StatefulChart) can await async results without importing app-level
* async-event middleware. Returns the resolved query results.
*/
handleAsyncChartData?: (
response: Response,
json: JsonObject,
useLegacyApi?: boolean,
signal?: AbortSignal,
) => Promise<QueryData[]> | QueryData[];
} & PlainObject;
/**

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent, fireEvent } from '@superset-ui/core/spec';
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { Icons } from '@superset-ui/core/components/Icons';
import { ActionButton } from '.';
@@ -45,18 +45,6 @@ test('calls onClick when clicked', async () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
test('calls onClick when activated with the keyboard', () => {
const onClick = jest.fn();
render(<ActionButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
fireEvent.keyDown(button, { key: 'Enter' });
expect(onClick).toHaveBeenCalledTimes(1);
fireEvent.keyDown(button, { key: ' ' });
expect(onClick).toHaveBeenCalledTimes(2);
});
test('renders with tooltip when tooltip prop is provided', async () => {
const tooltipText = 'This is a tooltip';
render(<ActionButton {...defaultProps} tooltip={tooltipText} />);
@@ -115,6 +103,6 @@ test('has proper accessibility attributes', () => {
render(<ActionButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('tabIndex', '0');
expect(button).toHaveAttribute('role', 'button');
expect(button.tagName).toBe('BUTTON');
expect(button).toHaveAttribute('type', 'button');
});

View File

@@ -17,8 +17,8 @@
* under the License.
*/
import { handleKeyboardActivation } from '../../utils';
import type { ReactElement, ReactNode } from 'react';
import cx from 'classnames';
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
@@ -28,6 +28,9 @@ export interface ActionProps {
placement?: TooltipPlacement;
icon: ReactNode;
onClick: () => void;
className?: string;
disabled?: boolean;
dataTest?: string;
}
export const ActionButton = ({
@@ -36,30 +39,45 @@ export const ActionButton = ({
placement,
icon,
onClick,
className,
disabled = false,
dataTest,
}: ActionProps) => {
const theme = useTheme();
const actionButton = (
<span
role="button"
tabIndex={0}
<button
type="button"
aria-disabled={disabled}
aria-label={typeof tooltip === 'string' ? tooltip : label}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
font: inherit;
line-height: 1;
display: inline-flex;
align-items: center;
cursor: pointer;
color: ${theme.colorIcon};
margin-right: ${theme.sizeUnit}px;
&:hover {
&:not(.disabled):hover {
path {
fill: ${theme.colorPrimary};
}
}
&.disabled {
color: ${theme.colorTextDisabled};
cursor: not-allowed;
}
`}
className="action-button"
data-test={label}
onClick={onClick}
onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined}
className={cx('action-button', className, { disabled })}
data-test={dataTest ?? label}
onClick={disabled ? undefined : onClick}
>
{icon}
</span>
</button>
);
const tooltipId = `${label.replaceAll(' ', '-').toLowerCase()}-tooltip`;

View File

@@ -21,7 +21,11 @@ import type { ButtonGroupProps } from './types';
export function ButtonGroup(props: ButtonGroupProps) {
const { className, children } = props;
return (
// role="group" is the correct ARIA pattern for a generic button toolbar;
// the suggested native tags (fieldset, etc.) carry unrelated form
// semantics and unwanted default browser styling.
<div
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="group"
className={className}
css={{

View File

@@ -134,6 +134,10 @@ const ImageContainer = ({
? imageMap[image as keyof typeof imageMap]
: image;
return (
// Groups Empty's SVG illustration + description into one accessible
// image; can't be a literal <img> since it's a component tree, not an
// image file reference.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
<div role="img" aria-label="empty">
<Empty
description={false}

View File

@@ -25,8 +25,12 @@ import { Icons } from '@superset-ui/core/components/Icons';
import { Tooltip } from '../Tooltip';
import type { FaveStarProps } from './types';
const StyledLink = styled.a`
const StyledLink = styled.button`
${({ theme }) => css`
appearance: none;
border: none;
background: none;
font: inherit;
font-size: ${theme.fontSizeXL}px;
display: flex;
padding: 0 0 0 ${theme.sizeUnit * 2}px;
@@ -55,12 +59,10 @@ export const FaveStar = ({
const content = (
<StyledLink
href="#"
type="button"
onClick={onClick}
className="fave-unfave-icon"
data-test="fave-unfave-icon"
role="button"
tabIndex={0}
>
{isStarred ? (
<Icons.StarFilled

View File

@@ -102,6 +102,10 @@ export const LabeledErrorBoundInput = ({
</Tooltip>
)
}
// input[type=password] doesn't get an implicit "textbox" role
// (unlike other text inputs), so this explicit override is
// needed for it to be discoverable via role-based queries/AT.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="textbox"
/>
) : renderAsTextArea ? (

View File

@@ -79,8 +79,12 @@ const IconButton: React.FC<IconButtonProps> = ({
};
return (
// antd's Card renders a fixed <div> (no polymorphic tag support) with
// its own rich internal layout (cover/title/tooltip); that doesn't map
// onto a native <button>, so role="button" is used instead.
<Card
hoverable
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
aria-label={buttonText}

View File

@@ -99,6 +99,10 @@ export function Loading({
$spinnerHeight="auto"
$opacity={opacity}
className={cls('loading', position, className)}
// role="status" is the standard WAI-ARIA live-region pattern for a
// loading spinner; <output> (the suggested tag) is for form
// calculation results, not a fit here.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="status"
aria-live="polite"
aria-label={t('Loading')}

View File

@@ -89,6 +89,7 @@ const StyledItem = styled.div<{
& .metadata-text {
color: ${theme.colorTextSecondary};
min-width: ${TEXT_MIN_WIDTH}px;
max-width: ${TEXT_MAX_WIDTH}px;
overflow: hidden;
text-overflow: ${collapsed ? 'unset' : 'ellipsis'};
white-space: nowrap;

View File

@@ -84,7 +84,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
`
}
.ant-modal-content {
.ant-modal-container {
background-color: ${theme.colorBgContainer};
display: flex;
flex-direction: column;
@@ -196,7 +196,7 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
height: 100%;
}
.ant-modal-content {
.ant-modal-container {
height: 100%;
.ant-modal-body {

View File

@@ -127,10 +127,15 @@ export const ModalTrigger = forwardRef(
</Button>
)}
{!isButton && (
// Generic wrapper used by 19+ callers passing arbitrary
// triggerNode content that relies on block-level <div> layout;
// swapping to <button> risks a layout regression across callers
// with no shared CSS to guide a safe reset.
<div
data-test="span-modal-trigger"
onClick={open}
onKeyDown={handleKeyboardActivation(open)}
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
>

View File

@@ -103,7 +103,18 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
})),
}}
>
<div role="button" css={{ display: 'flex', alignItems: 'center' }}>
<button
type="button"
css={{
appearance: 'none',
border: 'none',
background: 'none',
padding: 0,
font: 'inherit',
display: 'flex',
alignItems: 'center',
}}
>
{selected && renderButton(selected)}
<Icons.DownOutlined
iconSize="s"
@@ -112,7 +123,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
marginLeft: theme.sizeUnit * 0.5,
}}
/>
</div>
</button>
</Dropdown>
);
};

View File

@@ -22,7 +22,7 @@ import PopoverSection from '.';
test('renders with default props', async () => {
render(
<PopoverSection title="Title">
<div role="form" />
<form aria-label="test-form" />
</PopoverSection>,
);
expect(await screen.findByRole('form')).toBeInTheDocument();
@@ -32,7 +32,7 @@ test('renders with default props', async () => {
test('renders tooltip icon', async () => {
render(
<PopoverSection title="Title" info="Tooltip">
<div role="form" />
<form />
</PopoverSection>,
);
expect((await screen.findAllByRole('img')).length).toBe(2);
@@ -41,7 +41,7 @@ test('renders tooltip icon', async () => {
test('renders a tooltip when hovered', async () => {
render(
<PopoverSection title="Title" info="Tooltip">
<div role="form" />
<form />
</PopoverSection>,
);
await userEvent.hover(screen.getAllByRole('img')[0]);
@@ -52,7 +52,7 @@ test('calls onSelect when clicked', async () => {
const onSelect = jest.fn();
render(
<PopoverSection title="Title" onSelect={onSelect}>
<div role="form" />
<form />
</PopoverSection>,
);
await userEvent.click(await screen.findByRole('img'));

View File

@@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '../../utils';
import { ReactNode, SyntheticEvent } from 'react';
import { MouseEventHandler, ReactNode } from 'react';
import { css, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { Tooltip } from '../Tooltip';
@@ -25,10 +24,7 @@ import { Tooltip } from '../Tooltip';
export interface PopoverSectionProps {
title: string;
isSelected?: boolean;
// `SyntheticEvent` (rather than `MouseEventHandler`) so the same callback
// can be reused as the keyboard-activation handler via
// `handleKeyboardActivation`, which invokes it with a `KeyboardEvent`.
onSelect?: (event: SyntheticEvent) => void;
onSelect?: MouseEventHandler<HTMLButtonElement>;
info?: string;
children?: ReactNode;
}
@@ -48,12 +44,17 @@ export default function PopoverSection({
opacity: isSelected ? 1 : 0.6,
}}
>
<div
role="button"
tabIndex={0}
<button
type="button"
onClick={onSelect}
onKeyDown={onSelect ? handleKeyboardActivation(onSelect) : undefined}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
text-align: left;
width: 100%;
display: flex;
align-items: center;
cursor: ${onSelect ? 'pointer' : 'default'};
@@ -68,8 +69,9 @@ export default function PopoverSection({
margin-right: ${theme.sizeUnit}px;
`}
>
{/* role is auto-computed by BaseIconComponent as "img" since
there's no onClick, so no explicit role needed here. */}
<Icons.InfoCircleOutlined
role="img"
iconSize="s"
iconColor={theme.colorIcon}
/>
@@ -77,10 +79,9 @@ export default function PopoverSection({
)}
<Icons.CheckOutlined
iconSize="s"
role="img"
iconColor={isSelected ? theme.colorPrimary : theme.colorIcon}
/>
</div>
</button>
<div
css={css`
margin-left: ${theme.sizeUnit}px;

View File

@@ -60,18 +60,26 @@ test('should render with success icon', () => {
expect(screen.getByLabelText('check-circle')).toBeInTheDocument();
});
// The stripes are painted on antd's inner progress track, so assert the
// computed style of that element — asserting the outer container would pass
// whether or not the gradient ever reached the track.
const getTrack = (container: HTMLElement) =>
container.querySelector('.ant-progress-track') as HTMLElement;
test('should render with stripes', () => {
const stripedProps = {
...mockedProps,
striped: true,
};
const { container } = render(<ProgressBar {...stripedProps} />);
expect(container).toHaveStyle(
`background-image: 'linear-gradient(
45deg,rgba(255, 255, 255, 0.15) 25%,
transparent 25%, transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%, transparent) !important'`,
expect(getComputedStyle(getTrack(container)).backgroundImage).toContain(
'linear-gradient(45deg',
);
});
test('should render without stripes by default', () => {
const { container } = render(<ProgressBar {...mockedProps} />);
expect(getComputedStyle(getTrack(container)).backgroundImage).not.toContain(
'linear-gradient',
);
});

View File

@@ -29,10 +29,10 @@ const ProgressBar = styled(({ striped, ...props }: ProgressBarProps) => (
<AntdProgress data-test="progress-bar" {...props} />
))`
position: static;
.ant-progress-inner {
.ant-progress-rail {
position: static;
}
.ant-progress-bg {
.ant-progress-track {
position: static;
${({ striped }) =>
striped &&

View File

@@ -35,7 +35,8 @@ const RefreshLabel = ({
<Tooltip title={tooltipContent}>
<Icons.SyncOutlined
iconSize="l"
role="button"
// role is auto-computed by BaseIconComponent as "button" whenever
// onClick is present (and "img" otherwise), so no explicit role here.
tabIndex={disabled ? -1 : 0}
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({

View File

@@ -0,0 +1,121 @@
/**
* 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 { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { RlsBadge, type RlsBadgeProps } from '.';
const mockFilters = [
{ id: 1, name: 'Finance filter', filter_type: 'Regular', group_key: 'dept' },
{ id: 2, name: 'Base restriction', filter_type: 'Base', group_key: null },
];
const asyncRender = (props: RlsBadgeProps) =>
waitFor(() => render(<RlsBadge {...props} />));
test('renders nothing when rlsFilters is empty', async () => {
const { container } = await asyncRender({ rlsFilters: [] });
expect(container.firstChild).toBeNull();
});
test('renders a lock icon when filters exist', async () => {
await asyncRender({ rlsFilters: mockFilters });
expect(
screen.getByRole('img', { name: /row-level security/i }),
).toBeInTheDocument();
});
test('renders a tooltip with filter details when hovered', async () => {
await asyncRender({ rlsFilters: mockFilters });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('Row-Level Security');
expect(tooltip).toHaveTextContent('Finance filter');
expect(tooltip).toHaveTextContent('Base restriction');
});
test('shows filter type in tooltip', async () => {
await asyncRender({ rlsFilters: mockFilters });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('Regular');
expect(tooltip).toHaveTextContent('Base');
});
test('shows group key in tooltip when present', async () => {
await asyncRender({ rlsFilters: mockFilters });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('[dept]');
});
test('shows inherited suffix and summary note for inherited filters', async () => {
const inheritedFilters = [
{
id: 10,
name: 'Inherited filter',
filter_type: 'Regular',
group_key: null,
inherited: true,
},
];
await asyncRender({ rlsFilters: inheritedFilters });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('from underlying table');
expect(tooltip).toHaveTextContent(
'Some filters are inherited from physical tables',
);
});
test('shows roles when present', async () => {
const filtersWithRoles = [
{
id: 20,
name: 'Role filter',
filter_type: 'Regular',
group_key: null,
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'Analyst' },
],
},
];
await asyncRender({ rlsFilters: filtersWithRoles });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('Roles');
expect(tooltip).toHaveTextContent('Admin');
expect(tooltip).toHaveTextContent('Analyst');
});
test('shows clause when present', async () => {
const filtersWithClause = [
{
id: 30,
name: 'Clause filter',
filter_type: 'Base',
group_key: null,
clause: "dept = 'Finance'",
},
];
await asyncRender({ rlsFilters: filtersWithClause });
await userEvent.hover(screen.getByRole('img'));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('Clause');
expect(tooltip).toHaveTextContent("dept = 'Finance'");
});

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.
*/
import { t } from '@apache-superset/core/translation';
import { useTheme, css } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { Tooltip } from '../Tooltip';
import type { IconType } from '@superset-ui/core/components/Icons/types';
export interface RlsFilterSummary {
id: number;
name: string;
filter_type?: string;
group_key?: string | null;
inherited?: boolean;
clause?: string;
roles?: Array<{ id: number; name: string }>;
}
export interface RlsBadgeProps {
rlsFilters: RlsFilterSummary[];
size?: IconType['iconSize'];
}
export function RlsBadge({ rlsFilters, size = 'l' }: RlsBadgeProps) {
const theme = useTheme();
if (!rlsFilters?.length) {
return null;
}
const hasInherited = rlsFilters.some(f => f.inherited);
const tooltipContent = (
<div>
<strong>
{t(
'Row-Level Security: %d filter(s) may restrict data based on your role.',
rlsFilters.length,
)}
</strong>
<ul
css={css`
padding-left: ${theme.sizeUnit * 4}px;
margin: ${theme.sizeUnit}px 0 0;
`}
>
{rlsFilters.map(filter => (
<li key={filter.id}>
<div>
{filter.name}
{filter.filter_type ? ` (${filter.filter_type})` : ''}
{filter.group_key ? ` [${filter.group_key}]` : ''}
{filter.inherited ? `${t('from underlying table')}` : ''}
</div>
{filter.roles && filter.roles.length > 0 && (
<div>
{t('Roles')}: {filter.roles.map(role => role.name).join(', ')}
</div>
)}
{filter.clause && (
<div>
{t('Clause')}: {filter.clause}
</div>
)}
</li>
))}
</ul>
{hasInherited && (
<div
css={css`
margin-top: ${theme.sizeUnit}px;
font-style: italic;
`}
>
{t(
'Some filters are inherited from physical tables referenced in this virtual dataset.',
)}
</div>
)}
</div>
);
return (
<Tooltip id="rls-badge-tooltip" title={tooltipContent}>
<Icons.LockOutlined
aria-label={t('Row-Level Security')}
iconColor={theme.colorWarning}
iconSize={size}
/>
</Tooltip>
);
}

View File

@@ -870,14 +870,14 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
test('does not fire onChange when searching but no selection', async () => {
const onChange = jest.fn();
render(
<div role="main">
<main>
<AsyncSelect
{...defaultProps}
onChange={onChange}
mode="multiple"
allowNewOptions
/>
</div>,
</main>,
);
await open();
await type('Joh');

View File

@@ -857,7 +857,7 @@ const AsyncSelect = forwardRef(
getPopupContainer={
getPopupContainer ||
((triggerNode: HTMLElement) =>
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
(triggerNode?.closest('.ant-modal-container') as HTMLElement) ||
(triggerNode.parentNode as HTMLElement))
}
headerPosition={headerPosition}

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