Compare commits

..
Author SHA1 Message Date
Claude Code fa52a38be9 Merge origin/master into misc-charts-examples 2026-08-14 18:01:35 -07:00
Claude Code 0ef7482713 fix(examples): fix broken histogram_v2 and deck_geojson thumbnails
Life Expectancy Histogram had the same bug as the earlier Bubble chart
fix: its adhoc filter pointed at 2014, which has zero SP_DYN_LE00_IN data
in the world_health dataset, so the thumbnail showed an empty chart.
Moved it to 2011, which has real data.

Deck.gl GeoJson rendered as a single solid-colored blob: the global
fill_color_picker (alpha=1) always overrides any per-feature fill color,
and line_width defaulted to ~1 meter, invisible at city zoom, so none of
the 25 zip-code boundaries were visible. Baked a population-ranked
choropleth fill-color into each zip code's GeoJSON Feature, set
fill_color_picker's alpha to 0 so the per-feature colors show through,
switched the stroke to a visible white pixel-width line, and turned off
the extruded flag since there's no per-feature elevation data backing it.
2026-08-14 17:51:56 -07:00
21ae918656 fix(mcp): validate chart queries before persistence (#43128)
Co-authored-by: Bexultan Mustafin <bexultan.mustafin@ffins.kz>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-14 17:48:19 -07:00
Joe Li ca94026e97 fix(sqllab): async queries no longer stuck at Running forever (#42896) 2026-08-14 17:47:10 -07:00
Evan RusackasandClaude Opus 4.8 a3bc2d908c ci(frontend): track bundle size over time with benchmark-action (#42511)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 17:40:46 -07:00
738d12677a fix(i18n-es): correct stranded translations in API key and semantic layer catalogs (#43080)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-14 17:40:23 -07:00
Claude Code 6828e17516 feat(examples): add a horizontally-oriented tree chart example
Sales Territory Tree Horizontal reuses the same sales_territory_hierarchy
dataset as the existing radial Sales Territory Tree example, but with
layout: orthogonal / orient: LR so the gallery shows tree_chart's other
common orientation instead of two renders of the same radial chart.
2026-08-14 16:33:41 -07:00
Claude Code d00bde7b8e Merge origin/master into misc-charts-examples
# Conflicts:
#	superset/examples/world_health/charts/Life_Expectancy_VS_Rural.yaml
2026-08-14 15:57:05 -07:00
Claude Code fb5029d06d feat(examples): capture every chart on every example dashboard, replacing hand-made art with live thumbnails
The capture tool discovered ~53 viz types across the example dashboards
but only ever captured one representative chart per type into
thumbnail.png; every gallery slot beyond that was hand-made art that
predated any of this, unrelated to what the example dashboards actually
contain. Two of those hand-made images turned out to be faked outright:
ag-grid-table's and cartodiagram's "dark" variants were a naive full-image
color inversion of the light one rather than a real dark-theme render
(confirmed by pixel-diffing every light/dark pair in the repo -- these
two matched a naive invert at 85-99%, every genuine capture matched at
under 2%).

To fix this without silently losing the old art or shipping anything
dishonest:

1. Every existing thumbnail/gallery image was renamed with a
   custom_thumb_ prefix (plugin imports updated to match), so it's
   preserved and clearly labeled as hand-made rather than captured.
2. The capture script (capture-viz-thumbnails.spec.ts) was generalized:
   it now captures every distinct chart per viz type, not just one --
   the preferred (or alphabetically-first) chart becomes the picker
   thumbnail, and every other distinct chart fills the next
   already-declared exampleGallery slot, via a new VIZ_TYPE_GALLERY map
   mirroring VIZ_TYPE_THUMBNAILS. Also fixed a pre-existing bug where
   VIZ_TYPE_THUMBNAILS.rose pointed at plugin-chart-rose, a package
   consolidated into plugin-chart-echarts/src/Rose long ago.
3. Ran it for real against a live instance with examples loaded
   (including the new Deck.gl GeoJson example). Every plugin whose
   real capture succeeded had its import flipped from the custom_thumb_
   file back to the plain, freshly-captured one.
4. For deck.gl chart types, the dark "capture" is confirmed genuinely
   pixel-identical to light (they render static map tiles that don't
   respond to prefers-color-scheme) rather than skipped/left stale, so
   the plain dark file is an honest copy of the real light capture
   instead of mismatched old art.

Four viz types still have zero chart on any example dashboard and keep
their custom_thumb_ imports as an honest label rather than a graduated
(but fake) real capture:
- cartodiagram: needs a hand-crafted embedded sub-chart config
- deck_multi: sub-layers are referenced by raw DB-assigned chart ids
  with no UUID-based remap path through the YAML importer -- a real
  gap needing an importer/exporter fix, not something a fixture alone
  can solve
- pop_kpi, ag-grid-table: gated behind ChartPluginsExperimental /
  AgGridTableEnabled and unregistered by default, so a default-visible
  dashboard chart would just show every regular user a broken card
2026-08-14 15:10:59 -07:00
Claude Code 041d735ddb feat(examples): add a Deck.gl GeoJson example chart
deck_geojson had zero chart on any example dashboard, needing an example
dataset with a serialized GeoJSON Feature column -- something no
existing example data had. Derived a `geojson` column for
sf_population_polygons (already used by the deck.gl demo dashboard for
deck_polygon/deck_contour) from its existing `contour` point-list
column, wrapping each zip code's boundary as a proper GeoJSON Feature.
Added the chart to Misc Charts and to the dashboard's cross-filter
scope list.

Also carries the world_health bubble_v2 migration + time_range fix
already sent separately to master (#43152); duplicated here so this
branch's own capture tooling and CI have working data independent of
that PR's merge timing.
2026-08-14 15:05:11 -07:00
Claude Code 49a4086715 fix(charts): fix Bubble chart axis-interval crash
xAxis.interval was set directly from the xAxisLabelInterval control
('auto'/'0'), but xAxis.interval forces echarts' IntervalScale into a
fixed-tick-spacing mode that expects a number and crashes the axis
"nice" tick calculation. Every bubble_v2 chart hit this at its default
control value; there was no existing test coverage for this file.

The interval belongs nested under axisLabel, where it only controls how
many labels are skipped, matching how every other echarts chart in this
codebase (e.g. Timeseries) wires it. Added a regression test.

Found while building out live thumbnail capture for example dashboards
(this PR) -- bubble_v2 apparently never had a real dashboard-placed
chart to exercise this path before.
2026-08-14 15:03:46 -07:00
Elizabeth ThompsonandClaude Opus 4.8 2eedc609a8 fix(sql_lab): return 400 not 500 when raise_for_access hits malformed Jinja in results.py (#43145)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 15:02:47 -07:00
JUST.in DO IT f4587218dd fix(mcp): populate user_id in MCP audit logs (#42767) 2026-08-14 16:28:01 -04:00
Amin Ghadersohi 8967e6c2d3 fix(mcp): accept changed_on_delta_humanized as order_column in list tools (#42571) 2026-08-14 16:11:46 -04:00
Amin Ghadersohi 81e431cd50 fix(mcp): return all chart query results (#42824) 2026-08-14 16:10:26 -04:00
Amin Ghadersohi edfb009e1c fix(mcp): expose query context to Jinja macros (#42822) 2026-08-14 16:10:03 -04:00
Amin Ghadersohi e808fcbcad fix(mcp): bind all generated charts to dashboard time filters (#42490) 2026-08-14 16:07:50 -04:00
Evan RusackasandClaude Code 0a7ebe1dd1 fix(ci): stop superseded Docs Deployment runs from showing as cancelled (#42488)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-14 12:14:08 -07:00
Evan RusackasandClaude Opus 4.8 dd1afb029f fix(ci): stop py311/py312 docker builds from silently matching lean (#42509)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:07:23 -07:00
joeyandAmin Ghadersohi c068a8c09c fix(dashboard): preserve certification fields when Certification panel is closed (#42957)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-14 11:19:39 -07:00
Evan RusackasandClaude Code b62ec512d2 fix(explore): hide edit-properties menu item for non-owner/non-editor users (#38884) (#42737)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-14 11:15:10 -07:00
1e65d93a83 fix(sql): handle ORDER BY in embedded MSSQL queries (#43127)
Co-authored-by: Bexultan Mustafin <bexultan.mustafin@ffins.kz>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-14 14:07:10 -04:00
Gabriel Torres Ruiz 856599027a fix(mcp): harden the embedded-guest chart data-read path 2026-08-14 14:55:11 -03:00
Evan RusackasandClaude Code c395b9a238 fix(examples): migrate Life Expectancy VS Rural % example off the removed legacy bubble viz_type (#43152)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-14 10:27:14 -07:00
92728169de docs: document the zstd prerequisite for the frontend dev server (#43109)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-14 20:41:13 +07:00
DanielSwift1992 84c371d56e chore: fix stale developer_portal paths (#43143) 2026-08-14 20:12:08 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a4c47359e6 chore(deps): bump the storybook group in /docs with 2 updates (#43154)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:25 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7c0c5283c3 chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#43155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:20 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9c5bde9491 chore(deps): bump mapbox-gl from 3.28.0 to 3.28.1 in /superset-frontend (#43156)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:16 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3b99e092d0 chore(deps-dev): bump tsx from 4.23.9 to 4.23.10 in /superset-frontend (#43157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:13 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> acf39e3ef0 chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43158)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:10 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8523ea4d0a chore(deps): bump caniuse-lite from 1.0.30001806 to 1.0.30001807 in /docs (#43159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:06 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9bc3173e3a chore(deps): bump antd from 6.5.3 to 6.5.4 in /docs (#43160)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:03 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a6db0d1cde chore(deps): bump query-string from 9.4.1 to 9.5.0 in /superset-frontend (#43161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:34:00 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aec567f7d6 chore(deps): bump antd from 6.5.3 to 6.5.4 in /superset-frontend (#43162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 02:33:56 -07:00
Evan RusackasandClaude Code bacaf08a22 docs(perf): add Dashboard Performance guide covering virtualization, lazy tabs, and chart-count guidance (#40238)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-13 23:41:00 -07:00
a188e9473a feat(chart): let Drill By scope to the clicked x-axis value (#42296)
Co-authored-by: Jacob Hartmann (BUVM-STIL) <jacob.hartmann@stil.dk>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 17:02:27 -07:00
rusackas 31218987ec Merge remote-tracking branch 'origin/master' into pr42328-local 2026-08-13 14:05:18 -07:00
537f0fd2db fix(sqllab): use dialect-specific quote chars for autocomplete identifiers (#41492)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 14:04:38 -07:00
Joe Li 8f78f9eb9c chore: update CODEOWNERS for extension and config files (#43142) 2026-08-13 13:33:49 -07:00
Joe Li 930ba64fdf fix(ci): format ErrorMessage tests (#43114) 2026-08-13 13:19:08 -07:00
3fb58900b9 chore(duckdb): remove cursor.description workaround, no longer reproducible (#43101)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:16:16 -07:00
a63483b9b1 fix(sql): rebase Dremio dialect on sqlglot's native dialect (#43099)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:16:06 -07:00
Viktor HögbergandJoe Li 01d0772d4f fix(box-plot): distribute across field not marked as required for datasets without a temporal column (#43087)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:15:51 -07:00
3c7633f935 fix(encrypt): stop naive padding from truncating secrets ending in '*' (#43074)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:15:37 -07:00
Evan RusackasandClaude Code f7a2f0ec50 feat(table): allow choosing Sum or Average for the "Show summary" totals row (#43027)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-13 13:15:23 -07:00
dc9547554b fix(echarts): normalize BigInt metric values before summing/dividing in stacked charts (#36401) (#42594)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:14:33 -07:00
Evan RusackasandClaude Opus 4.8 67bbe0ac17 ci: speed up backend CI with astral-sh/setup-uv and apt package caching (#42498)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:14:24 -07:00
6211f9936b feat(mobile): Add mobile-friendly dashboard consumption mode (#37141)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 13:14:11 -07:00
Bernedotcom2312andClaude Opus 5 5b28158519 fix(helm): stop shipping conflicting podDisruptionBudget defaults (#42995)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 13:02:51 -07:00
Amin Ghadersohi 117b92a517 fix(mcp): expose table column formatting (#42658) 2026-08-13 15:21:44 -04:00
Evan RusackasandClaude e1468a709c fix(sunburst): keep SQL null and literal string "null" as distinct groups (#43013)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-13 11:07:06 -07:00
Evan RusackasandClaude Code c8cb09d458 fix(dev): stop the webpack dev-proxy from hanging on a mid-stream backend disconnect (#42811)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-13 11:06:30 -07:00
ec136f6d8d fix(charts): decode CSV bytes payload before feeding pandas StringIO (#32370) (#42735)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 11:06:09 -07:00
d0658bacc8 fix(mcp): give concurrent tool calls isolated db sessions (#42629)
Co-authored-by: goingforstudying-ctrl <goingforstudying-ctrl@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-13 11:05:40 -07:00
ʈᵃᵢ b8ca729f9f fix(sql): only force a LIMIT onto query expressions (#43097) 2026-08-13 10:43:45 -07:00
rusackasandClaude Opus 4.8 de4def38cb fix: drop redundant alembic merge migration (rebase cruft)
aaed1198e21f duplicated master's own resolution (d7cecc48bd55) of the
same two heads (4f145192b583, c4a1b8e2d739); nothing in this branch's
chain depends on it, so it can be dropped outright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 10:36:25 -07:00
Evan RusackasandClaude Code 25c2ca1127 ci: run pre-commit checks via prek (#42500)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-13 10:28:11 -07:00
Evan RusackasandClaude Code d5ae93c9b8 test(echarts): pin Bar chart X Axis Title flows through untouched (#42560) (#42599)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-13 10:27:32 -07:00
Evan RusackasandClaude fbeba10f75 fix(sqllab): make MenuDotsDropdown trigger focusable for tab rename (#43015)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-13 10:26:59 -07:00
Evan RusackasandClaude Opus 4.8 c8305b0ba9 feat(ci): publish Python unit test results as PR check annotations (#42503)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 10:26:42 -07:00
Evan RusackasandClaude Opus 4.8 e1777737a7 fix(docs): sync live component demos to the docs site's dark mode toggle (#42602)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 10:15:54 -07:00
Amin Ghadersohi 762fdccfde fix(oauth2): log database token failures (#42644) 2026-08-13 12:54:55 -04:00
Amin GhadersohiandClaude 5554b892ff fix(mcp): preserve table chart state during updates (#42655)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-13 12:54:05 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e2bb33b1da chore(deps): bump the ag-grid group across 1 directory with 2 updates (#43103)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 06:50:19 -07:00
Evan RusackasandClaude Opus 4.8 59361581cd fix(embedded): restore chart rendering for guest dashboards (#43095)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 05:38:57 -07:00
Evan RusackasandClaude Sonnet 5 08dfca9631 fix(ci): enforce a single Alembic migration head (#42890)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 05:38:45 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 276d6f04f5 chore(deps): bump actions/setup-java from 5.6.0 to 5.7.0 (#43102)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 02:33:25 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 524d99159a chore(deps): bump immer from 11.1.15 to 11.1.16 in /superset-frontend (#43104)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 02:33:21 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 657a2a4cb2 chore(deps): bump mapbox-gl from 3.27.0 to 3.28.0 in /superset-frontend (#43105)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 02:33:17 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> eefb3e3835 chore(deps-dev): bump tsx from 4.23.7 to 4.23.9 in /superset-frontend (#43106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 02:33:13 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 07accd56f2 chore(deps): bump core-js from 3.49.0 to 3.50.0 in /superset-frontend (#43107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 02:33:09 -07:00
Evan RusackasandClaude Opus 4.8 22caf221b7 fix(echarts): truncate values at Y axis bounds instead of dropping them (#42300)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 02:30:57 -07:00
Evan RusackasandClaude Fable 5 de2276225a test(playwright): pin drill-to-detail modal table height (#42406)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 22:48:49 -07:00
endimonan 685f26b1bf fix(cache): warm native dashboard filter defaults (#43073) 2026-08-12 22:30:48 -07:00
Anupam MedirattaandClaude Sonnet 4.6 739365979d fix: upgrade brace-expansion to 5.0.8 (CVE-2026-14257) (#42435)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-12 22:23:33 -07:00
bc1a8e0858 refactor(tags): align ExportTagsCommand with ExportModelsCommand (#42339)
Co-authored-by: Prathamesh Hukkeri <prathamesh04@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-12 21:44:53 -07:00
Evan RusackasandClaude Opus 4.8 0915a39bcb fix(dataset): improve dataset, report, and expression validation (#42929)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 20:13:45 -07:00
8014f782d3 feat: bump SQLAlchemy to 2.0 and flask-sqlalchemy to 3.1.1 (#42803)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-08-12 19:55:14 -07:00
endimonanandEvan Rusackas eb7d4cba42 fix(explore): hide Superset annotation source for users without annotation access (#43006)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-12 18:51:39 -07:00
Evan RusackasandClaude Opus 4.8 01ecefd732 fix(security_manager): stop SupersetAuthView from shadowing AUTH_REMOTE_USER (#42949)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 18:09:46 -07:00
Elizabeth Thompson 0c2f91968e fix: commit migration write in get_shared_value to stop recurring md5 deprecation warning (#42916) 2026-08-12 15:02:34 -07:00
Evan RusackasandClaude Code ed696b9933 test(sql): pin optimizer hint blocks survive format() round-trip (#38189) (#42733)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-12 14:49:55 -07:00
Joe Li 4baf1cf648 fix(reports): preserve Slack v1 private-channel text delivery (#42089) 2026-08-12 14:42:57 -07:00
Rafael BenitezandClaude Opus 4.8 aefee48223 fix(GridTable): remove unused rowSelection option to silence AG Grid error #200 (#43078)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 23:07:40 +02:00
Evan RusackasandClaude Fable 5 cc35056bc9 test(core): pin dashboard save-error toast mapping for non-JSON 403 responses (#42250)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 11:49:11 -07:00
Evan RusackasandClaude Code a0d7ec9faf fix(select): permission label search matches displayed label (#42041) (#42592)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-12 08:58:22 -07:00
onheapandkeyao_yang a501fed560 fix(rls): handle same-named CTEs and quoted aliases in the SQL rewrite (#43005)
Co-authored-by: keyao_yang <keyao.yang@airbnb.com>
2026-08-12 10:38:36 -03:00
Evan RusackasandClaude Code 4354b37b96 fix(sql-lab): apply SQL_QUERY_MUTATOR to streaming exports (#40465) (#42739)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-12 05:56:01 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d840568f3b chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /superset-frontend (#43082)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-12 02:56:56 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 174d35380d chore(deps-dev): bump tsx from 4.23.5 to 4.23.7 in /superset-frontend (#43083)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-12 02:56:52 -07:00
Evan RusackasandClaude Sonnet 5 2c10e6260f fix(explore): pin and fix clipped segments in horizontal row-contribution stacked bar charts (#42610)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 02:42:15 -07:00
Evan RusackasandClaude Code 8f6587d0e6 fix(caching): sort extra_cache_keys before hashing (#34543) (#42597)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-11 23:34:01 -07:00
Evan RusackasandClaude Code b4f3fae288 fix(sql): guard FORCE_LIMIT against SHOW statements (#36939) (#42588)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-11 23:33:15 -07:00
Evan RusackasandClaude Code 8e455034d0 chore(codeowners): trim stale ownership blocks, reword maps notice (#43079)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-11 23:29:46 -07:00
8734a232d8 fix(reports): apply chart number and currency formatting to tables sent as text (#42820)
Co-authored-by: Jean Massucatto <massucattoj@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 21:48:37 -07:00
Amin Ghadersohi 56573fa2cd feat(mcp): filter asset lists by certification (#42656) 2026-08-12 00:12:38 -04:00
Evan RusackasandClaude Code 885f00130c fix(sqla): drop stale main_dttm_col from dttm_cols when non-temporal (#30510) (#41964)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-11 19:38:53 -07:00
Evan RusackasandClaude Opus 4.8 584466e02b fix(sql-lab): improved SQL statement parsing and validation (#42928)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 19:35:43 -07:00
d570335f67 fix: bind permission-sync task to user id, use per-user RLS cache sentinel on parse failure (#42938)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 19:35:40 -07:00
Evan RusackasandClaude Opus 4.8 a0099af88f fix(deck.gl): dismiss custom tooltips on hover-out (#43075)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 19:32:43 -07:00
Claude Code 4f77538bc2 feat(examples): add a Handlebars CSS showcase tab to the misc_charts dashboard
Addresses rusackas's leftover note on #42328: "The handlebars showcase tab
idea (keep the logo, add a Misc tab of CSS-heavy handlebars examples)."

- Kept the existing Sales Handlebars List example (the Superset logo demo)
  untouched, per "keep the logo".
- Added a new "Sales Handlebars Showcase" chart: a product-line leaderboard
  rendered as cards, using the plugin's actual CSS-heavy capabilities --
  the dedicated "CSS Styles" field for card/badge/gradient-bar styling, the
  registered `formatNumber` helper, and just-handlebars-helpers'
  conditional (`gt`, `eq`) and math (`sum`, `division`, `multiplication`)
  helpers for rank medals (top 3), tier badges (Top Seller/Strong/Growing),
  and proportional progress bars. The card markup, rank, and badge text
  are plain text/emoji and always render; the colors/shape/bars come from
  the CSS field and require an administrator to allow `style` elements via
  HTML_SANITIZATION_SCHEMA_EXTENSIONS (server-side sanitization strips
  `<style>` by default) -- same caveat the control's own tooltip warns
  about, documented in the chart's description.
- Restructured misc_charts/dashboard.yaml from a flat grid into two tabs
  ("Charts" holding all existing content unchanged, "Handlebars Showcase"
  holding the new chart), matching the TABS/TAB position-JSON schema real
  Superset-authored tabbed dashboards use (e.g. sales_dashboard).

Verified against a local build with the new content loaded: the "Charts"
tab still renders every pre-existing chart, and the new tab renders the
leaderboard correctly in both light and dark app themes (screenshotted,
not just structurally validated) -- caught and fixed one real issue this
way, a rank-number color that inherited the app's dark-mode text color and
went low-contrast against the card background.

Not part of this change: the handlebars plugin's own thumbnail.png
gallery art (the crawler has no VIZ_TYPE_THUMBNAILS entry for handlebars
by design, to preserve the existing logo art) and the separate "gallery
diversification" backlog item from the same PR comment.
2026-08-06 16:23:46 -07:00
Claude Code 5bbd4c6d23 fix(examples): show real up/down, hierarchy, density and variance in 4 misc-chart thumbnails
Addresses rusackas's own leftover checklist on #42328:

- Waterfall: pointed at a new quarterly_sales_delta dataset (Q2 2003 - Q2
  2005 quarter-over-quarter sales change) instead of raw quarterly totals,
  which are always positive and rendered as all-green bars regardless of
  whether sales actually grew or shrank. The delta metric produces genuine
  green/red bars.
- Tree: added a Territory > Country > City sales hierarchy (41 nodes, 3
  levels) built from cleaned_sales_data, replacing the tiny 12-node
  USA-only example that shipped with this PR.
- Horizon: new Population Growth Horizon example (20 most populous
  countries' annual population growth rate, 1960-2014) replacing the old
  7-band/2.5-year example that left most of the thumbnail empty; 20 bands
  at the default 25px row height now fill the full 512px canvas and show
  real oscillation (several countries dip negative).
- deck.gl Heatmap: fixed a real bug -- color_scheme_type was set to
  'default_palette', which isn't a valid value in this plugin's control
  panel (only 'fixed_color'/'linear_palette' are), so getColorRange()
  fell through every switch case and the layer silently rendered with an
  undefined colorRange. Set it to 'linear_palette' with the 'fire' scheme,
  and raised row_limit from 5,000 to 50,000 (of 261k available rows).

Recaptured thumbnails for exactly these 4 viz types (light + dark, deck.gl
heatmap dark correctly skipped as canvas/theme-invariant) via
capture-viz-thumbnails.spec.ts against a local build with the new examples
loaded, and visually confirmed each one before committing.
2026-08-06 15:55:12 -07:00
Claude Code 3b98869e0a Merge remote-tracking branch 'origin/master' into misc-charts-examples
# Conflicts:
#	superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail.png
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx
#	superset/migrations/shared/migrate_viz/base.py
#	tests/unit_tests/migrations/viz/upgrade_malformed_query_context_test.py
2026-08-06 15:20:30 -07:00
Claude Code e7fa8529ce Merge remote-tracking branch 'origin/remove-legacy-viz-pipeline' into HEAD
# Conflicts:
#	UPDATING.md
#	superset-frontend/package-lock.json
#	superset-frontend/package.json
#	superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts
#	superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
#	superset-frontend/playwright/components/dashboard/index.ts
#	superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1-dark.jpg
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1.jpg
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2-dark.jpg
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2.jpg
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts
#	superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts
#	superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts
#	superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.ts
#	superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-chord/package.json
#	superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-chord/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-chord/test/index.test.ts
#	superset-frontend/plugins/plugin-chart-country-map/package.json
#	superset-frontend/plugins/plugin-chart-country-map/src/countries/chile.geojson
#	superset-frontend/plugins/plugin-chart-country-map/src/countries/fiji.geojson
#	superset-frontend/plugins/plugin-chart-country-map/src/countries/french_polynesia.geojson
#	superset-frontend/plugins/plugin-chart-country-map/src/countries/philippines_regions.geojson
#	superset-frontend/plugins/plugin-chart-country-map/src/countries/united_states_minor_outlying_islands.geojson
#	superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/index.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/controlPanel.tsx
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/images/example1-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/images/example1.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/images/example2-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/images/example2.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-echarts/src/Rose/index.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/EchartsTimePivot.tsx
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example-dark.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example.jpg
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/index.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformProps.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
#	superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
#	superset-frontend/plugins/plugin-chart-echarts/src/index.ts
#	superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformProps.test.ts
#	superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-paired-t-test/package.json
#	superset-frontend/plugins/plugin-chart-paired-t-test/src/buildQuery.ts
#	superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-paired-t-test/test/buildQuery.test.ts
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/package.json
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/buildQuery.ts
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-partition/src/buildQuery.ts
#	superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-partition/src/images/thumbnailLarge.png
#	superset-frontend/plugins/plugin-chart-rose/CHANGELOG.md
#	superset-frontend/plugins/plugin-chart-rose/README.md
#	superset-frontend/plugins/plugin-chart-rose/package.json
#	superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx
#	superset-frontend/plugins/plugin-chart-rose/src/Rose.ts
#	superset-frontend/plugins/plugin-chart-rose/src/controlPanel.tsx
#	superset-frontend/plugins/plugin-chart-rose/src/images/example1-dark.jpg
#	superset-frontend/plugins/plugin-chart-rose/src/images/example1.jpg
#	superset-frontend/plugins/plugin-chart-rose/src/images/example2-dark.jpg
#	superset-frontend/plugins/plugin-chart-rose/src/images/example2.jpg
#	superset-frontend/plugins/plugin-chart-rose/src/index.ts
#	superset-frontend/plugins/plugin-chart-rose/src/stories/Rose.stories.tsx
#	superset-frontend/plugins/plugin-chart-rose/src/stories/data.ts
#	superset-frontend/plugins/plugin-chart-rose/src/transformProps.ts
#	superset-frontend/plugins/plugin-chart-rose/tsconfig.json
#	superset-frontend/plugins/plugin-chart-rose/types/external.d.ts
#	superset-frontend/plugins/plugin-chart-world-map/package.json
#	superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail-dark.png
#	superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail.png
#	superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnailLarge.png
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.color.test.tsx
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.test.tsx
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx
#	superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.ts
#	superset-frontend/scripts/check-custom-rules.js
#	superset-frontend/src/components/Chart/chartAction.ts
#	superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
#	superset-frontend/src/visualizations/presets/MainPreset.ts
#	superset-frontend/tsconfig.json
#	superset/async_events/async_query_manager.py
#	superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py
#	superset/views/utils.py
#	tests/unit_tests/async_events/async_query_manager_tests.py
2026-08-04 22:53:51 -07:00
Claude Code c487c006e9 fix(migrations): merge report-retry-state-columns with compare-chart merge revision
master added a new migration head (f3a8c1d2e9b7) branching from the same
revision already folded into 9d744c5dd981, producing two heads again.
2026-08-04 10:18:55 -07:00
Claude Code fd27a0c2fd Merge remote-tracking branch 'origin/master' into HEAD
# Conflicts:
#	superset-frontend/package-lock.json
2026-08-04 10:13:25 -07:00
Claude Code 42c6746339 Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
A second, smaller conflict landed while pushing the previous rebase
fix: master's Prettier-to-Oxfmt migration touched every plugin
package.json, colliding with this branch's own renames/deletions of
the legacy-prefixed packages.

Resolved:
- Two modify/delete conflicts (legacy-plugin-chart-rose,
  legacy-preset-chart-nvd3): kept deleted. Both packages are fully
  removed on this branch with no replacement package (Rose now lives
  inside plugin-chart-echarts/src/Rose/).
- Five package.json field-reordering conflicts (chord, country-map,
  paired-t-test, parallel-coordinates, world-map): master's migration
  reordered/added fields (keywords, homepage) on the old legacy-
  prefixed paths while this branch had independently reordered the
  same renamed files. Reconstructed each to the canonical field order
  already used by sibling packages that merged cleanly (partition,
  echarts).
- One content conflict in paired-t-test/src/TTestTable.tsx: discarded
  master's dead legacy reactable-based sortConfig block, already fully
  replaced by this branch's own COMPARATORS object earlier in the same
  file.

package-lock.json regenerated via a clean npm install (no
--legacy-peer-deps, after clearing corrupted npm cache entries) to
correctly reflect both the dependency-tree changes from this rebase
and the Oxfmt migration's own devDependency swap (prettier/eslint-
plugin-prettier out, oxfmt in).
2026-08-03 10:51:30 -07:00
Claude Code fbd2e6032e fix(deps): regenerate package-lock.json, previous regeneration was incomplete
The lockfile committed while resolving the last rebase conflict against
master was missing several real transitive dependencies (preact,
@react-spring/*, polished, react-ace, @deck.gl/widgets), which failed
npm ci in CI's frontend-build/docker-build with "Missing: X from lock
file" errors, cascading into pre-commit/cypress/playwright/docker
failures.

Root-caused to two compounding issues on the machine that regenerated
it: a corrupted local npm cache (npm cache verify found and cleaned up
643 bad entries) and the --legacy-peer-deps flag, which does not
auto-install a peer dependency's own transitive deps the way a plain
npm install does -- @deck.gl/widgets is itself a peer dependency, and
preact is one of its own dependencies, so it was silently dropped.

Regenerated cleanly with a plain npm install after clearing the cache;
verified node_modules/preact, node_modules/@react-spring/core, and
node_modules/@deck.gl/widgets are all present this time.
2026-08-03 10:33:07 -07:00
Claude Code e66b2f8be5 Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
Routine rebase; the only conflict was package-lock.json, regenerated
via npm install against the already-cleanly-merged package.json.
Everything else (package.json, plugin-chart-partition's package.json,
superset/views/base.py, superset/views/core.py,
tests/integration_tests/security_tests.py) merged automatically.
2026-08-03 10:04:50 -07:00
Claude Code 04bd07ad6e fix(migrations): don't discard an original empty query_context on downgrade
Addresses @sadpandajoe's follow-up review comment on #41714: an
original query_context with "queries": [] backs up as a falsy-but-
present empty list, not None. downgrade_slice's truthiness check
(`if queries_bak:`) treated that the same as "no context was ever
stored" and set query_context to None, discarding the slice's
original datasource and form_data instead of restoring it.

Also fixes the same ambiguity at the source: QUERIES_BAK_FIELD_NAME
defaulted to {} rather than None when absent from form_data, which
could be misread as a real (if malformed) backup rather than "key not
present". Changed to the natural None default so "no backup" and "an
empty list backup" stay distinguishable, and dedented the
query_context assignment that was incorrectly nested inside the
"form_data" in query_context check (a context missing "form_data"
would restore params/viz_type but silently leave query_context at its
upgraded value).

Also updates the stale UPDATING.md note for this PR: the percent-
re-basing and deck_multi autozoom limitations it warned about were
both resolved earlier in this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 09:27:28 -07:00
rusackasandClaude Opus 4.8 1c98c0711e fix(deckgl): address review feedback on Multi.tsx v1 rewrite
- Enforce DECK_MULTI_MAX_SLICES client-side before fanning out
  per-layer metadata/data requests, mirroring the cap the removed
  viz.py pipeline used to enforce server-side.
- Accumulate autozoom features by slice_id (bucketed by viz_type)
  instead of overwriting by viz_type, so two layers sharing a
  viz_type no longer clobber each other's points.
- ChartClient.loadQueryData now posts the built query context at
  the top level of the request body (as /api/v1/chart/data expects)
  instead of nesting it under a query_context key, and unwraps the
  {result: [...]} response shape correctly.
- migrate_viz downgrade no longer discards a hand-edited
  query_context that lacks a "queries" key; the whole context is
  backed up and restored verbatim in that case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:02:54 -07:00
Claude Code 13dd061907 fix(migrations): merge alembic heads after master rebase
The merge onto master left two migration heads: this branch's
88360afb61ed (migrate_compare_chart_to_echarts) and master's
e7d93a524ff6 (add_purge_audit_log), from parallel migration work
landing on both sides. Adds the standard no-op merge revision joining
them, generated via `superset db merge` and verified with a clean
`superset db upgrade` from an empty database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 12:57:39 -07:00
Claude Code bf47750682 Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
The only real conflict was an import-list collision in
Timeseries/transformProps.ts (this branch's DTTM_ALIAS next to master's
DataRecordValue); kept both. Country-map's regenerated GeoJSON files and
notebook edits from master merged cleanly via rename detection onto this
branch's already-renamed plugin-chart-country-map/ package, with no
overlapping edits to reconcile.
2026-07-31 10:47:57 -07:00
Claude Code 990785cfa4 Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
Resolves a conflict in ExploreChartPanel/index.tsx between master's new
standalone-download-control feature and this branch's earlier removal
of the raw core Alert import in favor of the ExploreAlert component:
kept master's URL_PARAMS/getUrlParam/StandaloneDownloadControl wiring,
dropped the reintroduced Alert import since it's unused on this branch.
2026-07-28 22:16:23 -07:00
Claude Code 7174af24ca Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
Resolves conflicts between the async-job-cancellation feature added on
master and this branch's removal of the legacy explore_json pipeline:
kept the cancellation feature (_register_cancellable_job, cancel_job,
is_job_cancelled, the task_id/SIGUSR1 revoke wiring, and its tests) and
dropped the parts of it that only applied to submit_explore_json_job /
load_explore_json_into_cache, which this branch already deleted. Also
merged master's get_request_json_body() hardening in views/utils.py
and its abort-signal wiring in chartAction.ts (adapted to this
branch's already-simplified, useLegacyApi-free handleChartDataResponse
signature).
2026-07-28 09:50:12 -07:00
Evan RusackasandClaude Code bbd021925a fix(viz): follow-ups from #41714 self-review (#42530)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-28 09:14:15 -07:00
Claude Code 464674f193 fix(migrations): re-point bubble-chart migration onto master's current alembic head
master added version_transaction_issued_at_index (d3b9a1f6c204) on top of
add_extension_storage_table (e5f6a7b8c9d0), which our bubble-chart migration
was still pointing at as its down_revision -- producing two heads and
failing "superset db upgrade" (and everything downstream: load_examples,
postgres/mysql/sqlite integration tests, cypress, playwright) with
"Multiple head revisions are present". Re-point onto the new head so the
chain is linear again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 20:35:30 -07:00
Claude Code c7a83794d2 Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline 2026-07-27 20:22:58 -07:00
Evan RusackasandClaude Code dbc561f80f chore(viz)!: rebuild Nightingale Rose on ECharts, removing nvd3 entirely (#42381)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 20:19:17 -07:00
rusackasandClaude Opus 4.8 86003221bc test: remove test_viz_query_obj.py leftover import of deleted viz module
This test module imported `superset.viz`, which this PR removes, breaking
unit-test collection. The jinja-preservation regression it guarded is
already covered by
core_tests.py::test_split_adhoc_filters_preserves_jinja_templates against
the new (non-viz.py) code path, so the file is redundant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 19:04:14 -07:00
rusackasandClaude Opus 4.8 9615e7c024 fix(viz): drop dead legacy-API dataset-required banner left over from rebase
The explore_json/viz.py removal commit deleted this banner's supporting
state and imports (it only applied to useLegacyApi chart types), but a
rebase conflict against master reintroduced the JSX referencing the
now-undefined vizTypeNeedsDataset/setShowDatasetModal/Alert. Also picks
up a prettier formatting fixup in StatefulChart.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:41:43 -07:00
rusackasandClaude Opus 4.8 80c8d9f388 fix(plugins): drop stale "Legacy" wording from renamed chart package descriptions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:24:00 -07:00
Claude Code 59f1d91b07 chore(explore): drop comments pointing at the long-gone nvd3_vis.css
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:59 -07:00
Claude Code a4fc9ae493 chore(viz)!: delete the unregistered preset-chart-nvd3 package
Nothing has registered this package since the Bullet and Time-series
Period Pivot charts moved to ECharts; remove the workspace, its
tsconfig reference, the custom-rules grandfather entry, and its
lockfile entries (which also sweeps the nvd3-only fast-safe-stringify
dependency). The nvd3-fork library itself remains a dependency of
plugin-chart-rose, whose legend and tooltip still use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:58 -07:00
Claude Code 5bbe3ac750 fix(rose): use the theme text color for chart and legend text
The nvd3-fork legend and axis labels render SVG text with the default
black fill, which is unreadable against a dark theme background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:58 -07:00
Claude Code 1aeff038d7 test(timeseries): cover the percent-change flag end-to-end from migrated params
Adds transformProps-level coverage for rebase_percent_change: series
rebased from the first point with a forced percent axis format, and the
snake_case key the compare-chart migration stores read through real
ChartProps camelization. Also guards the rebase against form data with
no explicit x-axis by falling back to the temporal alias, matching
extractSeries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:57 -07:00
Claude Code b73f6a8bd5 fix(bullet): range labels inside their bands, always-on tooltip, legend-aware grid
Range labels move from mid-bar scatter points onto the markArea bands
(insideTopRight), so each label sits inside the range it names and the
rightmost label no longer clips at the chart edge. The tooltip stays
enabled regardless of the Show labels/legend toggles and the measure bar
tooltip names the range the value falls within. The grid top now
estimates wrapped legend rows from item text widths instead of assuming
a single row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:57 -07:00
Claude Code 1859b375a7 chore(migrations): re-id compare-chart migration and re-point bubble migration after rebase
Master's add_extension_storage_table migration landed with revision id
e5f6a7b8c9d0, colliding with the compare-chart migration's id; re-id it
to 88360afb61ed and chain the bubble migration off master's head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:56 -07:00
Claude Code b85468ee17 chore: drop unused feature_flag_manager import after rebase
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:55 -07:00
Claude Code 2261c61791 docs(embedded): document the iframe session cookie race in the pivot spec
Also retriggers CI so the merge ref picks up the master fix from #42274
(an empty commit does not trigger the path-gated workflows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:55 -07:00
Claude Code 9fe31e3ec5 test(embedded): store a query_context on the pivot collapse fixture chart
The spec's fixture chart was created with params only. Real charts saved
through Explore always persist a query_context, and guest (embedded)
chart data requests are validated against the stored chart: with no
stored query_context and no columns/groupby params keys, the pivot
payload's query columns read as tampering and every request 403s.

The suite previously passed only when the embedded iframe happened to
ride the admin session from storageState instead of the guest token (a
cookie race between the /embedded response's anonymous session and the
SDK's csrf/guest-token fetches), which masked the 403 on master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:54 -07:00
Claude Code c7f0167a28 test(charts): update warm-up cache assertions for the actionable message
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:54 -07:00
Claude Code 52b5110a53 style: prettier formatting for UPDATING.md and SavedQueries test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:53 -07:00
Claude Code a33ab097e1 fix(time-table): escape backslashes in flattened labels; actionable null-query-context errors
Escaping only commas let group tuples like (a\\, b) and (a-comma, b)
flatten to identical column keys; backslashes are now escaped first, with
a regression test pinning the collision pair. The cache warm-up error for
charts whose query context has not been generated yet tells the operator
how to resolve it, and UPDATING.md documents the one-time lazy generation
for charts migrated in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:53 -07:00
Evan RusackasandClaude Code 05ce4d41bf feat(bullet): rebuild the Bullet Chart on ECharts (#42225)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 15:23:53 -07:00
Evan RusackasandClaude Code 8cd636bcd0 feat(time-pivot): rebuild Time-series Period Pivot on ECharts (#42245)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 15:23:53 -07:00
Evan RusackasandClaude Code 18231d9bd4 feat(timeseries): percent-change rebasing with a draggable baseline (#42247)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 15:23:52 -07:00
EvanandClaude Opus 4.8 ca17830175 fix: cast Arc test color accessors through unknown for tsc
lint-frontend's tsc build failed with TS2352 on the direct Accessor ->
ColorAccessor cast; go through unknown first as the compiler suggests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:52 -07:00
EvanandClaude Opus 4.8 2be95c6283 fix: repair type errors from removed useLegacyApi metadata field
Three plugin tests still asserted on ChartMetadata.useLegacyApi, which
this PR drops entirely, and the Arc layer test called deck.gl color
accessors without narrowing their Accessor union type. Both broke
lint-frontend's tsc check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code 6edeafff6b fix(deck-arc): default missing layer colors instead of crashing
The Arc layer read color_picker/target_color_picker straight off form data
and dereferenced .r/.g/.b, so a saved chart without those controls crashed
the ArcLayer on init ("Cannot read properties of undefined (reading 'r')").
This surfaced in deck.gl Multiple Layers, where sub-slices arrive as raw
saved form data without control-default hydration and the Arc example never
persisted target_color_picker. Fall back to the control-panel default color
(PRIMARY_COLOR) when a picker is absent, matching how Path and Scatter
already guard, and harden Polygon's fill/stroke pickers the same way. Adds
Arc color-accessor tests for the missing and present cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code b2d2aa4452 feat(deck-multi): surface layer load failures in the chart
When a deck.gl Multiple Layers sub-layer fails to load (e.g. it is bound to
a dataset that lacks the columns it needs), the error previously only threw
to the browser console, leaving the map looking silently empty. Capture the
per-slice failure and render a warning Alert over the map, normalized via
getClientErrorObject so the server message (e.g. "Columns missing in
dataset") is shown. Errors reset on each reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code 09a034c573 test(plugins): cover the migrated calendar/chord/country-map registration
Codecov flagged the calendar transformProps and the calendar/chord/
country-map plugin index modules as uncovered. Add plugin-registration
tests (instantiate the plugin, assert v1 metadata and the buildQuery
loader), a calendar transformProps test (v1 reshape plus display-option
passthrough, and the non-array passthrough branch), and a calendar
buildQuery case for an unrecognized subdomain granularity falling back to
minutes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code f2fd536823 fix(deck-multi): query layers against their real datasource, not stale params
Each deck.gl Multiple Layers sub-layer is an independent saved chart with
its own dataset, and its layer query must hit that dataset. fetchSubslices
rebuilt the layer form_data from the saved params, whose `datasource`
string can be stale -- example charts hardcode a datasource id that does
not match the imported dataset's real id. The layer query then went to the
wrong dataset and the server rejected it with 400 "Columns missing in
dataset" (e.g. LON/LAT), so no layer rendered. Standalone the chart works
because it resolves its datasource from the slice relationship, not params.

Use the chart's authoritative datasource_id/datasource_type from the chart
API to build the layer datasource, falling back to params only when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code 9c6b1f0eb6 fix(deck-multi): correct the points-collection typing to unblock tsc
The GET_POINTS_BY_VIZ_TYPE map annotated its values as
(features: JsonObject[]) => [number, number][], but the per-layer
getPoints helpers have looser, inconsistent signatures (getPointsGeojson
takes Point[]), so the map failed tsc. Replace it with an explicit-spread
collectPoints(features) mirroring the original getAdjustedViewport, which
type-checks and works for both the payload and the per-layer accumulator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:51 -07:00
Claude Code f2055d8446 fix(deck-multi): autozoom to layer data in the v1 fetch path
deck.gl Multiple Layers computed its viewport once, from
props.payload.data.features, and never again. In the v1 path that payload
is empty (each layer is fetched client-side after mount), so autozoom had
no points to fit and the camera stayed at the default location while the
layers rendered off-screen -- the map showed but appeared to have no data.

Accumulate each layer's features as it is fetched and refit the viewport
to the combined points (when autozoom is enabled), mirroring what the
legacy pre-merged payload allowed. Also factor the per-viz_type point
collection into a shared helper. Adds a v1-path test covering both layer
rendering and the viewport refit, which the existing suite (all legacy
payload.data.slices) did not exercise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:50 -07:00
Claude Code fbd088af48 chore(paired-t-test): drop the unused prop-types dependency
Nothing in the plugin imports prop-types since the class components were
migrated to function components. Remove it and its lockfile reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:50 -07:00
Claude Code d7c34215a2 fix(paired-t-test): bound the scroll container to the chart height
The container relied on height: 100%, which does not resolve because the
chart's parent is not height-bounded in the v1 render path, so tall output
(many groups across multiple metrics) still clipped without scrolling.
Thread the explicit height SuperChart passes via chartProps through
transformProps and apply it to the scroll container, matching the pattern
used by the pivot-table chart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:50 -07:00
Claude Code a82128cde0 refactor(paired-t-test): use the antd Table and drop the reactable dependency
Replace the interim plain-table rendering with the design-system antd Table
from @superset-ui/core/components: native column sorting via the ported
comparators, per-cell lift/p-value/significance coloring through semantic
theme tokens (no bespoke CSS classes), and row-click control selection via
onRow. Removes the reactable dependency entirely (its fragile, React-18-
incompatible rendering was the production bug) and the leftover reactable-*
styling. Also drops the now-stale distributions/cephes lockfile entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:50 -07:00
Claude Code 9ff15cb53e fix(paired-t-test): scroll the chart when tables overflow its height
The container styled the non-existent .scrollbar-container class, so the
chart never scrolled and tall output (many groups across multiple metrics)
was clipped with no way to reach the rest. Make the chart root fill its
allotted height and scroll on overflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:49 -07:00
Claude Code ac7f53326f fix(paired-t-test): render the table without the reactable dependency
The migrated chart returned correct data (verified end-to-end) yet showed
table headers with no body rows in production builds, while every jest
path rendered rows. The common factor was reactable, an unmaintained 2016
library that relies on React-18-incompatible legacy lifecycles
(componentWillMount/componentWillReceiveProps) and identifies its row
children by stringifying components -- fragile in a production bundle.

Replace it with a plain semantic table that reproduces every behavior
(control-row selection, per-cell lift/p-value/significance coloring, and
the same column sort comparators), reusing the existing CSS class hooks so
the surrounding styles are unchanged. Rendering is now deterministic and
fully unit-testable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:49 -07:00
EvanandClaude Opus 4.8 feb224686b fix(tests): drop removed useLegacyApi prop from ChartRenderer test metadata
The legacy-pipeline removal deleted useLegacyApi from ChartMetadataConfig;
the enableNoResults test still passed it, breaking tsc in lint-frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:49 -07:00
Claude Code f93cc1a67b test(paired-t-test): prove the v1 pipeline renders table rows end-to-end
Guards the "headers render but no body rows" failure mode by driving the
real transformProps -> PairedTTest pipeline with a realistic flat
/api/v1/chart/data timeseries response (main.birth_names shape): asserts
transformProps keys the reshaped data by metric label with one series per
group tuple, and that the rendered tables contain a body row per group,
including when optional precision/significance controls are absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:49 -07:00
Claude Code d2911672bf fix(explore): honor chart enableNoResults metadata in ChartRenderer
ChartRenderer computed the SuperChart enableNoResults prop purely from
server-pagination + AG Grid filter state (bypassNoResult) and never read
the chart plugin's own enableNoResults metadata. Charts that fetch their
own data and issue no top-level query, like deck.gl Multiple Layers, set
enableNoResults: false but still hit the "No results were returned for
this query" empty state in explore, which pre-empts their self-fetching
renderer from ever mounting.

AND the chart's enableNoResults metadata (defaulting to true) into
bypassNoResult so deck_multi and similar self-fetching charts render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:49 -07:00
Evan RusackasandClaude Opus 4.8 f9dfea8791 test(plugins): regression guards from the migration bug sweep
Adds low-risk import-and-assert tests across migrated plugins, guarding the
classes of bugs found while testing this branch:
- is_timeseries charts must expose a datetime control (calendar, horizon, rose,
  partition). Partition genuinely lacked one, so add sections.legacyTimeseriesTime
  to its control panel (same 500 as paired-t-test otherwise).
- deck.gl Multiple Layers must issue an empty query (self-fetches its layers).
- deck.gl Arc is timeseries only when time_grain_sqla is set.
- Time Pivot's numeric x-axis format must render a number, not the literal
  SMART_NUMBER string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:48 -07:00
Evan RusackasandClaude Opus 4.8 a5aa619668 test(paired-t-test): mock the statistics module instead of removed distributions
Removing the distributions dependency left TTestTable.test.tsx mocking a module
the component no longer imports, so the p-value/significance tests failed. Mock
./statistics.studentTwoSidedPValue to a deterministic 0.02 instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:48 -07:00
Evan RusackasandClaude Opus 4.8 dfc745c922 fix(nvd3,controls): time-pivot x-axis number format + single-line async multiselect
- Time Pivot's x-axis is a numeric offset within the period (its x_axis_format
  defaults to SMART_NUMBER), but NVD3Vis ran it through getTimeFormatter, which
  made d3 print the literal string "SMART_NUMBER" on every tick. Format it as a
  number instead. (Pre-existing bug, bycatch.)
- SelectAsyncControl never forwarded the Select 'oneLine' prop, so many selected
  tags wrapped outside the control's fixed-height box (visible on deck.gl
  Multiple Layers). Pass oneLine for multi mode so tags collapse to a '+N' tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:48 -07:00
Evan RusackasandClaude Opus 4.8 936ec951d5 fix(paired-t-test): compute p-values without the Node-only distributions dep
The chart imported 'distributions' to compute the Student's t p-value, but that
package references Node's Buffer global, which is only polyfilled in dev builds
-- so the chart rendered in dev and threw "ReferenceError: Buffer is not
defined" in production. Replace it with a self-contained, browser-safe
two-sided p-value via the regularized incomplete beta function, verified
against standard t-table critical values, and drop the dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:48 -07:00
Evan RusackasandClaude Opus 4.8 acc93dfe33 fix(deck-multi): render the map instead of the no-results empty state
deck.gl Multiple Layers issues no query of its own -- each layer is a saved
chart that fetches its data client-side, so buildQuery is intentionally empty.
Dropping useLegacyApi exposed the default enableNoResults=true, so the empty
query response now shows "No results were returned for this query" and the map
never renders. Set enableNoResults: false, matching the other self-fetching
charts (Select filter, DeckglLayerVisibility).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:47 -07:00
Evan RusackasandClaude Opus 4.8 a973290890 fix(paired-t-test): restore the datetime column control
The paired t-test chart's buildQuery sends is_timeseries, so the backend
requires a datetime column, but its migrated control panel has no time
section. Creating the chart fails with "Datetime column not provided as part
table configuration and is required by this type of chart" with no way to
supply one. Add the shared legacyTimeseriesTime section, matching its sibling
timeseries charts (rose, horizon), so a granularity column can be selected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 15:23:47 -07:00
EvanandClaude Opus 4.8 377e8e6345 fix: rebase bubble-chart migration onto master's current alembic head
master added strip_metricsqlexpressions_from_ag_grid_params (d24e6b0a9c7f)
on top of shadow_live_row_indexes (8f3a1b2c4d5e) after this branch's
bubble-chart migration was last pointed at 8f3a1b2c4d5e, producing two
alembic heads and failing `db upgrade` across every integration/E2E job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:47 -07:00
EvanandClaude Opus 4.8 71443460d1 fix: resolve oxlint unused-var/param errors and remove orphaned legacy test
Drops the dead `method` param from getChartDataRequest (the legacy
GET/POST branching it supported no longer exists now that chart data
always POSTs to /api/v1/chart/data), removes bubble-only NVD3Vis props
(entity/maxBubbleSize/xField/yField/sizeField) and the unused module-level
`formatter` left over from the bubble->bubble_v2 migration, and deletes
plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts, an
orphaned test for a src/ directory that no longer exists after the
rename to preset-chart-nvd3 (superseded by
plugins/preset-chart-nvd3/test/TimePivot.test.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:47 -07:00
EvanandClaude Opus 4.8 dcda71e869 fix: point bubble-chart migration at master's actual alembic head
down_revision on d4e5f6a7b8c9 pointed at b1c2d3e4f5a6
(add_subjects_tables), which already has a child on master
(56cd24c07170 -> 8f3a1b2c4d5e). That left two alembic heads and
broke `superset db upgrade` in every DB-backed CI job. Chain the
migration after 8f3a1b2c4d5e, master's real current head, instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:46 -07:00
EvanandClaude Opus 4.8 d59120a632 fix: resolve alembic multiple-heads conflict in migration chain
down_revision on d4e5f6a7b8c9 still pointed at 3a8e6f2c1b95, which
master's add_subjects_tables migration (b1c2d3e4f5a6) also forked
from, leaving two alembic heads and breaking `superset db upgrade`
in every DB-backed CI job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:46 -07:00
EvanandClaude Opus 4.8 ba795ccfa0 fix: format Multi.tsx and update Multi.color.test.tsx for registry-based layer loading
Multi.tsx now resolves sublayer buildQuery/transformProps via the chart
plugin registries instead of the removed legacy explore.ts helper; update
the color test's mocks to register deck_scatter/deck_arc stubs and mock
SupersetClient.post (matching the new fetch call) instead of .get.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:45 -07:00
EvanandClaude Opus 4.8 6a5049daed fix: repair stale package-lock.json and alembic migration branch
- Remove dangling package-lock.json link entries for the removed
  legacy-* plugin workspaces and regenerate entries for their
  renamed plugin-chart-* counterparts; npm ci was failing with
  EUSAGE because the lockfile referenced workspace paths that no
  longer exist on disk.
- Re-parent the bubble-chart-to-echarts migration
  (d4e5f6a7b8c9) onto the current single migration head
  (3a8e6f2c1b95) instead of a now-superseded ancestor, resolving
  the "Multiple head revisions" alembic error that was failing
  test-sqlite/test-postgres/test-mysql/cypress/playwright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:45 -07:00
EvanandClaude Opus 4.8 83e7098ee8 fix: restore OAuth2RedirectError import after rebase
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:44 -07:00
Claude Code b71ea77080 docs: add UPDATING.md entries for the legacy viz pipeline removal and drop the working trackers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:23:43 -07:00
Evan RusackasandClaude Code b3435a04cf chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline (#41750)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 15:23:41 -07:00
Evan RusackasandClaude Code 1771f05d33 chore(viz): drop the legacy- prefix from migrated chart plugin packages (#41751)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 15:05:59 -07:00
Claude Code 3dfe193ca0 chore(viz): update SCOPE tracker — all charts migrated, phases 2-3 in review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:57 -07:00
Evan RusackasandClaude Code c54abcfda8 feat(compare): migrate compare charts to echarts_timeseries_line and drop the legacy plugin (#41738)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:57 -07:00
Evan RusackasandClaude Code b2849d826a feat(deck-multi): migrate deck_multi chart to the v1 chart data API (#41730)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:57 -07:00
Evan RusackasandClaude Code 2cd9873540 feat(bubble): migrate saved bubble charts to bubble_v2 and drop the legacy plugin (#41728)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:56 -07:00
Evan RusackasandClaude Code 8948b5af94 fix(legacy-viz): restore line-engine default ordering for horizon and rose (#41732)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:55 -07:00
Evan RusackasandClaude Code 3bcb003eeb feat(partition): migrate partition chart to v1 chart data API (#41729)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:54 -07:00
Claude Code 480fbe0e06 chore(viz): update SCOPE tracker after tier 2 completion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:54 -07:00
Evan RusackasandClaude Code 3abbe1c84a feat(time-pivot): migrate time_pivot chart to v1 chart data API (#41727)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:53 -07:00
Evan RusackasandClaude Code 311b0087f3 feat(rose): migrate rose chart to v1 chart data API (#41726)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:53 -07:00
Evan RusackasandClaude Code 0fea3c88a5 feat(horizon): migrate horizon chart to v1 chart data API (#41725)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:53 -07:00
Claude Code e61610bd98 chore(viz): update SCOPE tracker after tier 1-2 merges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:52 -07:00
Evan RusackasandClaude Code 9729306c06 feat(time-table): migrate time_table chart to v1 chart data API (#41723)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:51 -07:00
Evan RusackasandClaude Code 93a02449db feat(world-map): migrate world_map chart to v1 chart data API (#41720)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:51 -07:00
Evan RusackasandClaude Code cbbcf1bed2 feat(calendar): migrate cal_heatmap chart to v1 chart data API (#41724)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:51 -07:00
Evan RusackasandClaude Code 218c64296f feat(paired-t-test): migrate paired_ttest chart to v1 chart data API (#41721)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Evan RusackasandClaude Code 39eced2c2e feat(chord): migrate chord chart to v1 chart data API (#41719)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Claude Code bef4680ffd chore(viz): update SCOPE tracker after phase 0 + tier 1 merges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Evan RusackasandClaude Code 5870259219 feat(bullet): migrate bullet chart to v1 chart data API (#41718)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Evan RusackasandClaude Code a4bcd2cc87 feat(parallel-coordinates): migrate para chart to v1 chart data API (#41716)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Evan RusackasandClaude Code f8dd65cc0b feat(country-map): migrate country_map chart to v1 chart data API (#41717)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:50 -07:00
Evan RusackasandClaude Code b1bc8fa8f2 chore(viz): phase 0 — remove orphaned viz.py classes and nvd3 BoxPlot leftovers (#41715)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-27 14:54:49 -07:00
Claude Code 5d86215a9d chore(viz): add SCOPE/UPDATES trackers for legacy viz pipeline removal
Working documents for the remove-legacy-viz-pipeline feature branch;
stripped before final merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:54:49 -07:00
Claude Code f866f25546 Merge branch 'remove-legacy-viz-pipeline' into misc-charts-examples
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 02:59:41 -07:00
Claude Code 3392576923 chore(explore): drop comments pointing at the long-gone nvd3_vis.css
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 02:59:10 -07:00
Claude Code e95b37450e chore(viz)!: delete the unregistered preset-chart-nvd3 package
Nothing has registered this package since the Bullet and Time-series
Period Pivot charts moved to ECharts; remove the workspace, its
tsconfig reference, the custom-rules grandfather entry, and its
lockfile entries (which also sweeps the nvd3-only fast-safe-stringify
dependency). The nvd3-fork library itself remains a dependency of
plugin-chart-rose, whose legend and tooltip still use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 02:57:52 -07:00
Claude Code ff73f2c070 Merge branch 'remove-legacy-viz-pipeline' into misc-charts-examples
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:21:13 -07:00
Claude Code 612fd0ac5d Merge branch 'master' into remove-legacy-viz-pipeline
Master's #41996 relocated legacy filter sanitization out of viz.py and
utils/core.py into the legacy explore path this branch removes; the
remaining utils signature change merges cleanly, viz.py stays deleted,
and the new test file importing superset.viz is removed with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:20:12 -07:00
Claude Code 83dbdfa355 Merge remote-tracking branch 'origin/misc-charts-examples'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:38:53 -07:00
Claude Code fc3872fe9e Merge branch 'remove-legacy-viz-pipeline' into misc-charts-examples
Brings in the rose dark-theme text fix and the master sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:38:04 -07:00
Claude Code 0946df50a1 chore(viz): recapture all gallery thumbnails on dashboard-context backgrounds
Every thumbnail regenerated from the tuned example charts on the
white/dark card background users actually see, including the restored
generic handlebars logo and a predictive-forecast generic time-series
thumbnail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:37:48 -07:00
Claude Code d9c3a2cf4c feat(examples): purpose-built thumbnail charts for the viz gallery
Nineteen additional Misc Charts examples modeling each chart type at
its most recognizable: simpler chord, YoY big number, triangular
funnel, multi-range gauge, compact heatmap grid, bell-curve histogram,
dual-axis bar+line mixed chart, labeled donut, video-game-sales radar,
sunburst with total, stacked area and bars, low-series marker lines
(straight, smooth, stepped), high-cardinality treemap, quarterly
waterfall, multi-metric table and a cell-highlighted pivot. Also tunes
existing examples: monthly-grain period pivot, sales-based time table,
tighter parallel coordinates, recentered point cluster map, and
deck.gl hexagon/scatter viewports pinned (autozoom off) for the
downtown 3D view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:37:46 -07:00
Claude Code f84d7c664e feat(playwright): dashboard-context backgrounds, hover captures, curated slice picks
Captures now paint the dashboard card background (white or dark) over
explore's gray layout background, hover a populated cell before the
still for charts whose identity benefits from a visible tooltip
(calendar heatmap), and prefer the purpose-built thumbnail examples for
every tuned viz type. Handlebars is intentionally unmapped so its
generic logo survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:37:38 -07:00
Claude Code 45fd4117a9 fix(rose): use the theme text color for chart and legend text
The nvd3-fork legend and axis labels render SVG text with the default
black fill, which is unreadable against a dark theme background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:36:32 -07:00
Evan Rusackas 79acc3a712 Merge branch 'remove-legacy-viz-pipeline' into misc-charts-examples 2026-07-23 21:09:22 -07:00
Claude Code 62d0cfb6d3 Merge branch 'master' into remove-legacy-viz-pipeline
Resolves StatefulChart/chartAction conflicts by keeping master's async
(202) handling and staleness guards with the legacy explore_json paths
stripped: the endpoint is always /api/v1/chart/data, the async handler
loses its useLegacyApi parameter, and the legacy body-wrapping branch
and its test are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:45:23 -07:00
Evan Rusackas b885657ad0 Merge branch 'master' into remove-legacy-viz-pipeline 2026-07-23 16:40:39 -07:00
Claude Code a191f72698 fix(playwright): treat AG Grid and period-over-period KPI as markup-only captures
Both render without svg/canvas/table, so the crawler needs the looser
rendered signal when capturing them from a flag-enabled environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:35:04 -07:00
Claude Code 498400e974 chore(viz): delete dead thumbnailLarge images and capture art for newly exemplified types
Removes 30 thumbnailLarge.png files no plugin imports, and captures
live thumbnails (light and dark) for the generic/smooth/step time
series, handlebars, deck.gl contour and heatmap, and point cluster map
from their new example charts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:35:03 -07:00
Claude Code b9a8321802 feat(examples): example charts for every remaining unexemplified viz type
Adds Misc Charts examples for the generic, smooth and stepped
time-series lines, handlebars, deck.gl contour and heatmap layers, and
the point cluster map (on the keyless maplibre renderer). Flag-gated
types (pop_kpi behind ChartPluginsExperimental, ag-grid-table behind
AgGridTableEnabled) are deliberately excluded: their plugins are
unregistered on default deployments, where their charts would render as
errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:34:49 -07:00
Claude Code 902ebcdf72 chore(viz): refresh viz-picker gallery art from live example charts
Regenerates thumbnails (light and dark) for every viz type on the
example dashboards via the thumbnail crawler, replaces the obsolete
nvd3-era Bullet/TimePivot/TimeTable example gallery screenshots, and
adds a percent-change example (Line3) to the Line chart gallery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:06:25 -07:00
Claude Code f76471f30f fix(playwright): harden the thumbnail crawler against hangs and text-only charts
Adds an explicit navigation timeout so one hung load cannot stall the
crawl until the test timeout, raises the overall budget to 90 minutes,
and gives markup-only charts (big_number_total, handlebars) a looser
rendered signal since they produce no svg/canvas/table for the default
selector to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:06:12 -07:00
Claude Code b671356748 fix(chart): guard the percent-change baseline against an unapplied chart option
On warm explore navigations getOption() can be undefined or empty when
the baseline effect first runs, crashing the chart render with 'Cannot
read properties of undefined (reading series)'. Guard the read and retry
installation once the chart finishes rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:05:32 -07:00
Claude Code ff273478ff docs(playwright): update thumbnail crawler header for dark-variant capture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:29:30 -07:00
Claude Code f1cc9367de feat(playwright): capture dark variants and obsolete gallery examples in the thumbnail crawler
Every capture now also renders a dark version via prefers-color-scheme
emulation when the plugin ships a -dark sibling (or the image is new),
skipping the write when the app ignores the emulation so real dark art
is never clobbered with light captures. The Bullet, TimePivot and
TimeTable example.jpg gallery images — still screenshots of the removed
nvd3 renderers — are refreshed as wide extra captures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:28:55 -07:00
Claude Code 02e4b4be7d feat(playwright): turn the thumbnail capture tool into a dashboard crawler
Instead of enumerating charts, the tool discovers every chart on every
dashboard via the API and captures one representative per viz type, so
it keeps working as example dashboards evolve. The only static piece is
the viz-type-to-image-path map (one line per plugin); unmapped viz types
are reported without failing. Supports VIZ_TYPES=a,b filtering, a
preferred-slice override per type, and named extra captures for gallery
images like the Line percent-change example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:19:48 -07:00
Claude Code ecb4aa93f6 feat(playwright): add a viz-picker thumbnail capture tool
A CAPTURE_THUMBNAILS-gated Playwright spec that renders each migrated
legacy chart's example standalone at 512x512 and overwrites the plugin's
gallery thumbnail, plus an npm run playwright:thumbnails entry point.
Covers the Misc Charts examples and the existing chord, horizon,
parallel coordinates, country map and world map examples; the Line
percent-change capture lands as a new gallery image to be registered in
the plugin metadata after capture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:53:57 -07:00
Claude Code 4e1913d436 feat(examples): add migrated legacy chart examples to the Misc Charts dashboard
Adds example charts for every migrated legacy visualization that had no
example instance: calendar heatmap, nightingale rose, partition, paired
t-test, bullet, time-series period pivot, a percent-change line, and a
time table. Country map, parallel coordinates, chord, horizon and world
map already have examples in other dashboards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:51:56 -07:00
Claude Code b6ae8fb25d test(timeseries): cover the percent-change flag end-to-end from migrated params
Adds transformProps-level coverage for rebase_percent_change: series
rebased from the first point with a forced percent axis format, and the
snake_case key the compare-chart migration stores read through real
ChartProps camelization. Also guards the rebase against form data with
no explicit x-axis by falling back to the temporal alias, matching
extractSeries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:11:42 -07:00
Claude Code 10f2461b5f fix(bullet): range labels inside their bands, always-on tooltip, legend-aware grid
Range labels move from mid-bar scatter points onto the markArea bands
(insideTopRight), so each label sits inside the range it names and the
rightmost label no longer clips at the chart edge. The tooltip stays
enabled regardless of the Show labels/legend toggles and the measure bar
tooltip names the range the value falls within. The grid top now
estimates wrapped legend rows from item text widths instead of assuming
a single row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:56:12 -07:00
Claude Code 4e715f66dc chore(migrations): re-id compare-chart migration and re-point bubble migration after rebase
Master's add_extension_storage_table migration landed with revision id
e5f6a7b8c9d0, colliding with the compare-chart migration's id; re-id it
to 88360afb61ed and chain the bubble migration off master's head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:52:46 -07:00
Claude Code 6dfae09cc5 chore: drop unused feature_flag_manager import after rebase
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:45 -07:00
Claude Code a4f74b09bb docs(embedded): document the iframe session cookie race in the pivot spec
Also retriggers CI so the merge ref picks up the master fix from #42274
(an empty commit does not trigger the path-gated workflows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Claude Code 9f39fe3e10 test(embedded): store a query_context on the pivot collapse fixture chart
The spec's fixture chart was created with params only. Real charts saved
through Explore always persist a query_context, and guest (embedded)
chart data requests are validated against the stored chart: with no
stored query_context and no columns/groupby params keys, the pivot
payload's query columns read as tampering and every request 403s.

The suite previously passed only when the embedded iframe happened to
ride the admin session from storageState instead of the guest token (a
cookie race between the /embedded response's anonymous session and the
SDK's csrf/guest-token fetches), which masked the 403 on master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Claude Code c6f8706aa1 test(charts): update warm-up cache assertions for the actionable message
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Claude Code 07a6406d97 style: prettier formatting for UPDATING.md and SavedQueries test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Claude Code a585a0901e fix(time-table): escape backslashes in flattened labels; actionable null-query-context errors
Escaping only commas let group tuples like (a\\, b) and (a-comma, b)
flatten to identical column keys; backslashes are now escaped first, with
a regression test pinning the collision pair. The cache warm-up error for
charts whose query context has not been generated yet tells the operator
how to resolve it, and UPDATING.md documents the one-time lazy generation
for charts migrated in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Evan RusackasandClaude Code fff5351a33 feat(bullet): rebuild the Bullet Chart on ECharts (#42225)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Evan RusackasandClaude Code b4f8d87a49 feat(time-pivot): rebuild Time-series Period Pivot on ECharts (#42245)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
Evan RusackasandClaude Code 3256ee4fbb feat(timeseries): percent-change rebasing with a draggable baseline (#42247)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:44 -07:00
EvanandClaude Opus 4.8 e5b3ba9efc fix: cast Arc test color accessors through unknown for tsc
lint-frontend's tsc build failed with TS2352 on the direct Accessor ->
ColorAccessor cast; go through unknown first as the compiler suggests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
EvanandClaude Opus 4.8 ae93dc2041 fix: repair type errors from removed useLegacyApi metadata field
Three plugin tests still asserted on ChartMetadata.useLegacyApi, which
this PR drops entirely, and the Arc layer test called deck.gl color
accessors without narrowing their Accessor union type. Both broke
lint-frontend's tsc check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 792d0fd2b6 fix(deck-arc): default missing layer colors instead of crashing
The Arc layer read color_picker/target_color_picker straight off form data
and dereferenced .r/.g/.b, so a saved chart without those controls crashed
the ArcLayer on init ("Cannot read properties of undefined (reading 'r')").
This surfaced in deck.gl Multiple Layers, where sub-slices arrive as raw
saved form data without control-default hydration and the Arc example never
persisted target_color_picker. Fall back to the control-panel default color
(PRIMARY_COLOR) when a picker is absent, matching how Path and Scatter
already guard, and harden Polygon's fill/stroke pickers the same way. Adds
Arc color-accessor tests for the missing and present cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 8dcb83fd3c feat(deck-multi): surface layer load failures in the chart
When a deck.gl Multiple Layers sub-layer fails to load (e.g. it is bound to
a dataset that lacks the columns it needs), the error previously only threw
to the browser console, leaving the map looking silently empty. Capture the
per-slice failure and render a warning Alert over the map, normalized via
getClientErrorObject so the server message (e.g. "Columns missing in
dataset") is shown. Errors reset on each reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 8f07ab4840 test(plugins): cover the migrated calendar/chord/country-map registration
Codecov flagged the calendar transformProps and the calendar/chord/
country-map plugin index modules as uncovered. Add plugin-registration
tests (instantiate the plugin, assert v1 metadata and the buildQuery
loader), a calendar transformProps test (v1 reshape plus display-option
passthrough, and the non-array passthrough branch), and a calendar
buildQuery case for an unrecognized subdomain granularity falling back to
minutes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 89d64b89b8 fix(deck-multi): query layers against their real datasource, not stale params
Each deck.gl Multiple Layers sub-layer is an independent saved chart with
its own dataset, and its layer query must hit that dataset. fetchSubslices
rebuilt the layer form_data from the saved params, whose `datasource`
string can be stale -- example charts hardcode a datasource id that does
not match the imported dataset's real id. The layer query then went to the
wrong dataset and the server rejected it with 400 "Columns missing in
dataset" (e.g. LON/LAT), so no layer rendered. Standalone the chart works
because it resolves its datasource from the slice relationship, not params.

Use the chart's authoritative datasource_id/datasource_type from the chart
API to build the layer datasource, falling back to params only when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 4ce589dcda fix(deck-multi): correct the points-collection typing to unblock tsc
The GET_POINTS_BY_VIZ_TYPE map annotated its values as
(features: JsonObject[]) => [number, number][], but the per-layer
getPoints helpers have looser, inconsistent signatures (getPointsGeojson
takes Point[]), so the map failed tsc. Replace it with an explicit-spread
collectPoints(features) mirroring the original getAdjustedViewport, which
type-checks and works for both the payload and the per-layer accumulator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code a31210df08 fix(deck-multi): autozoom to layer data in the v1 fetch path
deck.gl Multiple Layers computed its viewport once, from
props.payload.data.features, and never again. In the v1 path that payload
is empty (each layer is fetched client-side after mount), so autozoom had
no points to fit and the camera stayed at the default location while the
layers rendered off-screen -- the map showed but appeared to have no data.

Accumulate each layer's features as it is fetched and refit the viewport
to the combined points (when autozoom is enabled), mirroring what the
legacy pre-merged payload allowed. Also factor the per-viz_type point
collection into a shared helper. Adds a v1-path test covering both layer
rendering and the viewport refit, which the existing suite (all legacy
payload.data.slices) did not exercise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:43 -07:00
Claude Code 00e95dcb87 chore(paired-t-test): drop the unused prop-types dependency
Nothing in the plugin imports prop-types since the class components were
migrated to function components. Remove it and its lockfile reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code c9960002f1 fix(paired-t-test): bound the scroll container to the chart height
The container relied on height: 100%, which does not resolve because the
chart's parent is not height-bounded in the v1 render path, so tall output
(many groups across multiple metrics) still clipped without scrolling.
Thread the explicit height SuperChart passes via chartProps through
transformProps and apply it to the scroll container, matching the pattern
used by the pivot-table chart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code dbac244857 refactor(paired-t-test): use the antd Table and drop the reactable dependency
Replace the interim plain-table rendering with the design-system antd Table
from @superset-ui/core/components: native column sorting via the ported
comparators, per-cell lift/p-value/significance coloring through semantic
theme tokens (no bespoke CSS classes), and row-click control selection via
onRow. Removes the reactable dependency entirely (its fragile, React-18-
incompatible rendering was the production bug) and the leftover reactable-*
styling. Also drops the now-stale distributions/cephes lockfile entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code 4483cb09f0 fix(paired-t-test): scroll the chart when tables overflow its height
The container styled the non-existent .scrollbar-container class, so the
chart never scrolled and tall output (many groups across multiple metrics)
was clipped with no way to reach the rest. Make the chart root fill its
allotted height and scroll on overflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code 28d977bc0c fix(paired-t-test): render the table without the reactable dependency
The migrated chart returned correct data (verified end-to-end) yet showed
table headers with no body rows in production builds, while every jest
path rendered rows. The common factor was reactable, an unmaintained 2016
library that relies on React-18-incompatible legacy lifecycles
(componentWillMount/componentWillReceiveProps) and identifies its row
children by stringifying components -- fragile in a production bundle.

Replace it with a plain semantic table that reproduces every behavior
(control-row selection, per-cell lift/p-value/significance coloring, and
the same column sort comparators), reusing the existing CSS class hooks so
the surrounding styles are unchanged. Rendering is now deterministic and
fully unit-testable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
EvanandClaude Opus 4.8 b248c4d7c0 fix(tests): drop removed useLegacyApi prop from ChartRenderer test metadata
The legacy-pipeline removal deleted useLegacyApi from ChartMetadataConfig;
the enableNoResults test still passed it, breaking tsc in lint-frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code 3e9fdc4cdf test(paired-t-test): prove the v1 pipeline renders table rows end-to-end
Guards the "headers render but no body rows" failure mode by driving the
real transformProps -> PairedTTest pipeline with a realistic flat
/api/v1/chart/data timeseries response (main.birth_names shape): asserts
transformProps keys the reshaped data by metric label with one series per
group tuple, and that the rendered tables contain a body row per group,
including when optional precision/significance controls are absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:42 -07:00
Claude Code 387b742303 fix(explore): honor chart enableNoResults metadata in ChartRenderer
ChartRenderer computed the SuperChart enableNoResults prop purely from
server-pagination + AG Grid filter state (bypassNoResult) and never read
the chart plugin's own enableNoResults metadata. Charts that fetch their
own data and issue no top-level query, like deck.gl Multiple Layers, set
enableNoResults: false but still hit the "No results were returned for
this query" empty state in explore, which pre-empts their self-fetching
renderer from ever mounting.

AND the chart's enableNoResults metadata (defaulting to true) into
bypassNoResult so deck_multi and similar self-fetching charts render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 533f90e4c6 test(plugins): regression guards from the migration bug sweep
Adds low-risk import-and-assert tests across migrated plugins, guarding the
classes of bugs found while testing this branch:
- is_timeseries charts must expose a datetime control (calendar, horizon, rose,
  partition). Partition genuinely lacked one, so add sections.legacyTimeseriesTime
  to its control panel (same 500 as paired-t-test otherwise).
- deck.gl Multiple Layers must issue an empty query (self-fetches its layers).
- deck.gl Arc is timeseries only when time_grain_sqla is set.
- Time Pivot's numeric x-axis format must render a number, not the literal
  SMART_NUMBER string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 b887c30573 test(paired-t-test): mock the statistics module instead of removed distributions
Removing the distributions dependency left TTestTable.test.tsx mocking a module
the component no longer imports, so the p-value/significance tests failed. Mock
./statistics.studentTwoSidedPValue to a deterministic 0.02 instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 2cd369d02a fix(nvd3,controls): time-pivot x-axis number format + single-line async multiselect
- Time Pivot's x-axis is a numeric offset within the period (its x_axis_format
  defaults to SMART_NUMBER), but NVD3Vis ran it through getTimeFormatter, which
  made d3 print the literal string "SMART_NUMBER" on every tick. Format it as a
  number instead. (Pre-existing bug, bycatch.)
- SelectAsyncControl never forwarded the Select 'oneLine' prop, so many selected
  tags wrapped outside the control's fixed-height box (visible on deck.gl
  Multiple Layers). Pass oneLine for multi mode so tags collapse to a '+N' tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 4b3bbe19f6 fix(paired-t-test): compute p-values without the Node-only distributions dep
The chart imported 'distributions' to compute the Student's t p-value, but that
package references Node's Buffer global, which is only polyfilled in dev builds
-- so the chart rendered in dev and threw "ReferenceError: Buffer is not
defined" in production. Replace it with a self-contained, browser-safe
two-sided p-value via the regularized incomplete beta function, verified
against standard t-table critical values, and drop the dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 28eae5956e fix(deck-multi): render the map instead of the no-results empty state
deck.gl Multiple Layers issues no query of its own -- each layer is a saved
chart that fetches its data client-side, so buildQuery is intentionally empty.
Dropping useLegacyApi exposed the default enableNoResults=true, so the empty
query response now shows "No results were returned for this query" and the map
never renders. Set enableNoResults: false, matching the other self-fetching
charts (Select filter, DeckglLayerVisibility).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
Evan RusackasandClaude Opus 4.8 239e3a4323 fix(paired-t-test): restore the datetime column control
The paired t-test chart's buildQuery sends is_timeseries, so the backend
requires a datetime column, but its migrated control panel has no time
section. Creating the chart fails with "Datetime column not provided as part
table configuration and is required by this type of chart" with no way to
supply one. Add the shared legacyTimeseriesTime section, matching its sibling
timeseries charts (rose, horizon), so a granularity column can be selected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
EvanandClaude Opus 4.8 573054346f fix: rebase bubble-chart migration onto master's current alembic head
master added strip_metricsqlexpressions_from_ag_grid_params (d24e6b0a9c7f)
on top of shadow_live_row_indexes (8f3a1b2c4d5e) after this branch's
bubble-chart migration was last pointed at 8f3a1b2c4d5e, producing two
alembic heads and failing `db upgrade` across every integration/E2E job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:41 -07:00
EvanandClaude Opus 4.8 a42c7e3be0 fix: resolve oxlint unused-var/param errors and remove orphaned legacy test
Drops the dead `method` param from getChartDataRequest (the legacy
GET/POST branching it supported no longer exists now that chart data
always POSTs to /api/v1/chart/data), removes bubble-only NVD3Vis props
(entity/maxBubbleSize/xField/yField/sizeField) and the unused module-level
`formatter` left over from the bubble->bubble_v2 migration, and deletes
plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts, an
orphaned test for a src/ directory that no longer exists after the
rename to preset-chart-nvd3 (superseded by
plugins/preset-chart-nvd3/test/TimePivot.test.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:40 -07:00
EvanandClaude Opus 4.8 935b4acdbd fix: point bubble-chart migration at master's actual alembic head
down_revision on d4e5f6a7b8c9 pointed at b1c2d3e4f5a6
(add_subjects_tables), which already has a child on master
(56cd24c07170 -> 8f3a1b2c4d5e). That left two alembic heads and
broke `superset db upgrade` in every DB-backed CI job. Chain the
migration after 8f3a1b2c4d5e, master's real current head, instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:40 -07:00
EvanandClaude Opus 4.8 bba2d5f812 fix: resolve alembic multiple-heads conflict in migration chain
down_revision on d4e5f6a7b8c9 still pointed at 3a8e6f2c1b95, which
master's add_subjects_tables migration (b1c2d3e4f5a6) also forked
from, leaving two alembic heads and breaking `superset db upgrade`
in every DB-backed CI job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:40 -07:00
EvanandClaude Opus 4.8 7d45b8ae7e fix: format Multi.tsx and update Multi.color.test.tsx for registry-based layer loading
Multi.tsx now resolves sublayer buildQuery/transformProps via the chart
plugin registries instead of the removed legacy explore.ts helper; update
the color test's mocks to register deck_scatter/deck_arc stubs and mock
SupersetClient.post (matching the new fetch call) instead of .get.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:40 -07:00
EvanandClaude Opus 4.8 5942d570c3 fix: repair stale package-lock.json and alembic migration branch
- Remove dangling package-lock.json link entries for the removed
  legacy-* plugin workspaces and regenerate entries for their
  renamed plugin-chart-* counterparts; npm ci was failing with
  EUSAGE because the lockfile referenced workspace paths that no
  longer exist on disk.
- Re-parent the bubble-chart-to-echarts migration
  (d4e5f6a7b8c9) onto the current single migration head
  (3a8e6f2c1b95) instead of a now-superseded ancestor, resolving
  the "Multiple head revisions" alembic error that was failing
  test-sqlite/test-postgres/test-mysql/cypress/playwright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:40 -07:00
EvanandClaude Opus 4.8 45adb8429d fix: restore OAuth2RedirectError import after rebase
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:50:39 -07:00
Claude Code 3a03f0a820 docs: add UPDATING.md entries for the legacy viz pipeline removal and drop the working trackers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:39 -07:00
Evan RusackasandClaude Code 105f6d0bc7 chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline (#41750)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:39 -07:00
Evan RusackasandClaude Code c8d6faf7e8 chore(viz): drop the legacy- prefix from migrated chart plugin packages (#41751)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:35 -07:00
Claude Code ae15dd883d chore(viz): update SCOPE tracker — all charts migrated, phases 2-3 in review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code 2afa775db5 feat(compare): migrate compare charts to echarts_timeseries_line and drop the legacy plugin (#41738)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code e17d49bec0 feat(deck-multi): migrate deck_multi chart to the v1 chart data API (#41730)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code 68324050cd feat(bubble): migrate saved bubble charts to bubble_v2 and drop the legacy plugin (#41728)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code 2a55ad77f4 fix(legacy-viz): restore line-engine default ordering for horizon and rose (#41732)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code cd535f89e2 feat(partition): migrate partition chart to v1 chart data API (#41729)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Claude Code 8b2c07f5f3 chore(viz): update SCOPE tracker after tier 2 completion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code a9045296d9 feat(time-pivot): migrate time_pivot chart to v1 chart data API (#41727)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:34 -07:00
Evan RusackasandClaude Code 32cbc4a7a6 feat(rose): migrate rose chart to v1 chart data API (#41726)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 4f47eda5de feat(horizon): migrate horizon chart to v1 chart data API (#41725)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Claude Code ba2b0d6ea3 chore(viz): update SCOPE tracker after tier 1-2 merges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code df176cfa57 feat(time-table): migrate time_table chart to v1 chart data API (#41723)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 66e911eeea feat(world-map): migrate world_map chart to v1 chart data API (#41720)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 759457264d feat(calendar): migrate cal_heatmap chart to v1 chart data API (#41724)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 5722b96280 feat(paired-t-test): migrate paired_ttest chart to v1 chart data API (#41721)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code deb6cacea7 feat(chord): migrate chord chart to v1 chart data API (#41719)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Claude Code 0f8e2a274d chore(viz): update SCOPE tracker after phase 0 + tier 1 merges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 7e0de356d4 feat(bullet): migrate bullet chart to v1 chart data API (#41718)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:33 -07:00
Evan RusackasandClaude Code 4e93b58a61 feat(parallel-coordinates): migrate para chart to v1 chart data API (#41716)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:32 -07:00
Evan RusackasandClaude Code 45b232cba0 feat(country-map): migrate country_map chart to v1 chart data API (#41717)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:32 -07:00
Evan RusackasandClaude Code bcc58d27f8 chore(viz): phase 0 — remove orphaned viz.py classes and nvd3 BoxPlot leftovers (#41715)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-22 17:50:32 -07:00
Claude Code b5df673d21 chore(viz): add SCOPE/UPDATES trackers for legacy viz pipeline removal
Working documents for the remove-legacy-viz-pipeline feature branch;
stripped before final merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:50:32 -07:00
1016 changed files with 30827 additions and 2980 deletions
+1
View File
@@ -79,6 +79,7 @@ github:
- lint-check
- cypress-matrix-required
- dependency-review
- enforce-single-migration-head
- frontend-build
- playwright-tests-required
- pre-commit (current)
+9 -32
View File
@@ -1,22 +1,3 @@
# Notify all committers of DB migration changes, per SIP-59
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe @rusackas
# Notify some committers of changes in the components
/superset-frontend/src/components/Select/ @michael-s-molina @geido @kgabryje
/superset-frontend/src/components/MetadataBar/ @michael-s-molina @geido @kgabryje
/superset-frontend/src/components/DropdownContainer/ @michael-s-molina @geido @kgabryje
# Notify Helm Chart maintainers about changes in it
/helm/superset/ @dpgaspar @villebro @nytai @michael-s-molina @mistercrunch @rusackas @Antonio-RiveroMartnez @hainenber
# Notify E2E test maintainers of changes
/superset-frontend/playwright/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
/superset-frontend/cypress-base/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
# Notify PMC members of changes to GitHub Actions
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @sha174n @dpgaspar @sadpandajoe @hainenber
@@ -30,23 +11,19 @@
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @Antonio-RiveroMartnez
# Maps are a finicky contribution process we care about
# Maps are fragile and political. GeoJson edits MUST be made in the Jupyter notebook or they'll be overwritten.
**/*.geojson @villebro @rusackas
**/*.ipynb @villebro @rusackas
/superset-frontend/plugins/plugin-chart-country-map/ @villebro @rusackas
# Notify translation maintainers of changes to translations
/superset/translations/ @sfirke @rusackas @villebro @sadpandajoe @hainenber
# Notify PMC members of changes to extension-related files
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas
/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset-extensions-cli/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset-frontend/src/packages/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset-frontend/src/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset-frontend/src/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/docs/developer_docs/extensions/ @michael-s-molina @villebro @rusackas
/superset-extensions-cli/ @michael-s-molina @villebro @rusackas @sadpandajoe
/superset/extensions/ @michael-s-molina @villebro @rusackas @sadpandajoe
/superset-frontend/src/extensions/ @michael-s-molina @villebro @rusackas @sadpandajoe
# Notify PMC members of config changes e.g. feature flags
/superset/config.py @michael-s-molina @villebro @rusackas @sadpandajoe
+19 -9
View File
@@ -5,10 +5,6 @@ inputs:
description: 'Python version to set up. Accepts a version number, "current", or "next".'
required: true
default: 'current'
cache:
description: 'Cache dependencies. Options: pip'
required: false
default: 'pip'
requirements-type:
description: 'Type of requirements to install. Options: base, development, default'
required: false
@@ -43,17 +39,31 @@ runs:
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }}
- name: Install uv
if: inputs.install-superset == 'true'
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: ${{ steps.set-python-version.outputs.python-version }}
enable-cache: true
- name: Update apt package lists
# cache-apt-pkgs-action assumes a fresh `apt-cache` index (true on GitHub-hosted
# runners, not on all self-hosted/custom runner images), so refresh it explicitly
# or package lookups silently resolve to an empty list.
if: inputs.install-superset == 'true'
shell: bash
run: sudo apt-get update
- name: Install apt packages
if: inputs.install-superset == 'true'
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libldap2-dev libsasl2-dev
version: 1.0
- name: Install dependencies
env:
INPUT_INSTALL_SUPERSET: ${{ inputs.install-superset }}
INPUT_REQUIREMENTS_TYPE: ${{ inputs.requirements-type }}
run: |
if [ "$INPUT_INSTALL_SUPERSET" = "true" ]; then
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
pip install --upgrade pip setuptools wheel uv
if [ "$INPUT_REQUIREMENTS_TYPE" = "dev" ]; then
uv pip install --system -r requirements/development.txt
elif [ "$INPUT_REQUIREMENTS_TYPE" = "base" ]; then
+4 -1
View File
@@ -45,7 +45,10 @@ jobs:
python-version: "3.11"
- name: Install uv
run: pip install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.11"
enable-cache: true
- name: supersetbot bump-python -p "${{ github.event.inputs.package }}"
env:
+56 -7
View File
@@ -51,6 +51,53 @@ jobs:
echo "matrix_config=${MATRIX_CONFIG}" >> $GITHUB_OUTPUT
echo $GITHUB_OUTPUT
# Runs unconditionally (no dependency on `changes`, and no build-preset
# matrix restriction) so a regression in the PY_VER override logic is
# always caught on PRs. Without this, the real docker-build job only runs
# when the change detector flags docker/python/frontend changes (a
# workflow-only edit like this one does not), and even then the PR build
# matrix never includes the "py311"/"py312" presets that logic protects -
# so a break here would otherwise first surface on a push to master.
pyver-override-check:
name: verify docker build PY_VER override
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
- name: Assert PY_VER override applies to every preset except py311/py312
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Asserts against the actual buildx command line `supersetbot docker
# --dry-run` would run, not just this repo's own extra-flags helper,
# so a regression in supersetbot itself (dropping the py311/py312
# PY_VER pin, or reordering args so our override no longer lands
# last) is caught here too, instead of only surfacing on master.
assert_effective_py_ver() {
local preset="$1" expected="$2" extra_flags command actual
extra_flags="$(scripts/docker-build-extra-flags.sh "$preset" dummy-tag)"
command="$(supersetbot docker --preset "$preset" --platform linux/amd64 --extra-flags "$extra_flags" --dry-run)"
# docker buildx keeps the LAST value of a repeated --build-arg key.
actual="$(grep -oE -- '--build-arg PY_VER=[^[:space:]]+' <<<"$command" | tail -1)"
if [ "$actual" != "--build-arg PY_VER=$expected" ]; then
echo "::error::preset '$preset' expected effective --build-arg PY_VER=$expected, got: ${actual:-<none>} (full command: $command)"
exit 1
fi
}
for preset in dev lean websocket dockerize; do
assert_effective_py_ver "$preset" "3.11.14-slim-trixie"
done
assert_effective_py_ver py311 "3.11-slim-bookworm"
assert_effective_py_ver py312 "3.12-slim-bookworm"
echo "PY_VER override logic verified against the assembled buildx command for all build presets"
docker-build:
name: docker-build
needs: [setup_matrix, changes]
@@ -124,19 +171,21 @@ jobs:
# the whole job. buildx reuses the buildkit layer cache from the
# failed attempt, so a retry mostly re-does just the failed push.
#
# supersetbot's "dev"/"lean" presets pin their own --build-arg
# PY_VER, which lands ahead of --extra-flags on the assembled
# buildx command line; docker/buildx keeps the last value for a
# repeated --build-arg key, so appending PY_VER here overrides
# supersetbot's pin and keeps the build on the Dockerfile's own
# supported Python version.
# See scripts/docker-build-extra-flags.sh for why "py311"/"py312"
# are excluded from the PY_VER override applied to every other
# preset; that logic is also exercised on every PR by the
# always-on pyver-override-check job below, since this job itself
# only runs when the change detector flags docker/python/frontend
# changes and the PR build matrix never includes py311/py312.
EXTRA_FLAGS="$(scripts/docker-build-extra-flags.sh "$BUILD_PRESET" "$IMAGE_TAG")"
for attempt in 1 2 3; do
if supersetbot docker \
$PUSH_OR_LOAD \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg PY_VER=3.11.14-slim-trixie --build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
--extra-flags "$EXTRA_FLAGS" \
$PLATFORM_ARG; then
break
fi
@@ -0,0 +1,82 @@
# 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.
name: Enforce single Alembic migration head
on:
push:
branches:
- "master"
- "[0-9].[0-9]*"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
# No `paths:` filter on purpose: this job is a required status check, and a
# required check that never runs for a given PR blocks that PR from merging
# forever. It has to fire on every PR so it always reports a status; whether
# migrations changed is decided inside the job, not the trigger.
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
enforce-single-migration-head:
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for migration file changes
id: check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ github.token }}
script: |
if (context.eventName === 'push') {
core.setOutput('changed', 'true');
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const changed = files.some((f) => f.filename.startsWith('superset/migrations/'));
core.setOutput('changed', String(changed));
- name: Setup Python
if: steps.check.outputs.changed == 'true'
uses: ./.github/actions/setup-backend/
with:
requirements-type: base
- name: Assert a single Alembic head
if: steps.check.outputs.changed == 'true'
env:
SUPERSET__SQLALCHEMY_DATABASE_URI: "sqlite:///:memory:"
run: |
heads="$(superset db heads)"
echo "$heads"
head_count=$(printf '%s\n' "$heads" | grep -c .)
if [ "$head_count" -ne 1 ]; then
echo "::error::superset/migrations resolves to $head_count Alembic heads (expected exactly 1)."
echo "Another migration already landed with the same down_revision this branch was cut from."
echo "Add a no-op merge revision joining the heads: https://superset.apache.org/docs/contributing/development#merging-db-migrations"
exit 1
fi
@@ -0,0 +1,135 @@
name: Frontend bundle size (nightly baseline + analyzer)
# Refreshes the bundle-size baseline that superset-frontend.yml's `bundle-size`
# job compares PRs against, and publishes a browsable bundle-analyzer treemap
# report of the same build. Deliberately NOT triggered on every push to
# master: a day-old baseline/report is fine for catching relative
# regressions on PRs and for browsing what's actually in the bundle, and
# building the production bundle on every one of the many pushes master
# gets per day would burn CI time for no benefit a nightly refresh doesn't
# already cover.
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
TAG: apache/superset:bundle-size-nightly-${{ github.run_id }}
permissions:
contents: read
jobs:
refresh-baseline:
runs-on: ubuntu-26.04
timeout-minutes: 30
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_BUNDLE_ANALYZER_SITE_ID }}
steps:
- name: "Checkout master"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: master
- name: Build Docker Image
run: |
docker buildx build \
-t $TAG \
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
--target superset-node-ci \
.
# Same cache the PR-time bundle-size job restores/writes -- webpack's
# persistent filesystem cache turns a warm production build into ~20s
# instead of several minutes. See superset-frontend.yml for the
# matching restore step and why it's keyed this way.
- name: Restore webpack build cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: superset-frontend/.temp_cache
key: >-
webpack-prod-cache-${{ hashFiles('superset-frontend/package-lock.json',
'superset-frontend/babel.config.js', 'superset-frontend/tsconfig.json',
'superset-frontend/webpack.config.js') }}
# Only ever pull the last recorded data point off the cache, keyed by
# run ID -- `restore-keys` prefix-matches the most recently created
# entry. Absent on the very first run ever; benchmark-action starts a
# fresh history in that case.
- name: Restore bundle size history
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: bundle-size-history.json
key: bundle-size-history-${{ github.run_id }}
restore-keys: |
bundle-size-history-
# BUNDLE_ANALYZER rides along in the same build as BUNDLE_SIZE_STATS --
# they're independent env-gated additions in webpack.config.js (one
# sets `config.stats`, the other pushes plugins), so one production
# build produces both the numeric stats.json and the analyzer's
# report.html. Only report.html is mounted out, not
# BUNDLE_ANALYZER's sibling `statistics.html` sunburst -- that file is
# documented in webpack.config.js as routinely exceeding 100MB for
# this app (it's .gitignore'd for exactly that reason), too large to
# publish as a static site page.
- name: Build production bundle with stats and analyzer report
run: |
mkdir -p ${{ github.workspace }}/superset-frontend/bundle-stats
mkdir -p ${{ github.workspace }}/superset-frontend/.temp_cache
mkdir -p ${{ github.workspace }}/superset/static/assets
docker run \
-v ${{ github.workspace }}/superset-frontend/bundle-stats:/app/superset-frontend/bundle-stats \
-v ${{ github.workspace }}/superset-frontend/.temp_cache:/app/superset-frontend/.temp_cache \
-v ${{ github.workspace }}/superset/static/assets:/app/superset/static/assets \
--rm $TAG \
bash -c \
"npm i && BUNDLE_SIZE_STATS=true BUNDLE_ANALYZER=true npm run build -- --json=bundle-stats/stats.json"
- name: Summarize bundle size
run: |
node superset-frontend/scripts/bundle-size-summary.js \
superset-frontend/bundle-stats/stats.json > bundle-size-summary.json
rm -rf superset-frontend/bundle-stats
# No PR to comment on here, so comment-on-alert is off -- the job
# summary (summary-always) is the only surface for this run.
- name: Update bundle size baseline
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
with:
tool: customSmallerIsBetter
output-file-path: bundle-size-summary.json
external-data-json-path: bundle-size-history.json
fail-on-alert: false
summary-always: true
- name: Save bundle size history
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: bundle-size-history.json
key: bundle-size-history-${{ github.run_id }}
# Publishes the treemap to Netlify (the same host already used for
# superset-storybook.netlify.app and docs previews, reusing the
# existing NETLIFY_AUTH_TOKEN). Skipped until
# NETLIFY_BUNDLE_ANALYZER_SITE_ID exists -- create a new (free)
# Netlify site named superset-bundle-analyzer and add its site ID as
# that secret to turn this on; nothing else in this workflow depends
# on it.
- name: Publish bundle analyzer report to Netlify
if: ${{ env.NETLIFY_SITE_ID != '' }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
run: |
mkdir -p netlify-publish
cp superset/static/assets/report.html netlify-publish/index.html
# zizmor: ignore[adhoc-packages] - netlify-cli is a one-shot CI deploy
# tool, not an application dependency; a global/npx install has no
# lockfile context. Version pinned above the floor set by other
# ad-hoc installs in this repo (bump deliberately when upgrading).
npx --yes netlify-cli@27.0.1 deploy --prod --dir=netlify-publish
@@ -45,5 +45,8 @@ jobs:
- name: Run Script
run: bash .github/workflows/github-action-validator.sh
- name: Test docs-deploy freshness gate
run: bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh
- name: Check for security issues on GHA workflows
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
+20 -11
View File
@@ -53,6 +53,15 @@ jobs:
- name: Install helm-docs
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
# Spike: run the existing .pre-commit-config.yaml through prek (a Rust
# reimplementation of pre-commit) instead of pre-commit itself, to see
# whether it's viable to speed up this job. CI-only — contributors keep
# installing/running `pre-commit` locally exactly as documented; nothing
# here changes that.
- name: Install prek
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.4.11/prek-installer.sh | sh
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -70,13 +79,13 @@ jobs:
cd docs
yarn install --immutable
- name: Cache pre-commit environments
- name: Cache prek environments
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
key: prek-v1-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-
prek-v1-${{ runner.os }}-py${{ matrix.python-version }}-
- name: Determine changed files
id: changed_files
@@ -142,7 +151,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
fi
- name: pre-commit
- name: pre-commit (via prek)
env:
MODE: ${{ steps.changed_files.outputs.mode }}
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
@@ -152,22 +161,22 @@ jobs:
case "${MODE}" in
all)
echo "️ Running pre-commit on all files."
pre-commit run --all-files
echo "️ Running prek on all files."
prek run --all-files
;;
files)
echo "️ Running pre-commit on changed files:"
echo "️ Running prek on changed files:"
echo "${CHANGED_FILES}"
# shellcheck disable=SC2086
pre-commit run --files ${CHANGED_FILES}
prek run --files ${CHANGED_FILES}
;;
none)
echo "️ No source files changed; nothing for pre-commit to check."
echo "️ No source files changed; nothing for prek to check."
exit 0
;;
*)
echo "⚠️ Unrecognized changed-files mode '${MODE}'; checking all files."
pre-commit run --all-files
prek run --all-files
;;
esac
PRE_COMMIT_EXIT_CODE=$?
+49
View File
@@ -0,0 +1,49 @@
#!/bin/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.
#
# Shared freshness gate used by the Docs Deployment workflow
# (superset-docs-deploy.yml) both up front (check-freshness) and again right
# before the deploy step (recheck-freshness). Writes an output declaring
# whether BUILD_SHA is still master's current tip, so a superseded run can
# skip cleanly instead of racing (and clobbering, or being force-cancelled
# by) a fresher run.
#
# Required env vars:
# BUILD_SHA - the commit SHA this run is building
# REPO - "owner/repo" to query, e.g. github.repository
# OUTPUT_NAME - the GITHUB_OUTPUT key to write, e.g. "is-current"
# GITHUB_OUTPUT - path to append outputs to (set by the Actions runner)
# Optional env vars:
# EVENT_NAME - if "workflow_dispatch", bypasses the check and always
# reports current, since a manual dispatch is a deliberate,
# one-off action rather than something racing other triggers
# GH_TOKEN - passed through to `gh`, needed to call the GitHub API
set -euo pipefail
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then
echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT"
exit 0
fi
latest_sha="$(gh api "repos/${REPO}/commits/master" --jq .sha)"
if [ "${latest_sha}" = "${BUILD_SHA}" ]; then
echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT"
else
echo "${OUTPUT_NAME}=false" >>"$GITHUB_OUTPUT"
echo "::notice::master has moved on to ${latest_sha} since ${BUILD_SHA} was triggered — skipping this stale run."
fi
@@ -0,0 +1,100 @@
#!/bin/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.
#
# Exercises check-docs-deploy-freshness.sh against a stubbed `gh`, covering
# the dispatch-bypass, current-tip and stale-tip branches so the output
# contract (is-current / still-current) can't silently regress. Run
# directly, no extra tooling required:
# bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
script_under_test="${script_dir}/check-docs-deploy-freshness.sh"
failures=0
# Runs the script under test with a stubbed `gh` reporting $1 as master's
# latest sha, asserting that GITHUB_OUTPUT ends up containing exactly $4.
run_case() {
local case_name="$1"
local latest_sha="$2"
local build_sha="$3"
local event_name="$4"
local expected_line="$5"
local workdir
workdir="$(mktemp -d)"
trap 'rm -rf "${workdir}"' RETURN
# Fake `gh` that just echoes back the requested "latest" sha regardless of
# arguments, so the script under test never touches the network.
cat >"${workdir}/gh" <<EOF
#!/bin/bash
echo '${latest_sha}'
EOF
chmod +x "${workdir}/gh"
local output_file="${workdir}/github_output"
: >"${output_file}"
if PATH="${workdir}:${PATH}" \
GITHUB_OUTPUT="${output_file}" \
OUTPUT_NAME="is-current" \
REPO="apache/superset" \
BUILD_SHA="${build_sha}" \
EVENT_NAME="${event_name}" \
GH_TOKEN="fake-token" \
bash "${script_under_test}"; then
:
else
echo "FAIL (${case_name}): script exited non-zero"
failures=$((failures + 1))
return
fi
local actual
actual="$(cat "${output_file}")"
if [ "${actual}" = "${expected_line}" ]; then
echo "PASS (${case_name})"
else
echo "FAIL (${case_name}): expected '${expected_line}', got '${actual}'"
failures=$((failures + 1))
fi
}
# `gh` prints "should-not-be-called" for the dispatch case above the trick:
# it's never actually invoked since the bypass short-circuits before the
# `gh api` call, but the fake still needs a body.
run_case "workflow_dispatch bypasses the check" \
"unused" "abc123" "workflow_dispatch" \
"is-current=true"
run_case "build sha matches master's tip" \
"abc123" "abc123" "push" \
"is-current=true"
run_case "build sha is stale" \
"def456" "abc123" "push" \
"is-current=false"
if [ "${failures}" -gt 0 ]; then
echo "${failures} case(s) failed"
exit 1
fi
echo "All cases passed"
+75 -12
View File
@@ -18,16 +18,6 @@ on:
workflow_dispatch: {}
# Serialize deploys: the action pushes to apache/superset-site without
# rebasing, so concurrent runs race on the final push and the loser fails
# with `! [rejected] asf-site -> asf-site (fetch first)`. Cancel any
# in-progress run as soon as a newer one starts — the destination repo
# isn't touched until the final push step, so canceling mid-build is safe,
# and the freshest content always wins.
concurrency:
group: docs-deploy-asf-site
cancel-in-progress: true
permissions:
contents: read
actions: read
@@ -48,17 +38,69 @@ jobs:
env:
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
# Master gets frequent, sometimes bursty pushes, and each one can trigger a
# deploy attempt. Rather than let every superseded attempt get force-killed
# by the build-deploy concurrency group below (which shows up as a
# `cancelled` — i.e. red/failing-looking — check on that commit), have each
# run check up front whether it's still building master's current tip and,
# if not, skip cleanly. Deliberately outside the docs-deploy-asf-site
# concurrency group so it runs immediately for every trigger without
# blocking or being blocked by anything.
check-freshness:
runs-on: ubuntu-26.04
outputs:
is-current: ${{ steps.check.outputs.is-current }}
steps:
# Sparse checkout: this job's only job is to be fast, so it fetches
# nothing but the freshness-check script itself.
- name: Checkout freshness-check script
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
.github/workflows/scripts
sparse-checkout-cone-mode: false
- name: "Check whether this is still master's current commit"
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
OUTPUT_NAME: is-current
run: .github/workflows/scripts/check-docs-deploy-freshness.sh
build-deploy:
needs: config
needs: [config, check-freshness]
# Only the run for master's current tip proceeds; anything superseded
# already skipped at check-freshness above instead of landing here.
# For workflow_run triggers, only deploy when the triggering run originated
# from this repository (not a fork), ensuring the checked-out code and any
# local actions executed with deploy credentials are trusted.
if: >-
needs.config.outputs.has-secrets &&
needs.check-freshness.outputs.is-current == 'true' &&
(github.event_name != 'workflow_run' ||
github.event.workflow_run.head_repository.full_name == github.repository)
name: Build & Deploy
runs-on: ubuntu-26.04
# Serialize deploys: the action pushes to apache/superset-site without
# rebasing, so concurrent runs race on the final push and the loser fails
# with `! [rejected] asf-site -> asf-site (fetch first)`. Queue instead of
# canceling: a run that already passed check-freshness can still be
# sitting in the queue for a runner when a newer run starts and finishes
# first. cancel-in-progress would let that stale, queued run kill the
# newer run's in-progress deploy the moment it's finally scheduled, and
# then skip itself at the re-check below — losing the deploy entirely.
# Queuing means the stale run just waits its turn and then no-ops at the
# re-check, so the fresher content that already deployed is never
# clobbered or lost. The check-freshness gate above means it should be
# rare for more than one run to reach this point, so the queue stays
# short in practice.
concurrency:
group: docs-deploy-asf-site
cancel-in-progress: false
steps:
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
with:
@@ -81,7 +123,11 @@ jobs:
distribution: "zulu"
java-version: "21"
- name: Install Graphviz
run: sudo apt-get install -y graphviz
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: graphviz
version: 1.0
execute_install_scripts: true
- name: Compute Entity Relationship diagram (ERD)
env:
SUPERSET_SECRET_KEY: not-a-secret
@@ -126,7 +172,24 @@ jobs:
working-directory: docs
run: |
yarn build
# The check-freshness job above narrows the window but doesn't close it: an
# older run can observe is-current=true, then sit through this build while a
# newer run's own freshness check also passes and it deploys and finishes
# first. If this (stale) run then wins entry into the concurrency group, it
# would overwrite the newer content that already deployed. Re-check right
# before the one step that actually mutates superset-site, so a stale run
# skips deploying instead of clobbering a fresher one that already ran.
- name: "Re-check freshness immediately before deploying"
id: recheck-freshness
if: github.event_name != 'workflow_dispatch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
REPO: ${{ github.repository }}
OUTPUT_NAME: still-current
run: .github/workflows/scripts/check-docs-deploy-freshness.sh
- name: deploy docs
if: github.event_name == 'workflow_dispatch' || steps.recheck-freshness.outputs.still-current == 'true'
uses: ./.github/actions/github-action-push-to-another-repository
env:
API_TOKEN_GITHUB: ${{ secrets.SUPERSET_SITE_BUILD }}
+97
View File
@@ -212,3 +212,100 @@ jobs:
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
with:
expand-composite-actions: true
# Compares a PR's own bundle size against the last nightly-recorded
# baseline (see frontend-bundle-size-nightly.yml, which owns actually
# persisting new baselines). PR-only: a push to master doesn't need this
# check re-run against itself, and re-persisting the baseline on every
# push to master -- which happens many times a day -- would burn a full
# production build for no benefit nightly refresh doesn't already cover.
bundle-size:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true' && github.event_name == 'pull_request'
runs-on: ubuntu-26.04
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Download Docker Image Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
- name: Load Docker Image
run: |
zstd -d < docker-image.tar.zst | docker load
# webpack's persistent filesystem cache (superset-frontend/webpack.config.js)
# turns a warm production build into ~20s instead of several minutes,
# but GH-hosted runners are fresh VMs with nothing carried over between
# jobs -- without restoring it explicitly, every single PR would pay
# the full cold-build cost. Keyed on the same files webpack's own
# `buildDependencies` invalidates on, so a stale cache is never used.
- name: Restore webpack build cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: superset-frontend/.temp_cache
key: >-
webpack-prod-cache-${{ hashFiles('superset-frontend/package-lock.json',
'superset-frontend/babel.config.js', 'superset-frontend/tsconfig.json',
'superset-frontend/webpack.config.js') }}
# Only ever pull the last recorded data point off the cache, keyed by
# run ID -- `restore-keys` prefix-matches the most recently created
# entry, which is always the latest nightly run. Absent before the
# first nightly run ever happens; benchmark-action starts a fresh
# history in that case.
- name: Restore bundle size history
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: bundle-size-history.json
key: bundle-size-history-${{ github.run_id }}
restore-keys: |
bundle-size-history-
- name: Build production bundle with stats
run: |
mkdir -p ${{ github.workspace }}/superset-frontend/bundle-stats
mkdir -p ${{ github.workspace }}/superset-frontend/.temp_cache
docker run \
-v ${{ github.workspace }}/superset-frontend/bundle-stats:/app/superset-frontend/bundle-stats \
-v ${{ github.workspace }}/superset-frontend/.temp_cache:/app/superset-frontend/.temp_cache \
--rm $TAG \
bash -c \
"npm i && BUNDLE_SIZE_STATS=true npm run build -- --json=bundle-stats/stats.json"
- name: Summarize bundle size
run: |
node superset-frontend/scripts/bundle-size-summary.js \
superset-frontend/bundle-stats/stats.json > bundle-size-summary.json
rm -rf superset-frontend/bundle-stats
# Comparison + alert only -- this job never persists. See
# frontend-bundle-size-nightly.yml for why.
#
# comment-on-alert is gated to same-repo PRs: on a fork PR,
# GITHUB_TOKEN is forced read-only regardless of the `permissions`
# block above, so once the alert threshold is crossed the action's
# `pulls.createReview` call 403s. That error isn't gated by
# fail-on-alert (which only governs the deliberate alert-threshold
# failure) -- it propagates and fails the job outright. Fork PRs
# still get the comparison via the job summary (summary-always).
- name: Compare bundle size against nightly baseline
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
with:
tool: customSmallerIsBetter
output-file-path: bundle-size-summary.json
external-data-json-path: bundle-size-history.json
github-token: ${{ secrets.GITHUB_TOKEN }}
comment-on-alert: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
alert-threshold: "110%"
fail-on-alert: false
summary-always: true
+11
View File
@@ -155,6 +155,17 @@ jobs:
INCLUDE_EMBEDDED: "true"
with:
run: playwright-run "${{ matrix.app_root }}" embedded
- name: Run Playwright (Mobile Tests)
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
# Scoped to this step for the same reason as the embedded flags
# above: the mobile consumption mode should not alter Flask's
# configuration for the required desktop test steps.
SUPERSET_FEATURE_MOBILE_CONSUMPTION_MODE: "true"
INCLUDE_MOBILE: "true"
with:
run: playwright-run "${{ matrix.app_root }}" mobile/
- name: Set safe app root
if: failure()
id: set-safe-app-root
@@ -0,0 +1,70 @@
name: Python Unit Test Results
on:
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes artifacts uploaded by Python-Unit; never checks out PR code (see note below)
workflow_run:
workflows: ["Python-Unit"]
types: [completed]
# This workflow publishes a check run annotating failing Python unit tests
# inline on the PR diff, using JUnit XML uploaded by the Python-Unit workflow.
# It uses the workflow_run trigger so that it always runs in the base-branch
# context and can safely be granted write permissions, even for PRs from
# forks or Dependabot.
#
# IMPORTANT: This workflow must NEVER check out code from the PR branch. All
# data comes from artifacts uploaded by the Python-Unit workflow.
permissions:
contents: read
checks: write
issues: read
actions: read
jobs:
report:
runs-on: ubuntu-26.04
timeout-minutes: 10
if: >
github.event.workflow_run.conclusion == 'success' ||
github.event.workflow_run.conclusion == 'failure'
steps:
# Fails soft (continue-on-error) because the source unit-tests job is
# itself gated on change detection: a docs-only PR skips it entirely,
# so there is nothing to download or report on.
- name: Download JUnit results
id: download
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
# merge-multiple is intentionally omitted: each matrix leg's
# artifact (junit-results-current, junit-results-next) uses the
# same XML filenames, so merging them into one directory would let
# one Python version's results overwrite the other's. Downloading
# into per-artifact subdirectories keeps both, and the glob below
# is recursive so it still picks up every XML file.
pattern: "junit-results-*"
path: artifacts
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download event file
id: download-event
if: steps.download.outcome == 'success'
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: "Event File"
path: event
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish test results
if: steps.download.outcome == 'success' && steps.download-event.outcome == 'success'
uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0
with:
commit: ${{ github.event.workflow_run.head_sha }}
event_file: event/event.json
event_name: ${{ github.event.workflow_run.event }}
files: "artifacts/**/*.xml"
check_name: "Python Unit Test Results"
comment_mode: "off"
+30 -3
View File
@@ -74,14 +74,14 @@ jobs:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50
pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear --maxfail=50 --junit-xml=test-results/junit-unit.xml
- name: Python 100% coverage unit tests
env:
SUPERSET_TESTENV: true
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-sql-coverage.xml
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100 --junit-xml=test-results/junit-semantic-layers-coverage.xml
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
@@ -89,6 +89,33 @@ jobs:
verbose: true
use_oidc: true
slug: apache/superset
# Uploaded even when a pytest step above fails, since that is exactly
# when the JUnit results are needed downstream, to annotate the PR with
# the failing tests. Consumed by the "Python Unit Test Results" workflow
# via workflow_run (see that workflow for why it can't just be a step
# here: it needs to run with write permissions, which this PR-triggered
# job can't safely have on a fork PR).
- name: Upload JUnit test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: junit-results-${{ matrix.python-version }}
path: test-results/
retention-days: 7
# Uploads the raw pull_request event payload so the "Python Unit Test
# Results" workflow (running via workflow_run, in base-branch context) can
# look up which PR/commit to annotate without checking out untrusted code.
event-file:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Upload event file
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: Event File
path: ${{ github.event_path }}
retention-days: 7
# Stable required-status-check anchor. `unit-tests` is a matrix job gated on
# change detection, so on non-Python PRs it is skipped and never produces its
+4 -1
View File
@@ -78,7 +78,10 @@ jobs:
- name: Install gettext tools
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
run: sudo apt-get update && sudo apt-get install -y gettext
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: gettext
version: 1.0
# Fetch the base ref so we can compare PR-introduced regressions
# against a fair baseline (also runs babel_update against the base
+1 -1
View File
@@ -167,7 +167,7 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
### Generator Location
- Script: `docs/scripts/generate-superset-components.mjs`
- Wrapper: `docs/src/components/StorybookWrapper.jsx`
- Output: `docs/developer_portal/components/`
- Output: `docs/developer_docs/components/`
## Architecture Patterns
+1 -1
View File
@@ -35,4 +35,4 @@ The Developer Portal includes comprehensive guides for:
- [Code Review Process](https://superset.apache.org/developer_portal/contributing/code-review)
- [Development How-tos](https://superset.apache.org/developer_portal/contributing/howtos)
Source for the Developer Portal documentation is [located here](https://github.com/apache/superset/tree/master/docs/developer_portal).
Source for the Developer Portal documentation is [located here](https://github.com/apache/superset/tree/master/docs/developer_docs).
+2
View File
@@ -31,6 +31,8 @@ under the License.
[![Open PRs](https://img.shields.io/github/issues-pr/apache/superset)](https://github.com/apache/superset/pulls)
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://bit.ly/join-superset-slack)
[![Documentation](https://img.shields.io/badge/docs-apache.org-blue.svg)](https://superset.apache.org)
[![Storybook](https://img.shields.io/badge/storybook-live-ff4785.svg)](https://superset-storybook.netlify.app)
[![Bundle Analyzer](https://img.shields.io/badge/bundle%20analyzer-nightly-8dd6f9.svg)](https://superset-bundle-analyzer.netlify.app)
<picture width="500">
<source
+77
View File
@@ -24,8 +24,16 @@ assists people when migrating to a new version.
## Next
### OAuth2 database callback metrics include their outcome
The unqualified `DatabaseRestApi.oauth2` StatsD counter has been replaced with
`DatabaseRestApi.oauth2.success`, `DatabaseRestApi.oauth2.warning`, and
`DatabaseRestApi.oauth2.error`. Update monitoring rules and dashboards that consume
the old counter to use the outcome-specific replacements.
- [42935](https://github.com/apache/superset/pull/42935): The MCP service now refuses to start (`MCPAuthConfigError`) when `MCP_JWT_ISSUER` trusts more than one issuer and no `MCP_USER_RESOLVER` is configured, instead of only logging a warning. This was already a documented misconfiguration (the default resolver isn't issuer-scoped, so distinct trusted issuers minting the same username/email would resolve to the same Superset user); deployments trusting multiple issuers must configure an `MCP_USER_RESOLVER` that derives its identity from the token's `iss` claim before upgrading. Single-issuer deployments are unaffected.
- [42393](https://github.com/apache/superset/pull/42393): Exported dataset YAML now carries a `uuid` for each metric and column so that custom folder assignments (which reference metrics/columns by UUID) survive an import into another workspace. This affects any export bundle that contains datasets, not just a dataset export: chart, dashboard, database and full-asset exports all embed the same dataset YAML, so a dashboard exported from this release also fails to import into an older one even though no dataset was exported directly. As with `folders` and `currency_code_column`, the affected `datasets/` files fail schema validation (`Unknown field: uuid`) when imported into Superset releases that predate this change; regenerate or hand-edit exports for older targets in mixed-version fleets.
- [42300](https://github.com/apache/superset/pull/42300): Timeseries charts (line/area/bar) with a Y-axis bound in effect — either an explicit `yAxisBounds` or one derived from `truncateYAxis` — now clamp out-of-range data points to that bound instead of letting ECharts drop the point (and the line segments around it) entirely. Any existing chart with a configured Y-axis bound and data outside it will look different after upgrading: a gap becomes a point pinned to the boundary. The clamp also rewrites the value ECharts reads for that point's tooltip and data label, so the displayed value is the bound rather than the true observation.
- [42087](https://github.com/apache/superset/pull/42087): Stored calculated-column and metric expressions are validated when a query is built, under the same sub-query policy already applied to adhoc expressions. Previously only the dataset update path checked them on save, so expressions written by v1 import, by dataset duplication, or before that check existed were never validated. Since `ALLOW_ADHOC_SUBQUERY` defaults to `False` (see [19242](https://github.com/apache/superset/pull/19242)), a dataset whose stored expression contains a sub-query works before upgrading and afterwards fails at chart render with `Custom SQL fields cannot contain sub-queries.` There is no migration step, and the error does not name the offending dataset column, so audit stored expressions before upgrading: either rewrite them without the sub-query, or set `ALLOW_ADHOC_SUBQUERY = True` to keep the previous behaviour for both stored and adhoc expressions.
### Selenium support removed — Playwright is now required for screenshots
@@ -54,6 +62,43 @@ pip install playwright && playwright install chromium
2. Remove any references to the removed config keys from custom `superset_config.py`
3. If you subclassed `MachineAuthProvider`, remove any `authenticate_webdriver` override and migrate auth logic to `authenticate_browser_context`
### CSV/XLSX report exports of Table charts keep raw numeric values
Table and Pivot Table charts sent as text in a report email now apply the
chart's number and currency formatting so the values match what a user sees in
Explore. As part of this, the CSV and XLSX result formats return early before
formatting: previously the Table post-processor applied `d3NumberFormat` to
every result format, so CSV/XLSX exports contained pre-formatted strings.
CSV/XLSX exports now preserve numeric values and column types, which is better
for downstream analysis but is a visible change for anyone who relied on the
formatted text in those files. The rendered email body (the only place the
formatting is intended for) is unaffected.
### SQLAlchemy bumped to 2.0, flask-sqlalchemy to 3.1.1
Superset's core ORM dependencies move from SQLAlchemy 1.4 to 2.0 and
flask-sqlalchemy `<3.0` to 3.1.1, completing the migration tracked in
[discussion #40273](https://github.com/apache/superset/discussions/40273).
**Custom `db_engine_specs`, plugins, or extensions that import SQLAlchemy
internals directly** should review the
[SQLAlchemy 1.4-to-2.0 migration guide](https://docs.sqlalchemy.org/en/20/changelog/migration_20.html)
for API changes that affect them — most 1.4 code already runs unmodified
under 2.0's compatibility mode, but patterns like `Engine.execute()`,
string-keyed `Row` access, and `MetaData(bind=)` are removed outright.
**Several optional DB-connector extras remain capped below their
SQLAlchemy-2.0-only releases**, either because that bump is a separate
follow-up ([#42891](https://github.com/apache/superset/pull/42891): dremio,
exasol, firebird, redshift, risingwave) or because the upstream dialect
package has no SQLAlchemy 2.0 support yet at all (aurora-data-api, d1,
kusto, solr; ocient's 2.0 compatibility is unverified). Installing one of
these extras continues to pull a SQLAlchemy-1.4-line version of that
dialect; each package's constraint in `pyproject.toml` documents why.
No application-level configuration changes are required for deployments
that don't touch SQLAlchemy directly.
### Soft delete is on by default, and purging is live
`SOFT_DELETE` now ships **on** (`DEFAULT_FEATURE_FLAGS`), so deleting a
@@ -159,6 +204,36 @@ will now get a TypeScript error and must remove the prop; keeping a manual
override was exactly the footgun this change removes (see #42510). No
callers in the Superset frontend codebase itself passed this prop.
### Row-level security now filters table reads a same-named CTE used to hide
`extract_tables_from_statement()` decided whether a reference was a CTE by matching its
bare name against the enclosing scope's CTE names; it now resolves the name through
`Scope.cte_sources`. Three kinds of real table read whose bare name collided with a CTE's
were mistaken for the CTE and dropped from a statement's tables, so they were neither
RLS-filtered nor access-checked: a schema- or catalog-qualified reference, a non-recursive
CTE's own name inside its body, and a forward reference to a later `WITH` item.
```sql
WITH orders AS (SELECT 1 AS d) SELECT * FROM (SELECT * FROM public.orders) AS z
WITH orders AS (SELECT * FROM orders) SELECT * FROM orders
WITH q1 AS (SELECT key FROM q2), q2 AS (SELECT 1 AS key) SELECT * FROM q1
```
Each read is now reported, so it is filtered when `RLS_IN_SQLLAB` is enabled, matched
against `DISALLOWED_SQL_TABLES`, and requires dataset access under
`raise_for_access(force_dataset_match=True)`. A query that previously ran, reading those
rows unfiltered, may now be filtered or rejected. There is no opt-out — the previous
behavior was a row-level-security bypass.
### Table aliases keep their quoting through the row-level security rewrite
Both RLS transformers took the table alias as a string with its quoting stripped and
emitted it verbatim; they now carry the parsed identifier. Emitted SQL is unchanged for an
unquoted identifier; a quoted one keeps its quoting, and a column-alias list
(`FROM t AS x (c1, c2)`) survives the rewrite instead of being dropped. This repairs
row-level security for any aliased table on Snowflake, and for at least one statement shape
on MSSQL where the rewrite previously raised `AttributeError`.
### Principal listing APIs now honour related-field filters
Two authorization-related listing behaviors changed for API clients. Neither
@@ -828,6 +903,8 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
- [42089](https://github.com/apache/superset/pull/42089) automatically upgrades resolvable Slack v1 recipients, preserves text-only v1 delivery with execution warnings when migration cannot finish, and rejects retired v1 file uploads with actionable scope guidance. Slack delivery uses at-most-once terminal writes and a schedule-wide retry budget configured by `SLACK_SEND_RETRY_MAX_TIME`, clamped to the report's remaining working timeout. Deployments using `SupersetMetastoreCache` for the Slack channel cache must schedule the `slack.cache_channels` Celery task to repopulate misses outside report transactions; see [Alerts and Reports](https://superset.apache.org/admin-docs/configuration/alerts-reports#slack-delivery-timeouts-and-retries).
### Soft delete and restore for dashboards
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dashboard/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
+1
View File
@@ -118,6 +118,7 @@ FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"MOBILE_CONSUMPTION_MODE": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
@@ -83,6 +83,28 @@ SLACK_CACHE_TIMEOUT = int(timedelta(days=2).total_seconds())
SLACK_API_RATE_LIMIT_RETRY_COUNT = 5
```
When the cache backend is `SupersetMetastoreCache`, report execution does not
write channel listings into the cache because that backend commits the report's
database session. Schedule the dedicated warm-up task so cache misses are
repopulated outside report transactions:
```python
from celery.schedules import crontab
from superset.config import CeleryConfig
class CustomCeleryConfig(CeleryConfig):
beat_schedule = {
**CeleryConfig.beat_schedule,
"slack.cache_channels": {
"task": "slack.cache_channels",
"schedule": crontab(minute="0", hour="*"),
},
}
CELERY_CONFIG = CustomCeleryConfig
```
#### Slack Enterprise Grid (org-scoped tokens)
On a Slack Enterprise Grid org, an org-scoped token spans multiple workspaces, so
@@ -98,6 +120,38 @@ SLACK_TEAM_ID = "T01234567"
This defaults to `None` and only needs to be set when using an org-scoped token;
it is accepted but ignored for standard workspace-level tokens.
#### Slack delivery timeouts and retries
Slack delivery uses a request timeout and an application retry budget:
```python
# Timeout for one Slack API request, in seconds
SLACK_API_TIMEOUT = 30
# Retry budget shared by every Slack destination and upload phase
SLACK_SEND_RETRY_MAX_TIME = 150
# Number of explicit HTTP 429 responses retried using Slack's Retry-After value
SLACK_API_RATE_LIMIT_RETRY_COUNT = 2
# Cooldown after an on-demand channel-cache refresh
SLACK_CHANNEL_REFRESH_COOLDOWN_SECONDS = 300
```
All channels and upload phases in one report execution share a single
`SLACK_SEND_RETRY_MAX_TIME` budget. This prevents a large recipient list from
multiplying the report's wall-clock retry time. The budget is also clamped to
the report's remaining working timeout, leaving Celery's configured timeout lag
available for final state persistence. The effective configured budget is at
least one second longer than `SLACK_API_TIMEOUT`.
To avoid posting the same report twice, Superset does not replay terminal
`chat.postMessage` or `files.completeUploadExternal` operations after ambiguous
server or transport failures. Explicit Slack HTTP 429 responses remain
retryable. These delivery settings and semantics apply to Slack v2 reports and
legacy text-only Slack delivery, independently of the
`ALERT_REPORT_SLACK_V2` feature flag.
### Webhook integration
Superset can send alert and report notifications to any HTTP endpoint — useful for chat platforms, incident management tools, or custom automation.
@@ -0,0 +1,184 @@
---
title: Dashboard Performance
hide_title: true
sidebar_position: 5
version: 1
---
<!--
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.
-->
# Dashboard Performance
A dashboard's perceived speed is determined by three independent things: how
many charts have to render, how many queries the backend can execute
concurrently, and how quickly the underlying data warehouse can return
results. Superset gives you levers for the first two; the third belongs to
your warehouse. This page covers the dashboard-side levers and the practical
guidance around them.
## Is there a maximum chart count per dashboard?
**No hard limit is enforced** — Superset has no configuration key that
caps the number of charts on a dashboard. In practice, dashboards behave
well up to a few dozen charts. Beyond that, you'll typically feel friction
on the initial load and during cross-filter / time-range updates, even with
the lazy-loading optimizations described below.
Rough thresholds to keep in mind:
- **Under ~25 charts**: usually no perceptible problem.
- **2550 charts**: still fine, but you start to want tabs to break the
page into chunks the user actually looks at.
- **Over ~50 charts**: split into multiple dashboards or use tabs
aggressively. The bottleneck is rarely Superset itself — it's the
warehouse executing dozens of queries in parallel and the browser
rendering dozens of chart frames.
These are guidelines, not guarantees. A dashboard of 100 sparkline-style
charts hitting a fast cache behaves very differently from a dashboard of
20 heavy aggregations against a cold warehouse.
## Lazy rendering — `DASHBOARD_VIRTUALIZATION`
Superset's dashboard layout is virtualized at the row level. Charts that
are far below the user's current scroll position render a placeholder
instead of their visualization until the user scrolls them into view, and
go back to a placeholder if scrolled well past. The chart component itself
stays mounted throughout — only the visualization is swapped for a
placeholder — so this alone does **not** reduce backend query load; see
[Deferred data fetch](#deferred-data-fetch--dashboard_virtualization_defer_data)
below for that. This is on by default.
**Feature flag**: `DASHBOARD_VIRTUALIZATION` (default: `True`)
The flag is `stable` and marked for path-to-deprecation — meaning the
behavior will eventually be non-optional, but the flag still exists so
operators can disable it if a specific layout misbehaves.
**Behavior** (from `superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx`):
- A chart's visualization is rendered when its row scrolls within **1
viewport height** of the visible area.
- A chart's visualization is swapped back for a placeholder when its row
scrolls more than **4 viewport heights** away from the visible area.
- Tabs that aren't currently selected don't render their content at all
(see below).
- The placeholder-swap-back is skipped in **embedded** mode (so an
embedded dashboard keeps its charts rendered once they've been seen,
which avoids re-rendering on scroll-up). Both halves are skipped for
**headless / bot** rendering (so screenshot / report jobs load every
chart).
## Deferred data fetch — `DASHBOARD_VIRTUALIZATION_DEFER_DATA`
By default, `DASHBOARD_VIRTUALIZATION` only controls whether a chart's
*visualization* is rendered — the chart component still mounts and issues
its data request immediately, regardless of scroll position.
`DASHBOARD_VIRTUALIZATION_DEFER_DATA` is a supplementary flag that skips
the data request itself for charts that aren't currently in view, useful
for backends where opening a connection or compiling a query is expensive
even if the result would be thrown away. It only has an effect when
`DASHBOARD_VIRTUALIZATION` is also enabled — with virtualization off,
every chart is treated as in view, so there's nothing left to defer.
**Feature flag**: `DASHBOARD_VIRTUALIZATION_DEFER_DATA` (default: `False`)
Enable this if you see warehouse load spike on dashboard *open* even
though most charts are off-screen.
## Per-tab lazy loading
**This is on by default and has no flag.** A tab's content is not rendered
until the user activates that tab, so charts inside an unselected tab do
not fetch data on dashboard open. When the user clicks the tab, that
tab's charts mount and fetch in the normal way.
Practically: tabs are the single most effective tool for a large
dashboard. Splitting 60 charts across 4 tabs effectively turns dashboard
open into "load ~15 charts," and the remaining ones lazy-load only if the
user goes looking.
## Is there a switch to cap concurrent chart queries?
**No.** Superset does not implement a frontend-side concurrent-request
limiter. Each chart issues its own data request when it mounts, and the
browser handles parallelism — typically ~6 in-flight requests per origin
under HTTP/1.1, though HTTP/2 or HTTP/3 (if your deployment terminates
TLS that way) can multiplex considerably more over a single connection.
Backend throughput is bounded by your
Gunicorn worker count for synchronous query execution, or by your Celery
worker pool when [async queries](./async-queries-celery.mdx) are enabled.
If you need to throttle warehouse load, the right place is:
1. The warehouse itself (connection pool / concurrency limits).
2. Superset's Celery configuration (smaller worker pool when async
queries are on).
3. Splitting heavy charts across tabs or separate dashboards (each
dashboard load only fetches what's visible).
## Splitting strategies
When a dashboard outgrows comfortable performance, the options in order
of effort:
**1. Move sections into tabs.** Same dashboard, but only the active tab's
charts fetch. This is the cheapest change and often the only one needed.
**2. Cache aggressively.** A Redis cache backend (see
[Caching](./cache.mdx)) means repeat dashboard loads serve from cache
rather than re-hitting the warehouse. This is especially impactful for
dashboards opened by many users in close succession.
**3. Enable async queries.** [Async query execution](./async-queries-celery.mdx)
via Celery decouples query duration from request lifetime, so a slow
chart doesn't block the page. The user sees other charts come in as
their queries complete.
**4. Split into multiple dashboards.** Group related charts into purpose-
specific dashboards rather than one mega-dashboard. Link them from a
landing dashboard or a navigation menu.
**5. Pre-aggregate at the warehouse level.** If the same expensive
aggregation appears across many charts, materialize it as a view or
scheduled table in the warehouse so each chart query is a cheap lookup.
## Operational notes
- The feature flags above are set in `superset_config.py`, e.g.:
```python
FEATURE_FLAGS = {
"DASHBOARD_VIRTUALIZATION": True,
"DASHBOARD_VIRTUALIZATION_DEFER_DATA": True,
}
```
- See [Feature Flags](./feature-flags.mdx) for the full list of supported
flags and their lifecycle stages.
- Server-side screenshot jobs (alerts, scheduled reports, thumbnails)
render the dashboard in a headless, webdriver-controlled browser, which
intentionally bypasses row virtualization so the rendered artifact
includes every chart, not just the ones above the fold. User-triggered
"download as image/PDF" is different: it captures whatever's currently
rendered in the user's own browser, so it's still subject to
virtualization like any other page view. Metadata/YAML dashboard export
doesn't render the frontend at all, so virtualization doesn't apply to
it either.
@@ -519,6 +519,30 @@ sh -c "$(curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install
For those interested, you may also try out [avn](https://github.com/nvm-sh/nvm#deeper-shell-integration) to automatically switch to the node version that is required to run Superset frontend.
##### zstd
`npm run dev-server` proxies requests to your local Superset server and rewrites the HTML it returns, so it has to decompress responses sent with `Content-Encoding: zstd`. It does that with [`simple-zstd`](https://www.npmjs.com/package/simple-zstd), which wraps the system `zstd` binary instead of bundling one. That binary has to be on your `PATH`:
```bash
# macOS
brew install zstd
# Ubuntu/Debian
sudo apt install zstd
# Windows
choco install zstd
```
`simple-zstd` looks for the binary when it is first imported, not when a response is decompressed, so a missing `zstd` stops the dev server at startup with:
```
Error: Can not access zstd! Is it installed?
at Object.<anonymous> (.../node_modules/simple-zstd/dist/src/index.js:102:11)
```
The message names the dependency, but it surfaces from inside `webpack.proxy-config.js` while the webpack config is loading, which reads like a build-tooling failure rather than a missing system package.
#### Install dependencies
Install third-party dependencies listed in `package.json` via:
@@ -198,7 +198,7 @@ Each component should come with its dedicated storybook file.
**One component per story:** Each storybook file should only contain one component unless substantially different variants are required
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/src/components/Select/Select.stories.tsx` as an example.
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx` as an example.
**Isolated state:** The storybook should show how the component works in an isolated state and with as few dependencies as possible
@@ -0,0 +1,93 @@
---
title: Mobile Experience
sidebar_position: 7
version: 1
---
import useBaseUrl from "@docusaurus/useBaseUrl";
# Mobile Experience
Superset ships an optional, consumption-only mobile experience for viewing
dashboards on phones and other small screens. When enabled, screens below
768px wide get a layout built for touch: dashboards render their charts
stacked full-width, navigation collapses into a drawer, and dashboard
filters open in a slide-out panel.
The mobile experience is **read-only by design**. It is aimed at consumers
of analytics — people checking a dashboard from a phone — not at dashboard
authors. Authoring surfaces (chart builder, SQL Lab, dataset management,
and administrative screens) remain desktop-only.
## Enabling the mobile experience
The mobile experience is gated behind the `MOBILE_CONSUMPTION_MODE` feature
flag, which is off by default. Enable it in your `superset_config.py`:
```python
FEATURE_FLAGS = {
"MOBILE_CONSUMPTION_MODE": True,
}
```
With the flag disabled, Superset renders identically at every screen size,
and phones display the desktop layout scaled down (the pre-existing
behavior). The flag also controls whether Superset serves a viewport meta
tag, which is required for mobile browsers to apply the responsive layout
at their native width.
## What works on mobile
| Area | Mobile behavior |
| --- | --- |
| **Dashboards** | Charts stack vertically at full width, sized to the screen. Tab bars are sticky and swipeable. Native filters open in a drawer via the filter icon in the header. |
| **Dashboard list** | Card view with full-width cards; search and filters open in a drawer. |
| **Home** | Recents (dashboards only) and dashboard cards; desktop-only sections are hidden. |
| **Navigation** | A hamburger menu opens a drawer with links to dashboards, theme and language selection, and user info/logout. |
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap'}}>
<img src={useBaseUrl("/img/screenshots/mobile/mobile_dashboard.jpg")} alt="A dashboard on mobile with charts stacked full width" width="260" />
<img src={useBaseUrl("/img/screenshots/mobile/mobile_filter_drawer.jpg")} alt="The dashboard filter drawer on mobile" width="260" />
<img src={useBaseUrl("/img/screenshots/mobile/mobile_dashboard_list.jpg")} alt="The dashboard list in card view on mobile" width="260" />
</div>
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap', marginTop: '1rem'}}>
<img src={useBaseUrl("/img/screenshots/mobile/mobile_home.jpg")} alt="The Superset home page on mobile" width="260" />
<img src={useBaseUrl("/img/screenshots/mobile/mobile_nav_drawer.jpg")} alt="The mobile navigation drawer" width="260" />
<img src={useBaseUrl("/img/screenshots/mobile/mobile_unsupported.jpg")} alt="The screen shown for views that are not available on mobile" width="260" />
</div>
## What doesn't work on mobile
Everything not listed above shows a friendly "This view isn't available on
mobile" screen with shortcuts back to dashboards and the home page. That
includes:
- Chart builder (Explore) and chart-level links — chart titles on
dashboards are plain text on mobile, and chart entries are filtered out
of the home page's Recents feed
- SQL Lab and query history
- Creating or editing dashboards, charts, datasets, and databases
- List views other than dashboards (charts, datasets, saved queries, etc.)
- Administrative and settings screens
Editing controls are also removed from the screens that *are* supported:
the dashboard header hides the edit, publish, and favorite controls, and
dashboard/chart kebab menus are reduced to view-oriented actions.
If a device crosses the 768px threshold — for example, rotating a tablet
to landscape or resizing a window — the full desktop experience becomes
available immediately.
## Notes for operators
- The flag is deployment-wide; there is no per-role or per-user targeting.
- Dashboard permalinks and links shared from desktop resolve normally on
mobile as long as they point at dashboards.
- Embedded dashboards are unaffected: the embedded SDK controls its own
layout, and the viewport meta tag is only interpreted by the top-level
page.
- Dashboards loaded with a `standalone` URL param (used for iframe embeds
and kiosk-style displays) always render the desktop layout, regardless
of viewport width, since the standalone chrome doesn't expose the mobile
filter drawer's trigger.
+4 -4
View File
@@ -58,12 +58,12 @@
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.5.6",
"@storybook/addon-docs": "^10.5.7",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.47",
"antd": "^6.5.3",
"antd": "^6.5.4",
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001806",
"caniuse-lite": "^1.0.30001807",
"docusaurus-plugin-openapi-docs": "^5.1.3",
"docusaurus-theme-openapi-docs": "^5.1.3",
"js-yaml": "^5.2.3",
@@ -77,7 +77,7 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.6",
"storybook": "^10.5.7",
"swagger-ui-react": "^5.32.12",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
+50 -17
View File
@@ -68,6 +68,8 @@ function getProviders() {
const { themeObject } = require('@apache-superset/core/theme');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { App, ConfigProvider } = require('antd');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { useColorMode } = require('@docusaurus/theme-common');
// Configure Ant Design to render portals (tooltips, dropdowns, etc.)
// inside the closest .storybook-example container instead of document.body
@@ -78,15 +80,39 @@ function getProviders() {
return container || document.body;
};
// `themeObject` is a module-level singleton (superset-core/src/theme
// index.tsx: `Theme.fromConfig()`), created once with no dark/light
// config, so SupersetThemeProvider always rendered whatever that default
// algorithm was -- it had no way to know about Docusaurus's theme toggle.
// Docusaurus tracks the toggle in React context (useColorMode), so
// mirror it onto the singleton via the toggleDarkMode() method Theme
// already exposes for exactly this purpose.
//
// Use useLayoutEffect (not useEffect) so the sync runs before the
// browser paints. This component only ever mounts client-side (it's
// built inside a BrowserOnly callback), so there's no SSR mismatch
// concern -- and running synchronously before paint avoids a brief
// flash of the singleton's previous palette when a page loads directly
// in dark mode or the toggle fires during route navigation.
function ThemeSync({ children }) {
const { colorMode } = useColorMode();
React.useLayoutEffect(() => {
themeObject.toggleDarkMode(colorMode === 'dark');
}, [colorMode]);
return children;
}
SupersetProviders = ({ children }) => (
<themeObject.SupersetThemeProvider>
<ConfigProvider
getPopupContainer={getPopupContainer}
getTargetContainer={() => document.body}
>
<App>{children}</App>
</ConfigProvider>
</themeObject.SupersetThemeProvider>
<ThemeSync>
<themeObject.SupersetThemeProvider>
<ConfigProvider
getPopupContainer={getPopupContainer}
getTargetContainer={() => document.body}
>
<App>{children}</App>
</ConfigProvider>
</themeObject.SupersetThemeProvider>
</ThemeSync>
);
return SupersetProviders;
} catch (error) {
@@ -133,7 +159,7 @@ function LoadingPlaceholder() {
return (
<div
style={{
border: '1px solid #e8e8e8',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
@@ -141,7 +167,7 @@ function LoadingPlaceholder() {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#999',
color: 'var(--ifm-color-emphasis-600)',
}}
>
Loading component...
@@ -162,7 +188,7 @@ export function StoryExample({ component, props = {} }) {
<div
className="storybook-example"
style={{
border: '1px solid #e8e8e8',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
@@ -172,7 +198,7 @@ export function StoryExample({ component, props = {} }) {
{Component ? (
<Component {...restProps}>{children}</Component>
) : (
<div style={{ color: '#999' }}>
<div style={{ color: 'var(--ifm-color-emphasis-600)' }}>
Component &quot;{String(component)}&quot; not found
</div>
)}
@@ -373,7 +399,7 @@ function StoryWithControlsInner({
<div
className="storybook-example"
style={{
border: '1px solid #e8e8e8',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
@@ -393,7 +419,7 @@ function StoryWithControlsInner({
</Component>
</>
) : (
<div style={{ color: '#999' }}>
<div style={{ color: 'var(--ifm-color-emphasis-600)' }}>
Component &quot;{String(componentToRender)}&quot; not found
</div>
)}
@@ -403,7 +429,7 @@ function StoryWithControlsInner({
<div
className="storybook-controls"
style={{
border: '1px solid #e8e8e8',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
padding: '20px',
marginBottom: '20px',
@@ -545,7 +571,7 @@ function ComponentGalleryInner({
if (!Component) {
return (
<div style={{ color: '#999' }}>
<div style={{ color: 'var(--ifm-color-emphasis-600)' }}>
Component &quot;{String(component)}&quot; not found
</div>
);
@@ -556,7 +582,14 @@ function ComponentGalleryInner({
<div className="component-gallery">
{sizes.map(size => (
<div key={size} style={{ marginBottom: 40 }}>
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
<h4
style={{
marginBottom: 16,
color: 'var(--ifm-color-emphasis-700)',
}}
>
{size}
</h4>
<div
style={{
display: 'flex',
+6
View File
@@ -69,6 +69,12 @@
"lifecycle": "development",
"description": "Enable Matrixify feature for matrix-style chart layouts"
},
{
"name": "MOBILE_CONSUMPTION_MODE",
"default": false,
"lifecycle": "development",
"description": "Serve a consumption-only mobile experience (dashboards, dashboard list, and home page) on small screens; other views show a \"not supported on mobile\" screen. Authoring features are hidden on mobile when enabled."
},
{
"name": "OPTIMIZE_SQL",
"default": false,
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+26 -26
View File
@@ -4095,23 +4095,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.6":
version "10.5.6"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.6.tgz#445d4e0992a0862a22bffcea321ee2cb034846b5"
integrity sha512-zyUJBrrpC9NTrmsREaVFNr+9WW6pikJtmRvo7GgZGqthEyhjQKSarHrW0aNWkwae2ep3jp1CZi8vIUVG1Dnp0w==
"@storybook/addon-docs@^10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.7.tgz#6d599c94fc871c248ce06a5c081f57655c83f40a"
integrity sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.6"
"@storybook/csf-plugin" "10.5.7"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.6"
"@storybook/react-dom-shim" "10.5.7"
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent "^2.0.0"
"@storybook/csf-plugin@10.5.6":
version "10.5.6"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.6.tgz#9fca28f5fd7d545a32638bb4f08902f6887072b2"
integrity sha512-PJLyOmcKe1OZDBw7RaGX/gjuiJuVfS5pVgc4W2RnHYOFpU6F5Bv9+9MqQwp0i7tWZBWc4fsCJudgVqwgjuTROA==
"@storybook/csf-plugin@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz#bc73f164d1b5f8e2931b2774f4b389a06453cf6e"
integrity sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==
dependencies:
unplugin "^2.3.5"
@@ -4125,10 +4125,10 @@
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
"@storybook/react-dom-shim@10.5.6":
version "10.5.6"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz#3685605c9dd27298fada7fef264801b2e7d62cbb"
integrity sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==
"@storybook/react-dom-shim@10.5.7":
version "10.5.7"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz#9a5aa0e0f89c09e71c6cbfc6bb1abeb537e5aabf"
integrity sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -6164,10 +6164,10 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.3.tgz#3c7d2ec4a20be116f72b7fbbdb4497d65ffd6dae"
integrity sha512-Q5r8sztf9Yk9B70bSUjnPYMCJ4A/eZM7uMoTj8UAhlSKR9aftjEuBEPcNSmRux7hB+87rxO8vN1X4HNjR97qyQ==
antd@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.4.tgz#b41665e86a5f46ca761abd3b0abef7460116ca0d"
integrity sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
@@ -6745,10 +6745,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.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==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001807:
version "1.0.30001807"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz#a113854941fb45b4c1f51793f4636920489079b4"
integrity sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==
ccount@^2.0.0:
version "2.0.1"
@@ -14765,10 +14765,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.6:
version "10.5.6"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.6.tgz#c91f22f617f3718dd06c58c87b46ff66f4ce7bf5"
integrity sha512-VhYwqxPySa24CVXKoWD6gCZXx9//DTmo43YpusGuAoHDYj5Osjt8wuBRQVeGoaLUWnHiPWv8S+GYHrJEaBM6Rg==
storybook@^10.5.7:
version "10.5.7"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.7.tgz#adfc465e51f337291c095278c23f1b8024ef2da7"
integrity sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
+1 -1
View File
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.22.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.22.6 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27
+14 -14
View File
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.22.5](https://img.shields.io/badge/Version-0.22.5-informational?style=flat-square)
![Version: 0.22.6](https://img.shields.io/badge/Version-0.22.6-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -205,9 +205,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetCeleryBeat.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetCeleryBeat.initContainers | list | a container waiting for postgres | List of init containers |
| supersetCeleryBeat.podAnnotations | object | `{}` | Annotations to be added to supersetCeleryBeat pods |
| supersetCeleryBeat.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryBeat pods |
| supersetCeleryBeat.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryBeat pods |
| supersetCeleryBeat.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetCeleryBeat.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryBeat.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryBeat.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryBeat.podLabels | object | `{}` | Labels to be added to supersetCeleryBeat pods |
| supersetCeleryBeat.podSecurityContext | object | `{}` | |
@@ -231,9 +231,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetCeleryFlower.livenessProbe.successThreshold | int | `1` | |
| supersetCeleryFlower.livenessProbe.timeoutSeconds | int | `1` | |
| supersetCeleryFlower.podAnnotations | object | `{}` | Annotations to be added to supersetCeleryFlower pods |
| supersetCeleryFlower.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryFlower pods |
| supersetCeleryFlower.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryFlower pods |
| supersetCeleryFlower.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetCeleryFlower.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryFlower.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryFlower.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetCeleryFlower.podLabels | object | `{}` | Labels to be added to supersetCeleryFlower pods |
| supersetCeleryFlower.podSecurityContext | object | `{}` | |
@@ -285,10 +285,10 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetMcp.livenessProbe.successThreshold | int | `1` | |
| supersetMcp.livenessProbe.timeoutSeconds | int | `3` | |
| supersetMcp.podAnnotations | object | `{}` | Annotations to be added to supersetMcp pods |
| supersetMcp.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetMcp pods |
| supersetMcp.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetMcp pods |
| supersetMcp.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetMcp.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget |
| supersetMcp.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget |
| supersetMcp.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetMcp.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetMcp.podLabels | object | `{}` | Labels to be added to supersetMcp pods |
| supersetMcp.podSecurityContext | object | `{}` | |
| supersetMcp.priorityClassName | string | `nil` | Set priorityClassName for supersetMcp pods |
@@ -341,9 +341,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetNode.livenessProbe.successThreshold | int | `1` | |
| supersetNode.livenessProbe.timeoutSeconds | int | `1` | |
| supersetNode.podAnnotations | object | `{}` | Annotations to be added to supersetNode pods |
| supersetNode.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetNode pods |
| supersetNode.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetNode pods |
| supersetNode.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetNode.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetNode.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetNode.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetNode.podLabels | object | `{}` | Labels to be added to supersetNode pods |
| supersetNode.podSecurityContext | object | `{}` | |
@@ -391,9 +391,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetWebsockets.livenessProbe.successThreshold | int | `1` | |
| supersetWebsockets.livenessProbe.timeoutSeconds | int | `1` | |
| supersetWebsockets.podAnnotations | object | `{}` | |
| supersetWebsockets.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWebsockets pods |
| supersetWebsockets.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWebsockets pods |
| supersetWebsockets.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetWebsockets.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWebsockets.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWebsockets.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWebsockets.podLabels | object | `{}` | |
| supersetWebsockets.podSecurityContext | object | `{}` | |
@@ -448,9 +448,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| supersetWorker.livenessProbe.successThreshold | int | `1` | |
| supersetWorker.livenessProbe.timeoutSeconds | int | `60` | |
| supersetWorker.podAnnotations | object | `{}` | Annotations to be added to supersetWorker pods |
| supersetWorker.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":1,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWorker pods |
| supersetWorker.podDisruptionBudget | object | `{"enabled":false,"maxUnavailable":null,"minAvailable":1}` | Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWorker pods |
| supersetWorker.podDisruptionBudget.enabled | bool | `false` | Whether the pod disruption budget should be created |
| supersetWorker.podDisruptionBudget.maxUnavailable | int | `1` | If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWorker.podDisruptionBudget.maxUnavailable | string | `nil` | If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWorker.podDisruptionBudget.minAvailable | int | `1` | If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget |
| supersetWorker.podLabels | object | `{}` | Labels to be added to supersetWorker pods |
| supersetWorker.podSecurityContext | object | `{}` | |
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetCeleryBeat.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetCeleryBeat.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetCeleryFlower.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetCeleryFlower.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetMcp.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetMcp.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetWorker.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetWorker.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetWebsockets.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetWebsockets.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+1 -1
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetNode.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "supersetNode.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
+87
View File
@@ -0,0 +1,87 @@
#
# 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: pod disruption budgets
templates:
- pdb.yaml
- pdb-worker.yaml
- pdb-beat.yaml
- pdb-flower.yaml
- pdb-ws.yaml
- pdb-mcp.yaml
# The chart must not ship conflicting minAvailable/maxUnavailable defaults: enabling a
# PDB with `enabled: true` alone has to render, since Helm merges user values on top of
# the chart defaults and the templates fail when both fields are set.
tests:
- it: renders no PDB by default
asserts:
- hasDocuments:
count: 0
- it: renders with minAvailable only when just enabled is set
set:
supersetNode.podDisruptionBudget.enabled: true
supersetWorker.podDisruptionBudget.enabled: true
supersetCeleryBeat.podDisruptionBudget.enabled: true
supersetCeleryFlower.podDisruptionBudget.enabled: true
supersetWebsockets.podDisruptionBudget.enabled: true
supersetMcp.podDisruptionBudget.enabled: true
asserts:
- hasDocuments:
count: 1
- isKind:
of: PodDisruptionBudget
- equal:
path: spec.minAvailable
value: 1
- notExists:
path: spec.maxUnavailable
- it: honors an overridden minAvailable
template: pdb.yaml
set:
supersetNode.podDisruptionBudget.enabled: true
supersetNode.podDisruptionBudget.minAvailable: 2
asserts:
- equal:
path: spec.minAvailable
value: 2
- notExists:
path: spec.maxUnavailable
- it: honors maxUnavailable when minAvailable is unset
template: pdb-worker.yaml
set:
supersetWorker.podDisruptionBudget.enabled: true
supersetWorker.podDisruptionBudget.minAvailable: null
supersetWorker.podDisruptionBudget.maxUnavailable: 1
asserts:
- equal:
path: spec.maxUnavailable
value: 1
- notExists:
path: spec.minAvailable
- it: fails when both minAvailable and maxUnavailable are set explicitly
template: pdb.yaml
set:
supersetNode.podDisruptionBudget.enabled: true
supersetNode.podDisruptionBudget.minAvailable: 1
supersetNode.podDisruptionBudget.maxUnavailable: 1
asserts:
- failedTemplate:
errorMessage: "supersetNode.podDisruptionBudget: only one of minAvailable or maxUnavailable should be set - unset the other one (set it to null)"
+13 -13
View File
@@ -410,8 +410,8 @@ supersetNode:
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
# -- Startup command
# @default -- See `values.yaml`
@@ -542,8 +542,8 @@ supersetWorker:
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
# -- Worker startup command
# @default -- a `celery worker` command
command:
@@ -665,8 +665,8 @@ supersetCeleryBeat:
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
# -- Command
# @default -- a `celery beat` command
command:
@@ -751,8 +751,8 @@ supersetCeleryFlower:
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
# -- Command
# @default -- a `celery flower` command
command:
@@ -869,8 +869,8 @@ supersetWebsockets:
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
ingress:
path: /ws
pathType: Prefix
@@ -999,10 +999,10 @@ supersetMcp:
podDisruptionBudget:
# -- Whether the pod disruption budget should be created
enabled: false
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget
# -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
minAvailable: 1
# -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/\#specifying-a-poddisruptionbudget
maxUnavailable: 1
# -- If set, minAvailable must be unset (`minAvailable: ~`) - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget
maxUnavailable: ~
# -- Command
# @default -- a `superset mcp run` command
command:
+23 -30
View File
@@ -60,15 +60,11 @@ dependencies = [
"flask-login>=0.6.0, < 1.0",
"flask-migrate>=4.1.0, <5.0",
"flask-session>=0.4.0, <1.0",
# Pinned explicitly below 3.0: 3.0.5 resolves without conflict and
# supports both SQLAlchemy 1.4 and 2.0, but real CI runs surfaced a
# structural incompatibility with Superset's current session/app-context
# handling across Celery task boundaries (see PR #42542) -- widespread
# "NoneType has no attribute X" failures and MySQL lock-wait timeouts,
# not just a connection-pool quirk. Needs dedicated investigation, not a
# driver-compat-prep bump; revisit alongside the actual SQLAlchemy 2.0
# core bump (discussion #40273, step 6).
"flask-sqlalchemy>=2.5.1, <4.0",
# Bumped to 3.1.1 alongside the SQLAlchemy 2.0 core bump (discussion
# #40273, step 6), which resolves the session/app-context handling
# across Celery task boundaries that previously blocked this (see
# PR #42542).
"flask-sqlalchemy>=3.1.1, <4.0",
"flask-wtf>=1.3.0, <2.0",
"geopy",
"greenlet<=3.5.4, >=3.5.4",
@@ -115,7 +111,7 @@ dependencies = [
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.1",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=1.4.43, <2", # 1.4.43 adds the python-oracledb (oracle+oracledb) dialect
"sqlalchemy>=2.0.0, <2.1",
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.16.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
@@ -164,11 +160,10 @@ databricks = [
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"]
# sqlalchemy-dremio 3.0.5+ hard-pins sqlalchemy~=2.0.41, dropping 1.4; 3.0.4
# is the last dual-compat release. Capped below 3.0.5 for now; widen back to
# <4 in lockstep with Superset's own SQLAlchemy 2.0 core bump (discussion
# #40273), not before.
dremio = ["sqlalchemy-dremio>=1.2.1, <3.0.5"]
# sqlalchemy-dremio 3.0.5+ hard-pins sqlalchemy~=2.0.41, dropping 1.4.
# Widened now that Superset's own SQLAlchemy 2.0 core bump has landed
# (discussion #40273).
dremio = ["sqlalchemy-dremio>=3.0.5, <4"]
# <2 was an artificial ceiling; upstream has no SQLAlchemy version cap and
# 1.1.10 already supports SQLAlchemy 2.0 (added `import_dbapi` in 1.1.7).
drill = ["sqlalchemy-drill>=1.1.10, <3"]
@@ -181,10 +176,9 @@ dynamodb = ["pydynamodb>=0.8.2"]
solr = ["sqlalchemy-solr>=0.2.4.3"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
# sqlalchemy-exasol cuts hard from SQLAlchemy 1.4-only (<6.0.0) to 2.0-only
# (>=6.0.0) with no dual-compat release. Capped below 6.0.0 for now; bump to
# >=6.0.0,<8.0 in lockstep with Superset's own SQLAlchemy 2.0 core bump
# (discussion #40273), not before.
exasol = ["sqlalchemy-exasol>=2.4.0, <6.0.0"]
# (>=6.0.0) with no dual-compat release. Bumped now that Superset's own
# SQLAlchemy 2.0 core bump has landed (discussion #40273).
exasol = ["sqlalchemy-exasol>=6.0.0, <8.0"]
excel = ["xlrd>=2.0.2, <2.1"]
# Async dashboard "Export Data/Images to Excel": uploads the workbook to S3 and
# emails a pre-signed link. boto3 is imported lazily by superset.utils.s3, so
@@ -199,9 +193,9 @@ fastmcp = [
]
# sqlalchemy-firebird >=2.0.0 unconditionally requires SQLAlchemy 2.0 on
# Python >=3.8 (which covers Superset's >=3.11 floor), with no dual-compat
# release. Capped below 2.0.0 for now; bump to >=2.2.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
firebird = ["sqlalchemy-firebird>=0.8.0, <2.0.0"]
# release. Bumped now that Superset's own SQLAlchemy 2.0 core bump has
# landed (discussion #40273).
firebird = ["sqlalchemy-firebird>=2.2.0"]
firebolt = ["firebolt-sqlalchemy>=1.1.2, <2"]
gevent = ["gevent>=26.7.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.5, <2"]
@@ -240,15 +234,14 @@ presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.338.0"]
prophet = ["prophet>=1.3.0, <2"]
# sqlalchemy-redshift cuts hard from SQLAlchemy 1.4-only (0.8.x) to 2.0-only
# (>=1.0.0) with no dual-compat release; the existing <0.9 ceiling already
# keeps this on the 1.4-only line. Bump to >=1.0.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
# (>=1.0.0) with no dual-compat release. Bumped now that Superset's own
# SQLAlchemy 2.0 core bump has landed (discussion #40273).
redshift = ["sqlalchemy-redshift>=1.0.0"]
# No release of sqlalchemy-risingwave has ever supported both SQLAlchemy 1.4
# and 2.0 (version numbers don't track SQLAlchemy compat monotonically); pin
# to the newest 1.4-only release for now. Bump to >=2.0.0 in lockstep with
# Superset's own SQLAlchemy 2.0 core bump (discussion #40273), not before.
risingwave = ["sqlalchemy-risingwave>=1.4.1, <3.0.0"]
# and 2.0 (version numbers don't track SQLAlchemy compat monotonically).
# Bumped to the 2.0-only line now that Superset's own SQLAlchemy 2.0 core
# bump has landed (discussion #40273).
risingwave = ["sqlalchemy-risingwave>=2.0.0"]
shillelagh = ["shillelagh[all]>=1.4.5, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
-20
View File
@@ -23,25 +23,5 @@ python_files = *_test.py test_*.py *_tests.py *viz/utils.py
asyncio_mode = auto
# `ignore` is effectively equivalent to `-p no:warnings`.
# Always print RemovedIn20Warning when SQLALCHEMY_WARN_20=1.
# Additionally, raise errors for refactored RemovedIn20Warning cases to prevent regression.
filterwarnings =
ignore
always::sqlalchemy.exc.RemovedIn20Warning
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"ReportExecutionLog" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"ReportRecipients" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
error:"SSHTunnel" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
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 connection.execute\(\) method: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
+3 -2
View File
@@ -144,7 +144,7 @@ flask-migrate==4.1.0
# via apache-superset (pyproject.toml)
flask-session==0.8.0
# via apache-superset (pyproject.toml)
flask-sqlalchemy==2.5.1
flask-sqlalchemy==3.1.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
@@ -381,7 +381,7 @@ six==1.17.0
# wtforms-json
slack-sdk==3.43.0
# via apache-superset (pyproject.toml)
sqlalchemy==1.4.54
sqlalchemy==2.0.51
# via
# apache-superset (pyproject.toml)
# alembic
@@ -419,6 +419,7 @@ typing-extensions==4.16.0
# pyopenssl
# referencing
# shillelagh
# sqlalchemy
# typing-inspection
typing-inspection==0.4.2
# via pydantic
+3 -2
View File
@@ -306,7 +306,7 @@ flask-session==0.8.0
# via
# -c requirements/base-constraint.txt
# apache-superset
flask-sqlalchemy==2.5.1
flask-sqlalchemy==3.1.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -950,7 +950,7 @@ slack-sdk==3.43.0
# apache-superset
sniffio==1.3.1
# via anyio
sqlalchemy==1.4.54
sqlalchemy==2.0.51
# via
# -c requirements/base-constraint.txt
# alembic
@@ -1033,6 +1033,7 @@ typing-extensions==4.16.0
# pyopenssl
# referencing
# shillelagh
# sqlalchemy
# starlette
# typing-inspection
typing-inspection==0.4.2
+48
View File
@@ -0,0 +1,48 @@
#!/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.
# Computes the `--extra-flags` value passed to `supersetbot docker` for a
# given build preset. Factored out of .github/workflows/docker.yml so the
# PY_VER override logic below can be exercised by an always-on CI check
# (docker.yml's docker-build job only runs when the change detector's
# docker/python/frontend outputs are true, and the PR build matrix never
# includes py311/py312 at all, so a regression here would otherwise go
# unnoticed until the fix actually runs on master) without duplicating -
# and risking drift from - the logic used by the real build step.
#
# supersetbot's "py311"/"py312" presets pin their own --build-arg PY_VER,
# which lands ahead of --extra-flags on the assembled buildx command line;
# docker/buildx keeps the last value for a repeated --build-arg key, so
# appending PY_VER here would override supersetbot's pin and silently make
# "py311"/"py312" build the exact same image as "lean". Every other preset
# gets the override so its build lands on the Dockerfile's own supported
# Python version.
#
# Usage: docker-build-extra-flags.sh <build_preset> <image_tag>
set -euo pipefail
BUILD_PRESET="${1:?usage: docker-build-extra-flags.sh <build_preset> <image_tag>}"
IMAGE_TAG="${2:?usage: docker-build-extra-flags.sh <build_preset> <image_tag>}"
EXTRA_FLAGS="--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG"
if [ "$BUILD_PRESET" != "py311" ] && [ "$BUILD_PRESET" != "py312" ]; then
EXTRA_FLAGS="--build-arg PY_VER=3.11.14-slim-trixie $EXTRA_FLAGS"
fi
echo "$EXTRA_FLAGS"
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
"isodate>=0.7.0",
"pyarrow>=16.0.0",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy>=2.0.0,<2.1",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.8.0, <31",
"typing-extensions>=4.0.0",
+8 -7
View File
@@ -3215,16 +3215,17 @@
"integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/browserslist": {
@@ -11184,9 +11185,9 @@
"integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
},
"brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"peer": true,
"requires": {
@@ -36,6 +36,9 @@
"cypress": {
"form-data": "^2.3.4"
},
"minimatch@>=10": {
"brace-expansion": ">=5.0.8"
},
"qs": "^6.14.2",
"uuid": "^11.1.1"
},
+125 -114
View File
@@ -79,9 +79,9 @@
"@visx/scale": "^4.0.0",
"@visx/tooltip": "^4.0.0",
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.0.2",
"ag-grid-react": "36.0.2",
"antd": "^6.5.3",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.5.4",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
@@ -100,7 +100,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.0",
"immer": "^11.1.15",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -108,7 +108,7 @@
"json-stringify-pretty-compact": "^4.0.0",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"mapbox-gl": "^3.27.0",
"mapbox-gl": "^3.28.1",
"markdown-to-jsx": "^9.10.2",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
@@ -116,7 +116,7 @@
"mustache": "^4.2.0",
"nanoid": "^6.0.1",
"ol": "^10.10.0",
"query-string": "9.4.1",
"query-string": "9.5.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.16.0",
@@ -180,9 +180,9 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.6",
"@storybook/addon-links": "10.5.6",
"@storybook/react-webpack5": "10.5.6",
"@storybook/addon-docs": "10.5.7",
"@storybook/addon-links": "10.5.7",
"@storybook/react-webpack5": "10.5.7",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.47",
@@ -235,7 +235,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"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.6",
"eslint-plugin-storybook": "10.5.7",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -266,13 +266,13 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.6",
"storybook": "10.5.7",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.12",
"tscw-config": "^1.1.2",
"tsx": "^4.23.5",
"tsx": "^4.23.10",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
@@ -10741,16 +10741,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.6.tgz",
"integrity": "sha512-zyUJBrrpC9NTrmsREaVFNr+9WW6pikJtmRvo7GgZGqthEyhjQKSarHrW0aNWkwae2ep3jp1CZi8vIUVG1Dnp0w==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.7.tgz",
"integrity": "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.5.6",
"@storybook/csf-plugin": "10.5.7",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.5.6",
"@storybook/react-dom-shim": "10.5.7",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -10761,7 +10761,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10770,9 +10770,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.6.tgz",
"integrity": "sha512-PJLyOmcKe1OZDBw7RaGX/gjuiJuVfS5pVgc4W2RnHYOFpU6F5Bv9+9MqQwp0i7tWZBWc4fsCJudgVqwgjuTROA==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz",
"integrity": "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10785,7 +10785,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "10.5.6",
"storybook": "10.5.7",
"vite": "*",
"webpack": "*"
},
@@ -10805,9 +10805,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz",
"integrity": "sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz",
"integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -10819,7 +10819,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10831,9 +10831,9 @@
}
},
"node_modules/@storybook/addon-links": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.6.tgz",
"integrity": "sha512-pw+OS/wUZ4ijdVGOsE5QOt59+C2i4fwtFBs2ircB7KMlwEE7gslovZEjnrz8bbaznvmoLoYGWYZxXcLi+bYmzg==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.7.tgz",
"integrity": "sha512-17PxEOocLhAEaPeQ4q+8yul/LF9YEIePS1arknCAS7U1pQXTe0uj+R0pB6uPLVflM5gECQMiP4WzIj4tEiL6+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10846,7 +10846,7 @@
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10940,15 +10940,15 @@
}
},
"node_modules/@storybook/react-webpack5": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.6.tgz",
"integrity": "sha512-UdsC+IrZHBAtEvvDkfCPhg5sy5jnJAHT4RS3I8wNHVJ+93gaUrZLElSIV+w5UB1u/yghoqmQm7/LfpPpZrjPZQ==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.7.tgz",
"integrity": "sha512-vvl07oXp2qfmHJHZ77Aw1F3LFOo7XubOta+lC8UmlEw3rDDJhQxJN3erJJVHavNhdA2jBTK6VUXQKdqQh7X7nQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-webpack5": "10.5.6",
"@storybook/preset-react-webpack": "10.5.6",
"@storybook/react": "10.5.6"
"@storybook/builder-webpack5": "10.5.7",
"@storybook/preset-react-webpack": "10.5.7",
"@storybook/react": "10.5.7"
},
"funding": {
"type": "opencollective",
@@ -10957,7 +10957,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6",
"storybook": "10.5.7",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -10967,13 +10967,13 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.6.tgz",
"integrity": "sha512-uWo/MzNC6HXMEpy8QQfbeYh1j6aOC6Ly0sAR6RE0LPvpyGEWC0VaVOBERIJJBjeuPuonDBVKfEjh1iUvZhJopg==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.7.tgz",
"integrity": "sha512-4n4c60LihFivZnjAcXGO5+XbgZthoUtKb/nPKVgypj3MpEetzjq6XR83A4UNnRsXYmjqfn6bsDWNgEJ/RvQg5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.6",
"@storybook/core-webpack": "10.5.7",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
@@ -10995,7 +10995,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11004,9 +11004,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.6.tgz",
"integrity": "sha512-o5PP3K+NcJAitZF7Ywweow0d8dJrEA1jxV5T1LMGMiWHUrnpoaPTiK1HcYw39pOQkdKL88mMSfDDUipGurZthw==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.7.tgz",
"integrity": "sha512-0dtDw/FNPREoeCHX2RgZz0OecxaAGol1R7bCobFevArxyFIPJisTfjDMUFHKr+3B7BilTd3vnatl7Nlvgs0EiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11017,17 +11017,17 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.6"
"storybook": "10.5.7"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.6.tgz",
"integrity": "sha512-QPUl2t+0VIp1Wy7JfqvV8cI1NrULUt+XFMKdIaNp39TuyMn3El4txvmxQWKhcYvnSOEzQ2SGNDqSWcJISwmeAA==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.7.tgz",
"integrity": "sha512-xwNRcoVlIDx1/YYCFBAxfh/91vFiOgrVI+0Ir4u9eO87SH2leehRnJh619QEOrlQEU5px487y2BmL2ZVtmTpYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.6",
"@storybook/core-webpack": "10.5.7",
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/semver": "^7.7.1",
"magic-string": "^0.30.5",
@@ -11044,7 +11044,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11053,9 +11053,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.6.tgz",
"integrity": "sha512-o5PP3K+NcJAitZF7Ywweow0d8dJrEA1jxV5T1LMGMiWHUrnpoaPTiK1HcYw39pOQkdKL88mMSfDDUipGurZthw==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.7.tgz",
"integrity": "sha512-0dtDw/FNPREoeCHX2RgZz0OecxaAGol1R7bCobFevArxyFIPJisTfjDMUFHKr+3B7BilTd3vnatl7Nlvgs0EiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11066,18 +11066,18 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.6"
"storybook": "10.5.7"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.6.tgz",
"integrity": "sha512-dXSdNoc9yAvpa4hiegQhmZPXOKunAxkPX94DxvRw/kM6+wujVFAGlZjYygKrWw357KOjPRK7SO1LRTc70mgrhQ==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.7.tgz",
"integrity": "sha512-uFvty2MMdFXzW5PcQe1JqDAZkz6cQq7q/9G/cbGVnBEvP6zsOVeL+bmrQ0/WBlFQN0Ko9+ZoCTvaQ9s65zBa5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "10.5.6",
"@storybook/react-dom-shim": "10.5.7",
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
@@ -11090,7 +11090,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6",
"storybook": "10.5.7",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -11106,9 +11106,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz",
"integrity": "sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz",
"integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -11120,7 +11120,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.6"
"storybook": "10.5.7"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -15025,28 +15025,28 @@
}
},
"node_modules/ag-charts-types": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-14.0.2.tgz",
"integrity": "sha512-F7ZG0g8Y+iKhJi50AfZRwEyUM/TBsNyh2IoXB0JaDN97lnbemIK8GE5kF1eBtXtN4mcC+lPXK9oZUeVXwO9EWA==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-14.1.0.tgz",
"integrity": "sha512-mmzkng88c0l+Z9PvCMowMilhVeNgDU/iMuMemcOQD4BG/qO4vbkxWvyQO0iqju8Dx7YOY81PNzC/RmElQNiVkA==",
"license": "MIT"
},
"node_modules/ag-grid-community": {
"version": "36.0.2",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-36.0.2.tgz",
"integrity": "sha512-TINZfuFvMY2nc3JfQHiUWT7dNIxI89ZxS5XkXIPi/rYICoNupRqpaM41KVzGPPfSkM0AwhuzTFxAiF08zEkV1Q==",
"version": "36.1.0",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-36.1.0.tgz",
"integrity": "sha512-WnvSQ4csRs8gv/b1B0lPHykQXvUJVgi2u5mpY0Aa6dv3pvhDVVqCT8dwUyOeCog4JNPptaXGrOrZ8qP2XoJzVA==",
"license": "MIT",
"dependencies": {
"ag-charts-types": "14.0.2",
"ag-stack": "36.0.2"
"ag-charts-types": "14.1.0",
"ag-stack": "36.1.0"
}
},
"node_modules/ag-grid-react": {
"version": "36.0.2",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-36.0.2.tgz",
"integrity": "sha512-yVPmqdhx1zp06FLyZmwmIxIO57w4ko+qN64MXgPlBMJVL0MNA5hULqAY/+SoB4cd0bBHqz8okuuAbHHpu5QoHQ==",
"version": "36.1.0",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-36.1.0.tgz",
"integrity": "sha512-FNsmrOmr+taZY11sCA+6Ow0mKs2JqnmrjBrMjkKC2rVTXPARtks+tv+Ne4ZkLGGtvIMDlBFCDbSzFXVP0CdwXQ==",
"license": "MIT",
"dependencies": {
"ag-grid-community": "36.0.2",
"ag-grid-community": "36.1.0",
"prop-types": "^15.8.1"
},
"peerDependencies": {
@@ -15055,9 +15055,9 @@
}
},
"node_modules/ag-stack": {
"version": "36.0.2",
"resolved": "https://registry.npmjs.org/ag-stack/-/ag-stack-36.0.2.tgz",
"integrity": "sha512-YuhQExQw5YsWK0wxrksRyYBAqOU0v08lJH5uxRsKx+49ko5vkDgnJuhX4yF995BBVdLY1LKlXukLEub+olKyuA==",
"version": "36.1.0",
"resolved": "https://registry.npmjs.org/ag-stack/-/ag-stack-36.1.0.tgz",
"integrity": "sha512-Kmkf5iRZmyduNx2KtW450j6GbCdJ4ejacSpb1AWl0yrdquRmScLRc1SlLolx6lyCbTBSJGo4tRraKbBzre+GlQ==",
"license": "MIT"
},
"node_modules/agent-base": {
@@ -15214,9 +15214,9 @@
}
},
"node_modules/antd": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/antd/-/antd-6.5.3.tgz",
"integrity": "sha512-Q5r8sztf9Yk9B70bSUjnPYMCJ4A/eZM7uMoTj8UAhlSKR9aftjEuBEPcNSmRux7hB+87rxO8vN1X4HNjR97qyQ==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/antd/-/antd-6.5.4.tgz",
"integrity": "sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.1",
@@ -18005,11 +18005,14 @@
}
},
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"version": "3.50.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"hasInstallScript": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -18910,9 +18913,9 @@
}
},
"node_modules/decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.5.0.tgz",
"integrity": "sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
@@ -20519,9 +20522,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.6.tgz",
"integrity": "sha512-uOXhNkIH+iTdyViSmWnCrwtapasL57M3nq5yfST1H7y9djRLyuAIfNcf9cPBedc2G1oqI8jn3up/VHdN3y3Btw==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.7.tgz",
"integrity": "sha512-mLpamG1Rsica2jYbUzIZOEuy7Fm1IMtVLMvvxGTpjTVKUMxTXJsANx3MBpH2VSbGQB8Yzlt5399WL/O07K97Ig==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20530,7 +20533,7 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "10.5.6"
"storybook": "10.5.7"
}
},
"node_modules/eslint-plugin-testing-library": {
@@ -24184,9 +24187,9 @@
"license": "MIT"
},
"node_modules/immer": {
"version": "11.1.15",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz",
"integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==",
"version": "11.1.16",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz",
"integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -28745,15 +28748,14 @@
}
},
"node_modules/mapbox-gl": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.27.0.tgz",
"integrity": "sha512-K8W9LTTjFEJsg9qsnJbKk+zbXrmSqa+nU1EiFXez5gQ0T0RMtylZUelgg1/RE6vCUMvHX0gaYfWU9g2mTWuA0g==",
"version": "3.28.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.28.1.tgz",
"integrity": "sha512-f8bCHFzZ51bKig7rnD7e08aoFLOV3MNFZduspZ4lgOgiaNVp9sw4NSWcgo3IWTyekYKKXjiICD2BP2o4DiYfxw==",
"license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [
"src/style-spec",
"plugins/mapbox-gl-pmtiles-provider",
"test/build/vite",
"test/build/webpack",
"test/bundlers/*",
"test/build/typings"
]
},
@@ -33666,12 +33668,12 @@
}
},
"node_modules/query-string": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.4.1.tgz",
"integrity": "sha512-lSyJeN3RuaG7DZGWThtYRhk96+kEyZ/+doZpERuWbjeFL+Ok3vEat/swU498rAI0NcVt5/RJp8UDuLz7FckxrA==",
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.5.0.tgz",
"integrity": "sha512-YlJmwNyi0RGYjlxYcuDncMsxFU7YyutbuI7gTm8ySxIGBlwx5yiBCOD5ig9ZNoHkawk/1Dey0N5mEfcUybMVAA==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.4.1",
"decode-uri-component": "^0.5.0",
"filter-obj": "^5.1.0",
"split-on-first": "^3.0.0"
},
@@ -37989,9 +37991,9 @@
}
},
"node_modules/storybook": {
"version": "10.5.6",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.6.tgz",
"integrity": "sha512-VhYwqxPySa24CVXKoWD6gCZXx9//DTmo43YpusGuAoHDYj5Osjt8wuBRQVeGoaLUWnHiPWv8S+GYHrJEaBM6Rg==",
"version": "10.5.7",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.7.tgz",
"integrity": "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -39964,9 +39966,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.23.5",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz",
"integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==",
"version": "4.23.10",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.10.tgz",
"integrity": "sha512-0Vb9eKU47njkxv/6B8CRZRDsxNDT/Pz+BIU+M5jw7xL3TdzAjSxlZUxu0xFL/kLpaG3sHZ0LH2wbK1T1yo7CUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -43229,11 +43231,11 @@
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^4.0.0",
"ace-builds": "^1.44.0",
"ag-grid-community": "36.0.2",
"ag-grid-react": "36.0.2",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"brace": "^0.11.1",
"classnames": "^2.5.1",
"core-js": "^3.49.0",
"core-js": "^3.50.0",
"csstype": "^3.2.3",
"d3-format": "^3.1.2",
"d3-interpolate": "^3.0.1",
@@ -43351,6 +43353,15 @@
"node": ">=12"
}
},
"packages/superset-ui-core/node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"packages/superset-ui-core/node_modules/react-ace": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz",
@@ -43934,7 +43945,7 @@
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.27.0",
"mapbox-gl": "^3.28.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.2",
"supercluster": "^8.0.1"
+16 -12
View File
@@ -78,6 +78,7 @@
"playwright:debug": "playwright test --debug",
"playwright:report": "playwright show-report",
"docs:screenshots": "playwright test --config=playwright/generators/playwright.config.ts docs/",
"playwright:thumbnails": "CAPTURE_THUMBNAILS=1 playwright test tests/tools/capture-viz-thumbnails.spec.ts --project chromium",
"prod": "npm run build",
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache",
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
@@ -156,9 +157,9 @@
"@visx/scale": "^4.0.0",
"@visx/tooltip": "^4.0.0",
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.0.2",
"ag-grid-react": "36.0.2",
"antd": "^6.5.3",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.5.4",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
@@ -177,7 +178,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.0",
"immer": "^11.1.15",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -185,7 +186,7 @@
"json-stringify-pretty-compact": "^4.0.0",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"mapbox-gl": "^3.27.0",
"mapbox-gl": "^3.28.1",
"markdown-to-jsx": "^9.10.2",
"match-sorter": "^8.3.0",
"memoize-one": "^6.0.0",
@@ -193,7 +194,7 @@
"mustache": "^4.2.0",
"nanoid": "^6.0.1",
"ol": "^10.10.0",
"query-string": "9.4.1",
"query-string": "9.5.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.16.0",
@@ -257,9 +258,9 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.6",
"@storybook/addon-links": "10.5.6",
"@storybook/react-webpack5": "10.5.6",
"@storybook/addon-docs": "10.5.7",
"@storybook/addon-links": "10.5.7",
"@storybook/react-webpack5": "10.5.7",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.47",
@@ -312,7 +313,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"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.6",
"eslint-plugin-storybook": "10.5.7",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -343,13 +344,13 @@
"source-map": "^0.8.0",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.5.6",
"storybook": "10.5.7",
"style-loader": "^4.0.0",
"stylelint": "^17.14.1",
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.12",
"tscw-config": "^1.1.2",
"tsx": "^4.23.5",
"tsx": "^4.23.10",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
@@ -412,6 +413,9 @@
"lerna": {
"js-yaml": "^4.3.0"
},
"minimatch@>=10": {
"brace-expansion": ">=5.0.8"
},
"nwsapi": "^2.2.13",
"puppeteer": "^22.4.1",
"tar": "^7.5.16",
@@ -0,0 +1,205 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useLayoutEffect, type ReactNode } from 'react';
import { render, screen, act } from '@testing-library/react';
import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
// SupersetThemeProvider stores theme state via React.useState, then
// registers a listener (in a useLayoutEffect, on mount) that calls
// setThemeState whenever a *later* call to setConfig/toggleDarkMode runs
// on the same Theme instance. Every provider currently mounted from that
// instance listens independently, so toggling the instance updates all of
// them, not just the most recently rendered one. Consumers
// (docs/src/components/StorybookWrapper.jsx in particular) rely on this:
// they call toggleDarkMode() from outside, on one or more already-mounted
// providers sharing a single Theme instance, expecting it to propagate to
// all of them.
//
// The probe below reads the theme via antd's theme.useToken() -- the same
// context-consumption path every real antd component (Button, Input, ...)
// uses internally -- rather than reading themeObject.theme directly off the
// singleton. That distinction matters: React bails out of re-rendering a
// child whose element reference didn't change (the common "static children
// prop" case, true here since <Probe /> is passed once and never
// recreated), UNLESS that child consumes a React Context whose value
// changed, which bypasses the bail-out. A probe reading the plain object
// directly would misleadingly appear "not updated" even though every real
// themed component downstream re-renders correctly.
function makeProbe() {
let renderCount = 0;
let lastColorBgBase: string | undefined;
function Probe() {
const { token } = antdThemeImport.useToken();
renderCount += 1;
lastColorBgBase = token.colorBgBase;
return <div data-test="probe" />;
}
return {
Probe,
getRenderCount: () => renderCount,
getLastColorBgBase: () => lastColorBgBase,
};
}
test('an already-mounted SupersetThemeProvider re-renders context-consuming children when toggleDarkMode is called on the same instance', () => {
const themeObject = Theme.fromConfig();
const { Probe, getRenderCount, getLastColorBgBase } = makeProbe();
render(
<themeObject.SupersetThemeProvider>
<Probe />
</themeObject.SupersetThemeProvider>,
);
expect(screen.getByTestId('probe')).toBeTruthy();
const rendersBefore = getRenderCount();
const tokenBefore = getLastColorBgBase();
act(() => {
themeObject.toggleDarkMode(true);
});
expect(getRenderCount()).toBeGreaterThan(rendersBefore);
expect(getLastColorBgBase()).not.toBe(tokenBefore);
});
test('toggleDarkMode updates every concurrently mounted provider for the same theme instance', () => {
const themeObject = Theme.fromConfig();
const first = makeProbe();
const second = makeProbe();
render(
<>
<themeObject.SupersetThemeProvider>
<first.Probe />
</themeObject.SupersetThemeProvider>
<themeObject.SupersetThemeProvider>
<second.Probe />
</themeObject.SupersetThemeProvider>
</>,
);
const firstTokenBefore = first.getLastColorBgBase();
const secondTokenBefore = second.getLastColorBgBase();
act(() => {
themeObject.toggleDarkMode(true);
});
// Both providers share the same Theme instance, so both must pick up the
// toggle -- not just whichever one rendered last.
expect(first.getLastColorBgBase()).not.toBe(firstTokenBefore);
expect(second.getLastColorBgBase()).not.toBe(secondTokenBefore);
});
test('a toggleDarkMode call on a different theme instance does not affect a mounted provider', () => {
const mounted = Theme.fromConfig();
const other = Theme.fromConfig();
const { Probe, getRenderCount, getLastColorBgBase } = makeProbe();
render(
<mounted.SupersetThemeProvider>
<Probe />
</mounted.SupersetThemeProvider>,
);
const rendersBefore = getRenderCount();
const tokenBefore = getLastColorBgBase();
act(() => {
other.toggleDarkMode(true);
});
// Each Theme instance owns its own set of provider listeners; toggling a
// *different* instance must not re-render a provider mounted from another.
expect(getRenderCount()).toBe(rendersBefore);
expect(getLastColorBgBase()).toBe(tokenBefore);
});
test('a toggleDarkMode call after a provider unmounts does not throw and no longer updates it', () => {
const themeObject = Theme.fromConfig();
const { Probe, getRenderCount, getLastColorBgBase } = makeProbe();
const { unmount } = render(
<themeObject.SupersetThemeProvider>
<Probe />
</themeObject.SupersetThemeProvider>,
);
const rendersBefore = getRenderCount();
const tokenBefore = getLastColorBgBase();
unmount();
expect(() => {
act(() => {
themeObject.toggleDarkMode(true);
});
}).not.toThrow();
// The unmounted provider's listener was deregistered, so it shouldn't
// have re-rendered (or updated) in response to the toggle.
expect(getRenderCount()).toBe(rendersBefore);
expect(getLastColorBgBase()).toBe(tokenBefore);
});
test('a toggleDarkMode call from an ancestor layout effect during the initial commit is not dropped', () => {
// Regression harness for the initial-mount race: StorybookWrapper.jsx
// toggles the singleton from its own layout effect (ThemeSync) as soon
// as a demo mounts. SupersetThemeProvider must have its listener
// registered *before* that ancestor effect fires, which only holds if
// registration itself runs in a layout effect -- layout effects fire
// bottom-up, so this component (nested inside the toggling ancestor)
// registers first. If that registration ever regresses to a plain
// useEffect, it runs after the ancestor's toggle (passive effects are
// deferred until after all layout effects), the notification is
// dropped, and this probe would still show the pre-toggle palette.
const lightBaseline = Theme.fromConfig();
const baseline = makeProbe();
render(
<lightBaseline.SupersetThemeProvider>
<baseline.Probe />
</lightBaseline.SupersetThemeProvider>,
);
const lightColorBgBase = baseline.getLastColorBgBase();
const themeObject = Theme.fromConfig();
const { Probe, getLastColorBgBase } = makeProbe();
function AncestorToggler({ children }: { children: ReactNode }) {
useLayoutEffect(() => {
themeObject.toggleDarkMode(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return children;
}
render(
<AncestorToggler>
<themeObject.SupersetThemeProvider>
<Probe />
</themeObject.SupersetThemeProvider>
</AncestorToggler>,
);
expect(getLastColorBgBase()).not.toBe(lightColorBgBase);
});
@@ -243,6 +243,47 @@ test('Theme.toggleDarkMode preserves other algorithms when toggling dark mode',
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DARK);
});
test('Theme.toggleDarkMode is a no-op when the requested mode is already active', () => {
// Pages with many live component demos (see docs/src/components/
// StorybookWrapper.jsx's ThemeSync) mount one dark-mode-sync bridge per
// demo, so a single toggle event can call toggleDarkMode once per demo
// with the same isDark value. Only the first of those calls should
// actually recompute the theme and fan out to providers.
const theme = Theme.fromConfig();
const setConfigSpy = jest.spyOn(theme, 'setConfig');
theme.toggleDarkMode(true);
expect(setConfigSpy).toHaveBeenCalledTimes(1);
// Repeating the same toggle should not recompute the theme again.
theme.toggleDarkMode(true);
theme.toggleDarkMode(true);
expect(setConfigSpy).toHaveBeenCalledTimes(1);
// Toggling to the other mode should still go through.
theme.toggleDarkMode(false);
expect(setConfigSpy).toHaveBeenCalledTimes(2);
setConfigSpy.mockRestore();
});
test('Theme.toggleDarkMode no-op check accounts for other algorithms in the array', () => {
// Start already in dark mode alongside a non-mode algorithm (compact).
const theme = Theme.fromConfig({
algorithm: [
antdThemeImport.compactAlgorithm,
antdThemeImport.darkAlgorithm,
],
});
const setConfigSpy = jest.spyOn(theme, 'setConfig');
// Already dark, so this should be a no-op rather than reordering the array.
theme.toggleDarkMode(true);
expect(setConfigSpy).not.toHaveBeenCalled();
setConfigSpy.mockRestore();
});
test('Theme.toSerializedConfig serializes theme config correctly', () => {
const theme = Theme.fromConfig({
token: {
@@ -25,7 +25,7 @@ import {
CacheProvider as EmotionCacheProvider,
} from '@emotion/react';
import createCache from '@emotion/cache';
import { noop, mergeWith } from 'lodash-es';
import { mergeWith } from 'lodash-es';
import { GlobalStyles } from './GlobalStyles';
import {
AntdThemeConfig,
@@ -156,8 +156,8 @@ export class Theme {
}),
} as SupersetTheme;
// Update the providers with the fully formed theme
this.updateProviders(
// Update every mounted provider with the fully formed theme
this.notifyProviders(
this.theme,
this.antdConfig,
createCache({ key: 'superset' }),
@@ -196,6 +196,27 @@ export class Theme {
newConfig.algorithm = newAlgorithm;
}
// Skip the update (and the notifyProviders fan-out it triggers) if the
// theme is already in the requested mode. Docs pages mount one
// dark-mode-sync bridge per live component demo (see
// docs/src/components/StorybookWrapper.jsx's ThemeSync), so a single
// toggle event calls this once per demo on the page. Without this
// check, every one of those calls would recompute the theme and
// notify every mounted provider, turning a single real toggle into
// O(n^2) provider notifications across n demos.
// Compare the algorithm sets rather than positions: reordering
// non-mode algorithms to the front doesn't change the effective
// theme, so it shouldn't count as a change either.
const currentAlgorithm = this.antdConfig.algorithm;
const algorithmUnchanged = Array.isArray(newConfig.algorithm)
? Array.isArray(currentAlgorithm) &&
newConfig.algorithm.length === currentAlgorithm.length &&
newConfig.algorithm.every(alg => currentAlgorithm.includes(alg))
: newConfig.algorithm === currentAlgorithm;
if (algorithmUnchanged) {
return;
}
// Update the theme with the new configuration
this.setConfig(newConfig);
}
@@ -204,13 +225,29 @@ export class Theme {
return JSON.stringify(serializeThemeConfig(this.antdConfig), null, 2);
}
private updateProviders(
// Every currently-mounted SupersetThemeProvider for this Theme instance
// registers a listener here (see the useEffect below). A single
// "last write wins" callback isn't enough once more than one provider can
// be mounted from the same Theme instance at a time -- e.g. multiple live
// component demos on one docs page -- since each render would overwrite
// the previous provider's callback and only the most-recently-rendered
// provider would ever hear about a setConfig/toggleDarkMode call.
private providerListeners = new Set<
(
theme: SupersetTheme,
antdConfig: AntdThemeConfig,
emotionCache: any,
) => void
>();
private notifyProviders(
theme: SupersetTheme,
antdConfig: AntdThemeConfig,
emotionCache: any,
): void {
noop(theme, antdConfig, emotionCache);
// Overridden at runtime by SupersetThemeProvider using setThemeState
this.providerListeners.forEach(listener =>
listener(theme, antdConfig, emotionCache),
);
}
SupersetThemeProvider({ children }: { children: React.ReactNode }) {
@@ -225,9 +262,42 @@ export class Theme {
emotionCache: createCache({ key: 'superset' }),
});
this.updateProviders = (theme, antdConfig, emotionCache) => {
setThemeState({ theme, antdConfig, emotionCache });
};
// Register (and, on unmount, deregister) this provider instance's own
// listener rather than assigning a single shared callback on every
// render, so every concurrently mounted provider for this Theme
// instance receives updates, not just the last one to render.
//
// Use useLayoutEffect (not useEffect) so registration happens in the
// same commit phase as any layout effect elsewhere that might call
// setConfig/toggleDarkMode on this instance during mount (e.g. the
// docs site's dark-mode sync in StorybookWrapper.jsx, which reads the
// toggle and pushes it onto the singleton via a layout effect of its
// own). Layout effects run bottom-up, so a listener registered here
// (this component is nested inside that caller) is guaranteed to be
// in place before an ancestor's layout effect can fire and notify it.
// If this were a passive effect instead, an ancestor's layout effect
// could call toggleDarkMode before this listener exists, dropping that
// notification, and the provider would render stale until a later
// toggle.
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useLayoutEffect(() => {
const listener = (
nextTheme: SupersetTheme,
nextAntdConfig: AntdThemeConfig,
nextEmotionCache: any,
) => {
setThemeState({
theme: nextTheme,
antdConfig: nextAntdConfig,
emotionCache: nextEmotionCache,
});
};
this.providerListeners.add(listener);
return () => {
this.providerListeners.delete(listener);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<EmotionCacheProvider value={themeState.emotionCache}>
@@ -0,0 +1,79 @@
/**
* 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 { QueryFormMetric } from '@superset-ui/core';
import { getTotalsMetrics } from './getTotalsMetrics';
const simpleMetric = (aggregate: string): QueryFormMetric =>
({
label: 'simple_metric',
expressionType: 'SIMPLE',
column: { column_name: 'col' },
aggregate,
}) as QueryFormMetric;
const sqlMetric = (): QueryFormMetric =>
({
label: 'sql_metric',
expressionType: 'SQL',
sqlExpression: 'SUM(col) / COUNT(*)',
}) as QueryFormMetric;
const savedMetric = (): QueryFormMetric => 'saved_metric';
describe('getTotalsMetrics', () => {
test('overrides the aggregate on simple (adhoc) metrics', () => {
const [result] = getTotalsMetrics([simpleMetric('SUM')], 'AVG');
expect(result).toEqual(
expect.objectContaining({ aggregate: 'AVG', expressionType: 'SIMPLE' }),
);
});
test('is a no-op when the simple metric already uses the requested aggregate', () => {
const [result] = getTotalsMetrics([simpleMetric('SUM')], 'SUM');
expect(result).toEqual(
expect.objectContaining({ aggregate: 'SUM', expressionType: 'SIMPLE' }),
);
});
test('leaves custom SQL metrics unchanged', () => {
const metric = sqlMetric();
const [result] = getTotalsMetrics([metric], 'AVG');
expect(result).toBe(metric);
});
test('leaves saved (string) metrics unchanged', () => {
const metric = savedMetric();
const [result] = getTotalsMetrics([metric], 'AVG');
expect(result).toBe(metric);
});
test('handles a mix of metric types, only rewriting simple metrics', () => {
const metrics = [simpleMetric('SUM'), sqlMetric(), savedMetric()];
const result = getTotalsMetrics(metrics, 'AVG');
expect(result).toHaveLength(3);
expect(result[0]).toEqual(expect.objectContaining({ aggregate: 'AVG' }));
expect(result[1]).toBe(metrics[1]);
expect(result[2]).toBe(metrics[2]);
});
test('returns an empty array when given no metrics', () => {
expect(getTotalsMetrics([], 'AVG')).toEqual([]);
});
});
@@ -0,0 +1,43 @@
/**
* 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 { isAdhocMetricSimple, QueryFormMetric } from '@superset-ui/core';
export type TotalsAggregate = 'SUM' | 'AVG';
/**
* Build the metrics for a chart's "Show summary" totals query, overriding
* each Simple (adhoc) metric's aggregate function with the user-chosen
* totals aggregate. The totals query has no GROUP BY, so the database
* evaluates each metric fresh over all rows -- swapping the aggregate here
* is a correct, independent computation, not a re-aggregation of
* already-aggregated per-row values.
*
* Custom-SQL metrics and saved (string) metrics pass through unchanged:
* there is no safe way to rewrite an arbitrary SQL expression's aggregate
* function without parsing it, so the totals row keeps their own native
* aggregate for those.
*/
export function getTotalsMetrics(
metrics: QueryFormMetric[],
aggregate: TotalsAggregate,
): QueryFormMetric[] {
return metrics.map(metric =>
isAdhocMetricSimple(metric) ? { ...metric, aggregate } : metric,
);
}
@@ -22,6 +22,7 @@ export * from './selectOptions';
export * from './D3Formatting';
export * from './expandControlConfig';
export * from './getColorFormatters';
export * from './getTotalsMetrics';
export { default as mainMetric } from './mainMetric';
export { default as columnChoices, columnsByType } from './columnChoices';
export * from './defineSavedMetrics';
@@ -56,11 +56,11 @@
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^4.0.0",
"ace-builds": "^1.44.0",
"ag-grid-community": "36.0.2",
"ag-grid-react": "36.0.2",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"brace": "^0.11.1",
"classnames": "^2.5.1",
"core-js": "^3.49.0",
"core-js": "^3.50.0",
"csstype": "^3.2.3",
"d3-format": "^3.1.2",
"d3-interpolate": "^3.0.1",
@@ -45,6 +45,13 @@ export interface ContextMenuFilters {
filters: BinaryQueryObjectFilterClause[];
groupbyFieldName: string;
adhocFilterFieldName?: string;
/**
* Filters scoped to the clicked x-axis value (category or time bucket),
* as opposed to `filters`, which are scoped to the clicked series.
* When both are present, the Drill By UI lets the user choose which
* of the two (or both) to apply to the drilled chart.
*/
xAxisFilters?: BinaryQueryObjectFilterClause[];
};
}
@@ -57,6 +57,9 @@ export interface AceCompleterKeywordData {
meta: string;
docText?: string;
docHTML?: string;
// The label Ace displays in the autocomplete popup and passes back to
// `insertMatch`; may differ from `value`, the text actually inserted.
caption?: string;
}
export type TextMode = OrigTextMode & { $id: string };
@@ -29,6 +29,9 @@ export interface AceCompleterKeywordData {
meta: string;
docText?: string;
docHTML?: string;
// The label Ace displays in the autocomplete popup and passes back to
// `insertMatch`; may differ from `value`, the text actually inserted.
caption?: string;
}
export type TextMode = OrigTextMode & { $id: string };
@@ -17,12 +17,45 @@
* under the License.
*/
import { createRef } from 'react';
import { render, fireEvent, screen } from '@superset-ui/core/spec';
import { NoAnimationDropdown } from '.';
import { MenuDotsDropdown, NoAnimationDropdown } from '.';
const props = {
overlay: <div>Test Overlay</div>,
};
describe('MenuDotsDropdown', () => {
test('renders a focusable, labeled button trigger', () => {
render(<MenuDotsDropdown {...props} />);
expect(screen.getByTestId('dropdown-trigger')).toEqual(
screen.getByRole('button', { name: 'Actions' }),
);
});
test('forwards a ref to the trigger so callers can focus it programmatically', () => {
const ref = createRef<HTMLButtonElement>();
render(<MenuDotsDropdown {...props} ref={ref} />);
ref.current?.focus();
expect(screen.getByTestId('dropdown-trigger')).toHaveFocus();
});
test('opens the menu when activated with the keyboard', async () => {
// Callers (e.g. the SQL Lab tab menu) open the dropdown on click, since
// antd's default trigger is hover, which keyboard activation can't
// reach.
render(<MenuDotsDropdown {...props} trigger={['click']} />);
const trigger = screen.getByTestId('dropdown-trigger');
trigger.focus();
// A native <button> converts an Enter keypress into a click once
// activated, so we simulate that browser behavior directly since
// jsdom does not implement it for us.
fireEvent.keyDown(trigger, { key: 'Enter', code: 'Enter' });
fireEvent.click(trigger);
expect(await screen.findByText('Test Overlay')).toBeInTheDocument();
});
});
describe('NoAnimationDropdown', () => {
test('requires children', () => {
expect(() => {
@@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactElement, cloneElement } from 'react';
import { ReactElement, cloneElement, forwardRef } from 'react';
import { Dropdown as AntdDropdown, DropdownProps } from 'antd';
import { styled } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import { Icons } from '@superset-ui/core/components/Icons';
import {
IconOrientation,
@@ -65,11 +66,14 @@ const MenuDots = styled.div`
}
`;
const MenuDotsWrapper = styled.div`
const MenuDotsWrapper = styled.button`
display: flex;
align-items: center;
padding: ${({ theme }) => theme.sizeUnit * 2}px;
padding-left: ${({ theme }) => theme.sizeUnit}px;
border: none;
background: transparent;
cursor: pointer;
`;
const RenderIcon = (
@@ -84,17 +88,23 @@ const RenderIcon = (
return component;
};
export const MenuDotsDropdown = ({
overlay,
iconOrientation = IconOrientation.Vertical,
...rest
}: MenuDotsDropdownProps) => (
export const MenuDotsDropdown = forwardRef<
HTMLButtonElement,
MenuDotsDropdownProps
>(({ overlay, iconOrientation = IconOrientation.Vertical, ...rest }, ref) => (
<AntdDropdown popupRender={() => overlay} {...rest}>
<MenuDotsWrapper data-test="dropdown-trigger">
<MenuDotsWrapper
ref={ref}
type="button"
aria-label={t('Actions')}
data-test="dropdown-trigger"
>
{RenderIcon(iconOrientation)}
</MenuDotsWrapper>
</AntdDropdown>
);
));
MenuDotsDropdown.displayName = 'MenuDotsDropdown';
export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => {
const { children, onBlur, onKeyDown, ...rest } = props;
@@ -49,6 +49,7 @@ const titleStyles = (theme: SupersetTheme) => css`
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
font-weight: inherit;
color: ${theme.colorText};
background-color: ${theme.colorBgContainer};
@@ -127,6 +128,21 @@ export const DynamicEditableTitle = memo(
}
}, [currentTitle, placeholder]);
// Webfont metrics differ from the fallback font's, so a measurement
// taken before fonts finish loading under- or over-sizes the input.
// Re-measure once all fonts are ready.
useEffect(() => {
let cancelled = false;
document.fonts?.ready?.then(() => {
if (!cancelled && sizerRef.current) {
setInputWidth(sizerRef.current.offsetWidth);
}
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const inputElement = inputRef.current?.input;
@@ -20,6 +20,7 @@ import { ReactNode, ReactElement, memo } from 'react';
import { t } from '@apache-superset/core/translation';
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { FeatureFlag, isFeatureEnabled } from '../../utils/featureFlags';
import type { DropdownProps } from '../Dropdown/types';
import type { TooltipPlacement } from '../Tooltip/types';
import type { CertifiedBadgeProps } from '../CertifiedBadge/types';
@@ -82,6 +83,20 @@ const headerStyles = (theme: SupersetTheme) => css`
display: flex;
align-items: center;
}
/* Mobile consumption mode: center the title between left/right panels */
${
isFeatureEnabled(FeatureFlag.MobileConsumptionMode) &&
css`
@media (max-width: ${theme.screenSMMax}px) {
.title-panel {
flex: 1;
justify-content: center;
margin-right: 0;
}
}
`
}
`;
const buttonsStyles = (theme: SupersetTheme) => css`
@@ -109,6 +124,7 @@ export type PageHeaderWithActionsProps = {
showFaveStar: boolean;
showMenuDropdown?: boolean;
faveStarProps: FaveStarProps;
leftPanelItems?: ReactNode;
titlePanelAdditionalItems: ReactNode;
rightPanelAdditionalItems: ReactNode;
additionalActionsMenu: ReactElement;
@@ -126,6 +142,7 @@ export const PageHeaderWithActions = memo(
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
leftPanelItems,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
@@ -136,6 +153,7 @@ export const PageHeaderWithActions = memo(
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
{leftPanelItems}
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
@@ -989,6 +989,52 @@ test('shows all options when filterOption is false', async () => {
expect(options[0]).toHaveTextContent('Server 0');
});
test('renders a server-matched option whose label diverges from the search term when filterOption is false (regression for #42041)', async () => {
// Mirrors the real permissions-search bug: the remote fetch legitimately
// matches the raw, underscore-containing value (e.g. a schema name like
// "stg_silver"), but the returned option's displayed label has had
// underscores replaced with spaces (see formatPermissionLabel in
// features/roles/utils.ts). filterOption defaults to true, which
// re-filters already-matched options against that same relabeled text
// client-side, so the underscore search term never matches and the
// legitimately fetched option gets hidden -- this is why
// PermissionsField (features/roles/RoleFormItems.tsx) sets
// filterOption={false}: the loader is already the authoritative filter,
// and its match doesn't depend on the label used to render the option.
const searchData = [{ label: 'stg silver', value: 100 }];
const loadOptions = jest.fn(async (search: string) =>
// totalCount must exceed the empty initial page here, otherwise
// AsyncSelect marks allValuesLoaded and short-circuits every later
// fetch, including the search request this test depends on.
search === ''
? { data: [], totalCount: 1 }
: { data: searchData, totalCount: 1 },
);
render(
<AsyncSelect
{...defaultProps}
options={loadOptions}
filterOption={false}
/>,
);
await open();
await type('stg_silver');
await waitFor(() =>
expect(loadOptions).toHaveBeenCalledWith(
'stg_silver',
expect.anything(),
expect.anything(),
),
);
// The backend legitimately matched and returned this option (asserted
// above); it should render in the dropdown despite the search term using
// underscores while the label uses spaces.
expect(await findSelectOption('stg silver')).toBeInTheDocument();
});
test('preserves new option entry across search fetch when allowNewOptions is on', async () => {
const page0Data = Array.from({ length: 10 }, (_, i) => ({
label: `Option ${i}`,
@@ -57,6 +57,7 @@ export enum FeatureFlag {
GranularExportControls = 'GRANULAR_EXPORT_CONTROLS',
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
Matrixify = 'MATRIXIFY',
MobileConsumptionMode = 'MOBILE_CONSUMPTION_MODE',
ScheduledQueries = 'SCHEDULED_QUERIES',
SemanticLayers = 'SEMANTIC_LAYERS',
SoftDelete = 'SOFT_DELETE',
@@ -336,3 +336,29 @@ test('getErrorText', async () => {
),
).toEqual('Sorry, an unknown error occurred.');
});
test('getErrorText for a non-JSON 403 response', async () => {
// A 403 originating outside Superset (reverse proxy, WAF, SSO gateway)
// carries an HTML or plain-text body instead of the API's JSON
// `{"message": "Forbidden"}`, so it must fall back to the generic
// status-derived text rather than the permission-denied copy.
const proxyForbidden = new Response(
'<html><head><title>403 Forbidden</title></head><body>Forbidden</body></html>',
{
status: 403,
statusText: 'Forbidden',
headers: { 'Content-Type': 'text/html' },
},
);
expect(await getErrorText(proxyForbidden, 'dashboard')).toEqual(
'Sorry, there was an error saving this dashboard: Forbidden',
);
const supersetForbidden = new Response(
JSON.stringify({ message: 'Forbidden' }),
{ status: 403, statusText: 'FORBIDDEN' },
);
expect(await getErrorText(supersetForbidden, 'dashboard')).toEqual(
'You do not have permission to edit this dashboard',
);
});
+18
View File
@@ -96,6 +96,7 @@ export default defineConfig({
'**/tests/auth/**/*.spec.ts',
'**/tests/sqllab/**/*.spec.ts',
'**/tests/embedded/**/*.spec.ts',
'**/tests/mobile/**/*.spec.ts',
...(process.env.INCLUDE_EXPERIMENTAL ? [] : ['**/experimental/**']),
],
use: {
@@ -156,6 +157,23 @@ export default defineConfig({
},
]
: []),
// Mobile consumption-mode tests need the MOBILE_CONSUMPTION_MODE feature
// flag enabled in the Flask backend (the workflow's mobile step sets
// SUPERSET_FEATURE_MOBILE_CONSUMPTION_MODE), so they only run when the
// environment opts in. Same strict 'true' check as INCLUDE_EMBEDDED.
...(process.env.INCLUDE_MOBILE?.toLowerCase() === 'true'
? [
{
name: 'chromium-mobile',
testMatch: '**/tests/mobile/**/*.spec.ts',
use: {
browserName: 'chromium' as const,
testIdAttribute: 'data-test',
storageState: 'playwright/.auth/user.json',
},
},
]
: []),
],
// Web server setup - disabled in CI (Flask started separately in workflow)
@@ -0,0 +1,173 @@
/**
* 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.
*/
/**
* Mobile Experience Documentation Screenshot Generator
*
* Captures phone-sized screenshots for the mobile consumption mode docs
* (docs/docs/using-superset/mobile-experience.mdx). Depends on example data
* loaded via `superset load_examples` AND the MOBILE_CONSUMPTION_MODE
* feature flag being enabled in the target environment:
*
* FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}
*
* Run locally:
* cd superset-frontend
* PLAYWRIGHT_BASE_URL=http://localhost:8088 PLAYWRIGHT_ADMIN_PASSWORD=admin npm run docs:screenshots
*
* Screenshots are saved under docs/static/img/screenshots/mobile/.
*/
import fs from 'fs';
import path from 'path';
import { Page, test, expect } from '@playwright/test';
import { URL } from '../../utils/urls';
const MOBILE_SCREENSHOTS_DIR = path.resolve(
__dirname,
'../../../../docs/static/img/screenshots/mobile',
);
// Committed to the repo alongside the generated images, but create it
// defensively in case someone deletes the directory and re-runs this
// generator standalone (Playwright does not create missing parent
// directories for screenshot paths).
fs.mkdirSync(MOBILE_SCREENSHOTS_DIR, { recursive: true });
// iPhone 12-class viewport; 2x scale factor for crisp docs images
test.use({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 2,
hasTouch: true,
});
/**
* Waits for animations and async renders to settle before taking a
* screenshot. ECharts entry animations, drawer transitions, and image
* lazy-loading require a short pause that can't be expressed as a
* deterministic wait condition.
*/
async function settle(page: Page, ms = 1000): Promise<void> {
await page.waitForTimeout(ms);
}
/**
* Opens the Sales Dashboard (from example data) at phone size and waits for
* the stacked charts to finish rendering.
*/
async function openSalesDashboardMobile(page: Page): Promise<void> {
await page.goto(URL.DASHBOARD_LIST);
// Mobile list is card-only; cards navigate on tap (titles are plain
// text, not links, in consumption mode)
const dashboardCard = page.getByText('Sales Dashboard', { exact: true });
await expect(dashboardCard.first()).toBeVisible({ timeout: 15000 });
await dashboardCard.first().click();
await expect(
page.locator('[data-test="dashboard-content-wrapper"]'),
).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 30000 });
}
test('mobile dashboard screenshot', async ({ page }) => {
await openSalesDashboardMobile(page);
await settle(page, 2000);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_dashboard.jpg'),
type: 'jpeg',
});
});
test('mobile dashboard filter drawer screenshot', async ({ page }) => {
await openSalesDashboardMobile(page);
const filterTrigger = page.locator('[data-test="mobile-filters-trigger"]');
await expect(filterTrigger).toBeVisible({ timeout: 15000 });
await filterTrigger.click();
// Wait for the drawer and its filter controls to render
await expect(page.locator('.ant-drawer-body')).toBeVisible({
timeout: 10000,
});
await expect(page.locator('[data-test="filter-bar"]')).toBeVisible({
timeout: 10000,
});
// Park the pointer so no hover card is open in the capture
await page.mouse.move(5, 830);
await settle(page);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_filter_drawer.jpg'),
type: 'jpeg',
});
});
test('mobile dashboard list screenshot', async ({ page }) => {
await page.goto(URL.DASHBOARD_LIST);
// Card view is forced on mobile; wait for cards to render
await expect(page.locator('[data-test="styled-card"]').first()).toBeVisible({
timeout: 15000,
});
await settle(page);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_dashboard_list.jpg'),
type: 'jpeg',
});
});
test('mobile home screenshot', async ({ page }) => {
await page.goto(URL.WELCOME);
await expect(page.getByText('Recents')).toBeVisible({ timeout: 15000 });
await settle(page, 2000);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_home.jpg'),
type: 'jpeg',
});
});
test('mobile navigation drawer screenshot', async ({ page }) => {
await page.goto(URL.WELCOME);
await expect(page.getByText('Recents')).toBeVisible({ timeout: 15000 });
const menuButton = page.getByRole('button', { name: 'Menu' });
await expect(menuButton).toBeVisible({ timeout: 10000 });
await menuButton.click();
await expect(page.locator('.ant-drawer-body')).toBeVisible({
timeout: 10000,
});
await expect(page.getByText('Dashboards').first()).toBeVisible();
await settle(page);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_nav_drawer.jpg'),
type: 'jpeg',
});
});
test('mobile unsupported route screenshot', async ({ page }) => {
await page.goto(URL.SQLLAB);
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: 15000 });
await settle(page);
await page.screenshot({
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_unsupported.jpg'),
type: 'jpeg',
});
});
@@ -103,16 +103,40 @@ export class EmbeddedPage {
/**
* Wait for dashboard content to render inside the iframe.
* Looks for the grid-container which indicates charts are loading/loaded.
*
* Races the grid against the test app's `#error` box so an embed failure
* surfaces its message immediately, instead of blindly timing out on the
* grid selector and hiding the real reason.
*/
async waitForDashboardContent(options?: { timeout?: number }): Promise<void> {
const frame = this.iframe;
await frame
const timeout = options?.timeout ?? EMBEDDED.DASHBOARD_RENDER;
const grid = this.iframe
.locator('.grid-container, [data-test="grid-container"]')
.first()
.waitFor({
state: 'visible',
timeout: options?.timeout ?? EMBEDDED.DASHBOARD_RENDER,
});
.first();
const errorBox = this.page.locator(EmbeddedPage.SELECTORS.ERROR);
const ready = grid
.waitFor({ state: 'visible', timeout })
.then(() => 'ready' as const)
.catch(() => 'gridTimeout' as const);
const failed = errorBox
.waitFor({ state: 'visible', timeout })
.then(() => 'error' as const)
.catch(() => 'errorTimeout' as const);
const outcome = await Promise.race([ready, failed]);
if (outcome === 'ready') return;
if (outcome === 'error') {
const message = (await errorBox.textContent())?.trim() || 'unknown error';
throw new Error(`Embedded dashboard failed to render: ${message}`);
}
const status = (
await this.page.locator(EmbeddedPage.SELECTORS.STATUS).textContent()
)?.trim();
throw new Error(
`Embedded dashboard did not render within ${timeout}ms ` +
`(status: ${status ?? 'unknown'})`,
);
}
/**
@@ -0,0 +1,162 @@
/**
* 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.
*/
/**
* Regression coverage for the Drill to Detail modal's results table.
*
* The drill pane measures its available height with a resize detector and
* hands it to a virtualized table, so the rows only render if the modal's
* internal height chain (resizable wrapper -> modal container -> modal body ->
* flex pane) actually resolves to a real height. That chain is wired together
* with CSS selectors targeting Ant Design's internal modal classes, which
* TypeScript cannot see and unit tests do not exercise: when the antd v6
* upgrade renamed `.ant-modal-content` to `.ant-modal-container`, the chain
* silently broke, the pane measured ~0, and the table briefly flashed its rows
* before collapsing to an empty body with only the header and pagination
* visible.
*
* Only a real browser sees layout, so this is pinned here rather than in the
* DOM-contract unit suite. Because the failure mode is
* render-then-collapse, a single "rows are visible" read could pass during
* the initial flash the assertion therefore lets the height settle once,
* then requires it to hold across further spaced reads with no more
* retrying, so a later or partial collapse cannot be masked by an early-exit
* retry that stopped at the first passing sample.
*
* CI green => the drill modal's table renders rows at a stable, non-collapsed
* height.
* CI red => the modal height chain broke again (or drill-to-detail failed to
* open/load at all).
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { TIMEOUT } from '../../utils/constants';
import { DashboardPage } from '../../pages/DashboardPage';
import { createDashboardWithCharts } from './dashboard-test-helpers';
const MIN_STABLE_BODY_HEIGHT = 100;
// Max fraction the body height may drift from the settled baseline below; a
// partial collapse (e.g. 400px -> 150px) still clears MIN_STABLE_BODY_HEIGHT
// but fails this, so the check enforces stability, not just a floor.
const HEIGHT_DRIFT_TOLERANCE = 0.25;
// Extra spaced reads taken *after* the height has settled, and the gap
// between them. These are plain assertions, not wrapped in a retrying
// helper: once settled, a retry would return on the first passing sample
// and could mask a collapse that only shows up later in the window.
const HEIGHT_SAMPLE_COUNT = 2;
const HEIGHT_SAMPLE_INTERVAL_MS = 300;
testWithAssets(
'drill to detail modal renders result rows at a stable height',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const { dashboardId } = await createDashboardWithCharts(
page,
testAssets,
testWithAssets.info(),
{
datasetName: 'birth_names',
chartNamePrefix: 'drill_detail',
dashboardTitlePrefix: 'drill_detail_modal',
chartSpecs: [
{
viz_type: 'pie',
params: {
groupby: ['gender'],
metric: 'count',
},
},
],
},
);
const dashboard = new DashboardPage(page);
await dashboard.gotoById(dashboardId);
await dashboard.waitForLoad();
await dashboard.waitForChartsToLoad();
// Open the chart context menu. The ECharts canvas exposes no data-test
// hooks for its regions, so right-click the centre of the chart container;
// the exact-text match below then works whether the click landed on a
// slice (which adds "Drill to detail by" items) or on the chart background.
// The first right-click after load can be swallowed by a chart re-render
// closing the menu, so retry the click until the menu actually shows.
const chart = page.locator('[data-test="chart-container"]').first();
await chart.scrollIntoViewIfNeeded();
const contextMenu = page.locator('[data-test="chart-context-menu"]');
await expect(async () => {
const box = await chart.boundingBox();
if (!box) {
throw new Error('chart container has no bounding box');
}
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, {
button: 'right',
});
await expect(contextMenu).toBeVisible({ timeout: 2000 });
}).toPass({ timeout: TIMEOUT.CHART_RENDER });
await page
.getByRole('menuitem', { name: 'Drill to detail', exact: true })
.click();
const modal = page.locator('.ant-modal:visible');
await expect(modal).toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
// Wait for the samples request to resolve into a rendered table: the row
// count pill and the virtualized body both come from the loaded page.
const tableBody = modal.locator('.virtual-grid');
await expect(tableBody).toBeAttached({ timeout: TIMEOUT.CHART_RENDER });
// The regression collapses the body *after* first paint, so first let the
// height settle above the floor (retrying is safe here: the collapse is
// persistent, so a broken build never finds a passing read and this
// still times out red), then, without any further retrying, take extra
// spaced reads and require each to hold within tolerance of that settled
// baseline — a delayed or partial collapse can no longer be masked by an
// early-exit retry that stopped at the first passing sample.
let baselineHeight = 0;
await expect
.poll(
async () => {
baselineHeight = (await tableBody.boundingBox())?.height ?? 0;
return baselineHeight;
},
{ timeout: TIMEOUT.CHART_RENDER },
)
.toBeGreaterThan(MIN_STABLE_BODY_HEIGHT);
for (let sample = 0; sample < HEIGHT_SAMPLE_COUNT; sample += 1) {
// eslint-disable-next-line no-await-in-loop -- reads must be sequential
// and spaced out to observe a delayed collapse; there is nothing to
// parallelize.
await page.waitForTimeout(HEIGHT_SAMPLE_INTERVAL_MS);
// eslint-disable-next-line no-await-in-loop -- see above
const box = await tableBody.boundingBox();
const height = box?.height ?? 0;
expect(height).toBeGreaterThan(MIN_STABLE_BODY_HEIGHT);
expect(Math.abs(height - baselineHeight)).toBeLessThanOrEqual(
baselineHeight * HEIGHT_DRIFT_TOLERANCE,
);
}
// And the rows are real data, not just an expanded empty scroller:
// birth_names sample rows always carry a gender value.
await expect(modal.getByText(/^(boy|girl)$/).first()).toBeVisible({
timeout: TIMEOUT.API_RESPONSE,
});
},
);
@@ -0,0 +1,284 @@
/**
* 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 { test, expect, devices, Page } from '@playwright/test';
// NOTE: These tests exercise the mobile consumption experience and require
// the MOBILE_CONSUMPTION_MODE feature flag to be enabled in the target
// environment (FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}).
import { TIMEOUT } from '../../utils/constants';
import { URL } from '../../utils/urls';
/**
* Mobile dashboard viewing tests verify that dashboards can be viewed
* and interacted with on mobile devices.
*
* These tests assume the World Bank's Health sample dashboard exists.
*/
// Use iPhone 12 viewport for mobile tests
const mobileViewport = devices['iPhone 12'];
/**
* Navigates to the dashboard list, clicks the first available dashboard
* card, and waits for navigation into that dashboard. Skips the current
* test when no dashboards are available to open.
*/
async function openFirstDashboard(page: Page): Promise<void> {
await page.goto(URL.DASHBOARD_LIST);
await page.waitForLoadState('networkidle');
const cards = page.locator('[data-test="styled-card"]');
const cardCount = await cards.count();
test.skip(cardCount === 0, 'No dashboards available to open on mobile');
await cards.first().click();
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
timeout: TIMEOUT.PAGE_LOAD,
});
}
/**
* Navigates to the World Bank's Health dashboard and returns a locator
* for its mobile filter button. Skips the current test when the fixture
* has no native filters configured.
*/
async function getMobileFilterButton(page: Page) {
// Navigate directly to the World Bank's Health dashboard, which this
// spec's fixtures require, rather than an arbitrary first card from
// the list. Whether it has native filters configured depends on the
// fixture, so callers skip themselves when none are present.
await page.goto('dashboard/world_health/');
await page.waitForLoadState('networkidle');
// Give filters time to load
await page.waitForTimeout(2000);
const filterButton = page
.locator('[data-test="mobile-filters-trigger"]')
.or(page.locator('[aria-label="Open filters"]'));
const filterCount = await filterButton.count();
test.skip(
filterCount === 0,
'world_health dashboard fixture has no native filters configured; ' +
'cannot verify mobile filter behavior.',
);
return filterButton;
}
test.describe('Mobile Dashboard Viewing', () => {
test.use({
viewport: mobileViewport.viewport,
userAgent: mobileViewport.userAgent,
});
test.beforeEach(async ({ page }) => {
// Navigate to dashboard list to find a dashboard
await page.goto(URL.DASHBOARD_LIST);
await page.waitForLoadState('networkidle');
});
test('dashboard list renders in card view on mobile', async ({ page }) => {
// On mobile, dashboard list should show cards, not table
// Look for card elements
const cards = page.locator('[data-test="styled-card"]');
// Should have at least one card if dashboards exist
// (This test may need adjustment based on test data availability)
const cardCount = await cards.count();
// Either cards are visible, or the empty state is shown; the table
// view must never render on mobile
if (cardCount > 0) {
await expect(cards.first()).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
} else {
await expect(page.locator('[data-test="empty-state"]')).toBeVisible({
timeout: TIMEOUT.PAGE_LOAD,
});
}
await expect(page.locator('[data-test="listview-table"]')).toHaveCount(0);
});
test('mobile search button appears in dashboard list', async ({ page }) => {
// On mobile, the search/filter button should appear in the header
const searchButton = page
.locator('[aria-label="Search"]')
.or(page.locator('[data-test="mobile-search-button"]'));
// Search button should be visible on mobile
await expect(searchButton.first()).toBeVisible({
timeout: TIMEOUT.PAGE_LOAD,
});
});
test('tapping dashboard card opens the dashboard', async ({ page }) => {
// Find a dashboard card
const cards = page.locator('[data-test="styled-card"]');
const cardCount = await cards.count();
if (cardCount > 0) {
// Click the first card
await cards.first().click();
// Should navigate to dashboard view
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
timeout: TIMEOUT.PAGE_LOAD,
});
// Dashboard should load (look for dashboard content)
await expect(
page
.locator('[data-test="dashboard-content-wrapper"]')
.or(page.locator('.dashboard')),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
} else {
test.skip();
}
});
});
test.describe('Mobile Dashboard Interaction', () => {
test.use({
viewport: mobileViewport.viewport,
userAgent: mobileViewport.userAgent,
});
// Skip this test suite if no dashboards exist
test.beforeAll(async ({ browser }) => {
// browser.newPage() does not inherit the project's `storageState`, so
// it must be passed explicitly to reuse the authenticated session -
// otherwise this check hits the login page and always finds 0 cards.
const page = await browser.newPage({
viewport: mobileViewport.viewport,
userAgent: mobileViewport.userAgent,
storageState: 'playwright/.auth/user.json',
});
await page.goto(URL.DASHBOARD_LIST);
await page.waitForLoadState('networkidle');
const cards = page.locator('[data-test="styled-card"]');
const cardCount = await cards.count();
await page.close();
if (cardCount === 0) {
test.skip();
}
});
test('dashboard loads and shows charts on mobile', async ({ page }) => {
await openFirstDashboard(page);
// Dashboard content should be visible
await expect(
page
.locator('[data-test="dashboard-content-wrapper"]')
.or(page.locator('.dashboard')),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
// Charts should start loading (look for chart containers)
const chartContainers = page
.locator('[data-test="chart-container"]')
.or(page.locator('.dashboard-chart'));
// Wait for at least one chart to be visible (with timeout)
await expect(chartContainers.first()).toBeVisible({
timeout: TIMEOUT.PAGE_LOAD * 2,
});
});
test('dashboard header shows hamburger menu on mobile', async ({ page }) => {
await openFirstDashboard(page);
// Look for the hamburger menu / more actions button
const menuButton = page
.locator('[data-test="actions-trigger"]')
.or(page.locator('[aria-label="Menu actions trigger"]'));
await expect(menuButton.first()).toBeVisible({
timeout: TIMEOUT.PAGE_LOAD,
});
});
test('refresh dashboard works from mobile menu', async ({ page }) => {
await openFirstDashboard(page);
// Open the actions menu
const menuButton = page
.locator('[data-test="actions-trigger"]')
.or(page.locator('[aria-label="Menu actions trigger"]'));
test.skip(
(await menuButton.count()) === 0,
'Mobile actions menu button not found on this dashboard',
);
await menuButton.first().click();
// Look for refresh option
const refreshOption = page.getByText('Refresh dashboard');
test.skip(
(await refreshOption.count()) === 0,
'Refresh dashboard option not found in mobile actions menu',
);
await refreshOption.click();
// Should show success toast or refresh the charts
// This is hard to verify without checking network requests
// Just verify the menu closes and we're still on the dashboard
await page.waitForTimeout(1000);
expect(page.url()).toMatch(/\/dashboard\/(?!list)/);
});
});
test.describe('Mobile Filter Drawer', () => {
test.use({
viewport: mobileViewport.viewport,
userAgent: mobileViewport.userAgent,
});
test('filter button appears on dashboards with filters', async ({ page }) => {
const filterButton = await getMobileFilterButton(page);
await expect(filterButton.first()).toBeVisible();
});
test('filter drawer opens when filter button is tapped', async ({ page }) => {
const filterButton = await getMobileFilterButton(page);
await filterButton.first().click();
// Filter drawer should open
const drawer = page
.locator('.ant-drawer-open')
.or(page.locator('[data-test="filter-bar"]'));
await expect(drawer.first()).toBeVisible({
timeout: TIMEOUT.FORM_LOAD,
});
});
});
@@ -0,0 +1,192 @@
/**
* 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 { test, expect, devices } from '@playwright/test';
// NOTE: These tests exercise the mobile consumption experience and require
// the MOBILE_CONSUMPTION_MODE feature flag to be enabled in the target
// environment (FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}).
import { URL } from '../../utils/urls';
import { TIMEOUT } from '../../utils/constants';
/**
* Mobile navigation tests verify the MobileRouteGuard behavior
* and mobile-specific navigation patterns.
*
* These tests run with a mobile viewport to trigger mobile-specific behavior.
*/
// Use iPhone 12 viewport for mobile tests
const mobileViewport = devices['iPhone 12'];
test.describe('Mobile Navigation', () => {
test.use({
viewport: mobileViewport.viewport,
userAgent: mobileViewport.userAgent,
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('mobile viewport redirects from chart list to MobileUnsupported page', async ({
page,
}) => {
// Navigate to chart list (not mobile-supported)
await page.goto(URL.CHART_LIST);
// Should show the MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
// Primary action buttons should be visible
await expect(
page.getByRole('button', { name: 'View Dashboards' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Go to Welcome Page' }),
).toBeVisible();
});
test('mobile viewport allows access to dashboard list', async ({ page }) => {
// Navigate to dashboard list (mobile-supported)
await page.goto(URL.DASHBOARD_LIST);
// Should NOT show MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
// Should show dashboard list content (look for dashboard list elements)
await expect(
page
.locator('[data-test="listview-table"]')
.or(page.locator('[data-test="styled-card"]'))
.first(),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
});
test('mobile viewport allows access to welcome page', async ({ page }) => {
// Navigate to welcome page (mobile-supported)
await page.goto(URL.WELCOME);
// Should NOT show MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
// Should show welcome page content
await expect(
page.getByText('Recents').or(page.getByText('Dashboards')).first(),
).toBeVisible({
timeout: TIMEOUT.PAGE_LOAD,
});
});
test('View Dashboards button navigates to dashboard list', async ({
page,
}) => {
// Navigate to unsupported route
await page.goto(URL.CHART_LIST);
// Wait for MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
// Click View Dashboards button
await page.getByRole('button', { name: 'View Dashboards' }).click();
// Should navigate to dashboard list
await page.waitForURL(url => url.pathname.includes('dashboard/list'), {
timeout: TIMEOUT.PAGE_LOAD,
});
// Dashboard list should be accessible
await expect(
page
.locator('[data-test="listview-table"]')
.or(page.locator('[data-test="styled-card"]'))
.first(),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
});
test('Go to Welcome Page button navigates to welcome', async ({ page }) => {
// Navigate to unsupported route
await page.goto(URL.CHART_LIST);
// Wait for MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
// Click Go to Welcome Page button
await page.getByRole('button', { name: 'Go to Welcome Page' }).click();
// Should navigate to welcome page
await page.waitForURL(url => url.pathname.includes('welcome'), {
timeout: TIMEOUT.PAGE_LOAD,
});
});
test('unsupported screen offers no bypass', async ({ page }) => {
// The "Continue anyway" bypass was removed: desktop views are unusable
// at phone width, and growing the viewport unblocks routes automatically
await page.goto(URL.CHART_LIST);
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
await expect(page.getByText('Continue anyway')).toHaveCount(0);
});
test('SQL Lab is not accessible on mobile', async ({ page }) => {
// Navigate to SQL Lab (not mobile-supported)
await page.goto(URL.SQLLAB);
// Should show the MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
});
});
test.describe('Desktop Navigation (control group)', () => {
// Use default desktop viewport
test('desktop viewport allows access to all routes', async ({ page }) => {
// Navigate to chart list
await page.goto(URL.CHART_LIST);
// Should NOT show MobileUnsupported page
await expect(
page.getByText("This view isn't available on mobile"),
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
// Should show chart list content
await expect(
page
.locator('[data-test="listview-table"]')
.or(page.locator('[data-test="styled-card"]'))
.first(),
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
});
});
@@ -0,0 +1,630 @@
/**
* 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.
*/
/**
* Crawls every dashboard on the target instance and refreshes viz-picker
* thumbnails and example galleries from live example charts.
*
* This is a maintenance tool, not a test: charts are DISCOVERED via the
* API (dashboards -> charts), so it keeps working as example dashboards
* evolve. Every distinct chart found for a viz type gets captured the
* preferred (or alphabetically-first) chart becomes the 512x512
* `thumbnail.png`, and each additional distinct chart fills the next
* `exampleGallery` slot declared for that viz type, so the gallery shows
* real variety instead of the same chart resized. The two static pieces
* are the viz type -> image path maps below (VIZ_TYPE_THUMBNAILS,
* VIZ_TYPE_GALLERY), which change when a plugin's images or gallery
* shape change never when examples change. Viz types found on
* dashboards but missing from both maps, and gallery slots left unfilled
* for lack of enough distinct example charts, are reported at the end
* without failing the run.
*
* It only runs when CAPTURE_THUMBNAILS=1 is set, so the regular
* Playwright suites never execute it.
*
* Usage (requires a running Superset with examples loaded):
* npm run playwright:thumbnails
* VIZ_TYPES=bullet,rose npm run playwright:thumbnails # subset
*
* Notes:
* - Dark variants (`thumbnail-dark.png`, `example-dark.jpg`) are captured
* via prefers-color-scheme emulation whenever the sibling file exists;
* if the app ignores the emulation (dark theming disabled) the dark
* file is left untouched.
* - A brand-new gallery image (rather than an existing file being
* refreshed) still needs to be registered in the plugin's metadata
* before it renders in the gallery; the capture logs a reminder.
*/
import * as fs from 'fs';
import * as path from 'path';
import { test, expect, Page } from '@playwright/test';
const THUMBNAIL_SIZE = 512;
const RENDERED_CHART_SELECTOR =
'[data-test="chart-container"]:has(svg, canvas, table):not(:has([data-test="loading-indicator"]))';
/**
* Charts that render plain markup no svg/canvas/table for the rendered
* selector to key on get a looser signal plus a longer settle.
*/
const TEXT_ONLY_VIZ_TYPES = new Set([
'ag-grid-table',
'big_number_total',
'handlebars',
'pop_kpi',
]);
/**
* Hover this element just before the screenshot, so charts whose identity
* benefits from an interaction (a visible tooltip) capture mid-hover.
*/
const HOVER_BEFORE_CAPTURE: Record<string, string> = {
cal_heatmap: '[data-test="chart-container"] svg rect[class*=" r"]',
};
const TEXT_RENDERED_CHART_SELECTOR =
'[data-test="chart-container"]:not(:has([data-test="loading-indicator"]))';
/** superset-frontend root, resolved from this spec's location. */
const FRONTEND_ROOT = path.resolve(__dirname, '..', '..', '..');
const ECHARTS = 'plugins/plugin-chart-echarts/src';
const DECKGL = 'plugins/preset-chart-deckgl/src/layers';
/**
* Where each viz type's gallery thumbnail lives, relative to
* superset-frontend. One entry per registered viz type with a thumbnail;
* add a line when a new plugin ships.
*/
const VIZ_TYPE_THUMBNAILS: Record<string, string> = {
'ag-grid-table':
'plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png',
big_number: `${ECHARTS}/BigNumber/BigNumberWithTrendline/images/thumbnail.png`,
big_number_total: `${ECHARTS}/BigNumber/BigNumberTotal/images/thumbnail.png`,
box_plot: `${ECHARTS}/BoxPlot/images/thumbnail.png`,
bubble_v2: `${ECHARTS}/Bubble/images/thumbnail.png`,
bullet: `${ECHARTS}/Bullet/images/thumbnail.png`,
cal_heatmap: 'plugins/plugin-chart-calendar/src/images/thumbnail.png',
cartodiagram: 'plugins/plugin-chart-cartodiagram/src/images/thumbnail.png',
chord: 'plugins/plugin-chart-chord/src/images/thumbnail.png',
country_map: 'plugins/plugin-chart-country-map/src/images/thumbnail.png',
deck_arc: `${DECKGL}/Arc/images/thumbnail.png`,
deck_contour: `${DECKGL}/Contour/images/thumbnail.png`,
deck_geojson: `${DECKGL}/Geojson/images/thumbnail.png`,
deck_grid: `${DECKGL}/Grid/images/thumbnail.png`,
deck_heatmap: `${DECKGL}/Heatmap/images/thumbnail.png`,
deck_hex: `${DECKGL}/Hex/images/thumbnail.png`,
deck_multi: 'plugins/preset-chart-deckgl/src/Multi/images/thumbnail.png',
deck_path: `${DECKGL}/Path/images/thumbnail.png`,
deck_polygon: `${DECKGL}/Polygon/images/thumbnail.png`,
deck_scatter: `${DECKGL}/Scatter/images/thumbnail.png`,
deck_screengrid: `${DECKGL}/Screengrid/images/thumbnail.png`,
echarts_area: `${ECHARTS}/Timeseries/Area/images/thumbnail.png`,
echarts_timeseries: `${ECHARTS}/Timeseries/images/thumbnail.png`,
echarts_timeseries_bar: `${ECHARTS}/Timeseries/Regular/Bar/images/thumbnail.png`,
echarts_timeseries_line: `${ECHARTS}/Timeseries/Regular/Line/images/thumbnail.png`,
echarts_timeseries_scatter: `${ECHARTS}/Timeseries/Regular/Scatter/images/thumbnail.png`,
echarts_timeseries_smooth: `${ECHARTS}/Timeseries/Regular/SmoothLine/images/thumbnail.png`,
echarts_timeseries_step: `${ECHARTS}/Timeseries/Step/images/thumbnail.png`,
funnel: `${ECHARTS}/Funnel/images/thumbnail.png`,
gantt_chart: `${ECHARTS}/Gantt/images/thumbnail.png`,
gauge_chart: `${ECHARTS}/Gauge/images/thumbnail.png`,
graph_chart: `${ECHARTS}/Graph/images/thumbnail.png`,
// handlebars intentionally unmapped: its generic logo represents the
// template-anything nature of the chart better than any one example.
heatmap_v2: `${ECHARTS}/Heatmap/images/thumbnail.png`,
histogram_v2: `${ECHARTS}/Histogram/images/thumbnail.png`,
horizon: 'plugins/plugin-chart-horizon/src/images/thumbnail.png',
mixed_timeseries: `${ECHARTS}/MixedTimeseries/images/thumbnail.png`,
paired_ttest: 'plugins/plugin-chart-paired-t-test/src/images/thumbnail.png',
para: 'plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png',
partition: 'plugins/plugin-chart-partition/src/images/thumbnail.png',
pie: `${ECHARTS}/Pie/images/thumbnail.png`,
pivot_table_v2: 'plugins/plugin-chart-pivot-table/src/images/thumbnail.png',
point_cluster_map:
'plugins/plugin-chart-point-cluster-map/src/images/thumbnail.png',
pop_kpi: `${ECHARTS}/BigNumber/BigNumberPeriodOverPeriod/images/thumbnail.png`,
radar: `${ECHARTS}/Radar/images/thumbnail.png`,
rose: `${ECHARTS}/Rose/images/thumbnail.png`,
sankey_v2: `${ECHARTS}/Sankey/images/thumbnail.png`,
sunburst_v2: `${ECHARTS}/Sunburst/images/thumbnail.png`,
table: 'plugins/plugin-chart-table/src/images/thumbnail.png',
time_pivot: `${ECHARTS}/TimePivot/images/thumbnail.png`,
time_table: 'src/visualizations/TimeTable/images/thumbnail.png',
tree_chart: `${ECHARTS}/Tree/images/thumbnail.png`,
treemap_v2: `${ECHARTS}/Treemap/images/thumbnail.png`,
waterfall: `${ECHARTS}/Waterfall/images/thumbnail.png`,
word_cloud: 'plugins/plugin-chart-word-cloud/src/images/thumbnail.png',
world_map: 'plugins/plugin-chart-world-map/src/images/thumbnail.png',
};
/**
* When several example charts share a viz type, prefer these slices over
* the default alphabetically-first pick. Missing slices fall back to the
* default, so stale entries degrade gracefully.
*/
const PREFERRED_SLICES: Record<string, string> = {
big_number: 'Sales Year over Year',
bubble_v2: 'Life Expectancy VS Rural %',
bullet: 'Total Sales Bullet',
cal_heatmap: 'Sales Calendar Heatmap',
chord: 'Product Line Chord',
echarts_area: 'Sales Stacked Area',
echarts_timeseries_bar: 'Sales Stacked Bars',
echarts_timeseries_line: 'Monthly Sales Line',
echarts_timeseries_smooth: 'Monthly Sales Smooth',
echarts_timeseries_step: 'Quarterly Sales Steps',
funnel: 'Population Funnel',
gauge_chart: 'Rural Population Gauge',
heatmap_v2: 'Sales Grid Heatmap',
histogram_v2: 'Life Expectancy Histogram',
horizon: 'Population Growth Horizon',
mixed_timeseries: 'Sales Mixed Chart',
paired_ttest: 'Population Paired t-Test',
partition: 'Population Partition',
pie: 'Product Line Donut',
pivot_table_v2: 'Sales Pivot Highlights',
radar: 'Game Sales Radar',
rose: 'Population Nightingale Rose',
sunburst_v2: 'Population Sunburst',
table: 'Sales Summary Table',
time_pivot: 'Sales Period Pivot',
time_table: 'Product Line Time Table',
tree_chart: 'Sales Territory Tree',
treemap_v2: 'Population Treemap',
waterfall: 'Quarterly Sales Waterfall',
};
/** Gallery example images use a wide aspect, matching the existing art. */
const EXAMPLE_WIDTH = 800;
const EXAMPLE_HEIGHT = 460;
/**
* Every plugin's `exampleGallery` image slots (never including
* `thumbnail.png`/`thumbnail-dark.png`, which stays governed by
* VIZ_TYPE_THUMBNAILS), in the order they appear in that plugin's
* `index.ts`. One entry per viz type that declares a gallery including
* `handlebars`, whose gallery is filled even though its picker thumbnail
* intentionally stays the generic logo (unmapped in VIZ_TYPE_THUMBNAILS).
*
* For a viz type with N slots here, the crawler assigns its (N+1)
* distinct dashboard charts (the thumbnail's pick, then this many more)
* to thumbnail, slot 1, slot 2, ... in that order, so gallery images show
* different charts than the thumbnail and each other rather than the same
* chart resized. Fewer distinct charts than slots just leaves the
* trailing slots untouched (reported, not failed) add more example
* charts to fill them.
*
* Update this when a plugin's exampleGallery array changes shape.
*/
const VIZ_TYPE_GALLERY: Record<string, string[]> = {
'ag-grid-table': [
'plugins/plugin-chart-ag-grid-table/src/images/Table.jpg',
'plugins/plugin-chart-ag-grid-table/src/images/Table2.jpg',
'plugins/plugin-chart-ag-grid-table/src/images/Table3.jpg',
],
big_number: [
`${ECHARTS}/BigNumber/BigNumberWithTrendline/images/Big_Number_Trendline.jpg`,
],
big_number_total: [
`${ECHARTS}/BigNumber/BigNumberTotal/images/BigNumber.jpg`,
`${ECHARTS}/BigNumber/BigNumberTotal/images/BigNumber2.jpg`,
],
box_plot: [`${ECHARTS}/BoxPlot/images/BoxPlot.jpg`],
bubble_v2: [
`${ECHARTS}/Bubble/images/example1.png`,
`${ECHARTS}/Bubble/images/example2.png`,
],
bullet: [`${ECHARTS}/Bullet/images/example.jpg`],
cal_heatmap: ['plugins/plugin-chart-calendar/src/images/example.jpg'],
cartodiagram: [
'plugins/plugin-chart-cartodiagram/src/images/example1.png',
'plugins/plugin-chart-cartodiagram/src/images/example2.png',
],
chord: ['plugins/plugin-chart-chord/src/images/chord.jpg'],
country_map: [
'plugins/plugin-chart-country-map/src/images/exampleUsa.jpg',
'plugins/plugin-chart-country-map/src/images/exampleGermany.jpg',
],
echarts_area: [`${ECHARTS}/Timeseries/Area/images/Area1.png`],
echarts_timeseries: [`${ECHARTS}/Timeseries/images/Time-series_Chart.jpg`],
echarts_timeseries_bar: [
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar1.png`,
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar2.png`,
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar3.png`,
],
echarts_timeseries_line: [
`${ECHARTS}/Timeseries/Regular/Line/images/Line1.png`,
`${ECHARTS}/Timeseries/Regular/Line/images/Line2.png`,
`${ECHARTS}/Timeseries/Regular/Line/images/Line3.png`,
],
echarts_timeseries_scatter: [
`${ECHARTS}/Timeseries/Regular/Scatter/images/Scatter1.png`,
],
echarts_timeseries_smooth: [
`${ECHARTS}/Timeseries/Regular/SmoothLine/images/SmoothLine1.png`,
],
echarts_timeseries_step: [
`${ECHARTS}/Timeseries/Step/images/Step1.png`,
`${ECHARTS}/Timeseries/Step/images/Step2.png`,
],
funnel: [`${ECHARTS}/Funnel/images/example.jpg`],
gantt_chart: [
`${ECHARTS}/Gantt/images/example1.png`,
`${ECHARTS}/Gantt/images/example2.png`,
],
gauge_chart: [
`${ECHARTS}/Gauge/images/example1.jpg`,
`${ECHARTS}/Gauge/images/example2.jpg`,
],
graph_chart: [`${ECHARTS}/Graph/images/example.jpg`],
handlebars: [
'plugins/plugin-chart-handlebars/src/images/example1.jpg',
'plugins/plugin-chart-handlebars/src/images/example2.jpg',
],
heatmap_v2: [
`${ECHARTS}/Heatmap/images/example1.png`,
`${ECHARTS}/Heatmap/images/example2.png`,
`${ECHARTS}/Heatmap/images/example3.png`,
],
histogram_v2: [
`${ECHARTS}/Histogram/images/example1.png`,
`${ECHARTS}/Histogram/images/example2.png`,
],
horizon: ['plugins/plugin-chart-horizon/src/images/Horizon_Chart.jpg'],
mixed_timeseries: [`${ECHARTS}/MixedTimeseries/images/example.jpg`],
paired_ttest: ['plugins/plugin-chart-paired-t-test/src/images/example.jpg'],
para: [
'plugins/plugin-chart-parallel-coordinates/src/images/example1.jpg',
'plugins/plugin-chart-parallel-coordinates/src/images/example2.jpg',
],
partition: ['plugins/plugin-chart-partition/src/images/example.jpg'],
pie: [
`${ECHARTS}/Pie/images/Pie1.jpg`,
`${ECHARTS}/Pie/images/Pie2.jpg`,
`${ECHARTS}/Pie/images/Pie3.jpg`,
`${ECHARTS}/Pie/images/Pie4.jpg`,
],
pivot_table_v2: ['plugins/plugin-chart-pivot-table/src/images/example.jpg'],
point_cluster_map: [
'plugins/plugin-chart-point-cluster-map/src/images/MapBox.jpg',
'plugins/plugin-chart-point-cluster-map/src/images/MapBox2.jpg',
],
radar: [
`${ECHARTS}/Radar/images/example1.jpg`,
`${ECHARTS}/Radar/images/example2.jpg`,
],
rose: [
`${ECHARTS}/Rose/images/example1.jpg`,
`${ECHARTS}/Rose/images/example2.jpg`,
],
sankey_v2: [
`${ECHARTS}/Sankey/images/example1.png`,
`${ECHARTS}/Sankey/images/example2.png`,
],
sunburst_v2: [
`${ECHARTS}/Sunburst/images/Sunburst1.png`,
`${ECHARTS}/Sunburst/images/Sunburst2.png`,
],
table: [
'plugins/plugin-chart-table/src/images/Table.jpg',
'plugins/plugin-chart-table/src/images/Table2.jpg',
'plugins/plugin-chart-table/src/images/Table3.jpg',
],
time_pivot: [`${ECHARTS}/TimePivot/images/example.jpg`],
time_table: ['src/visualizations/TimeTable/images/example.jpg'],
tree_chart: [`${ECHARTS}/Tree/images/tree.png`],
treemap_v2: [
`${ECHARTS}/Treemap/images/treemap_v2_1.png`,
`${ECHARTS}/Treemap/images/treemap_v2_2.jpg`,
],
waterfall: [
`${ECHARTS}/Waterfall/images/example1.png`,
`${ECHARTS}/Waterfall/images/example2.png`,
`${ECHARTS}/Waterfall/images/example3.png`,
],
word_cloud: [
'plugins/plugin-chart-word-cloud/src/images/Word_Cloud.jpg',
'plugins/plugin-chart-word-cloud/src/images/Word_Cloud_2.jpg',
],
world_map: [
'plugins/plugin-chart-world-map/src/images/WorldMap1.jpg',
'plugins/plugin-chart-world-map/src/images/WorldMap2.jpg',
],
deck_arc: [`${DECKGL}/Arc/images/example.png`],
deck_contour: [`${DECKGL}/Contour/images/example.png`],
deck_geojson: [`${DECKGL}/Geojson/images/example.png`],
deck_grid: [`${DECKGL}/Grid/images/example.png`],
deck_heatmap: [`${DECKGL}/Heatmap/images/example.png`],
deck_hex: [`${DECKGL}/Hex/images/example.png`],
deck_multi: ['plugins/preset-chart-deckgl/src/Multi/images/example.png'],
deck_path: [`${DECKGL}/Path/images/example.png`],
deck_polygon: [`${DECKGL}/Polygon/images/example.png`],
deck_scatter: [`${DECKGL}/Scatter/images/example.png`],
deck_screengrid: [`${DECKGL}/Screengrid/images/example.png`],
};
interface ExampleChart {
id: number;
sliceName: string;
vizType: string;
}
interface DashboardRow {
id: number;
}
interface DashboardChartRow {
id?: number;
slice_name?: string;
form_data?: { viz_type?: string };
}
/** Pages through a list endpoint, returning every result row. */
async function fetchAllPages<T>(page: Page, endpoint: string): Promise<T[]> {
const rows: T[] = [];
const pageSize = 100;
for (let pageNum = 0; ; pageNum += 1) {
const q = encodeURIComponent(
`(page_size:${pageSize},page:${pageNum},order_direction:asc)`,
);
const response = await page.request.get(`${endpoint}?q=${q}`);
expect(response.ok(), `GET ${endpoint} page ${pageNum}`).toBeTruthy();
const { result }: { result: T[] } = await response.json();
rows.push(...result);
if (result.length < pageSize) return rows;
}
}
/** Discovers every chart placed on any dashboard. */
async function discoverDashboardCharts(page: Page): Promise<ExampleChart[]> {
const dashboards = await fetchAllPages<DashboardRow>(
page,
'/api/v1/dashboard/',
);
const chartsById = new Map<number, ExampleChart>();
for (const dashboard of dashboards) {
const response = await page.request.get(
`/api/v1/dashboard/${dashboard.id}/charts`,
);
if (!response.ok()) continue;
const { result }: { result: DashboardChartRow[] } = await response.json();
for (const chart of result) {
const vizType = chart.form_data?.viz_type;
if (chart.id && chart.slice_name && vizType) {
chartsById.set(chart.id, {
id: chart.id,
sliceName: chart.slice_name,
vizType,
});
}
}
}
return [...chartsById.values()];
}
/** thumbnail.png -> thumbnail-dark.png, example.jpg -> example-dark.jpg */
function darkSibling(output: string): string {
return output.replace(/\.(png|jpg)$/, '-dark.$1');
}
async function renderAndShoot(
page: Page,
chart: ExampleChart,
colorScheme: 'light' | 'dark',
): Promise<Buffer> {
// An explicit navigation timeout keeps one hung load from stalling the
// whole crawl until the test timeout.
await page.goto(`/explore/?slice_id=${chart.id}&standalone=1`, {
timeout: 60_000,
});
// Dashboards render charts on colorBgContainer cards, but the standalone
// explore page paints the gray colorBgLayout (via the antd Layout
// wrapper); match the dashboard context so thumbnails look like charts
// do where users see them.
await page
.addStyleTag({
content: `body, .ant-layout { background: ${
colorScheme === 'dark' ? '#141414' : '#ffffff'
} !important; }`,
})
.catch(() => {});
const textOnly = TEXT_ONLY_VIZ_TYPES.has(chart.vizType);
await page
.locator(textOnly ? TEXT_RENDERED_CHART_SELECTOR : RENDERED_CHART_SELECTOR)
.first()
.waitFor({ state: 'visible', timeout: 60_000 });
// Give animations/map tiles (or text-only chart data) time to settle
await page.waitForTimeout(textOnly ? 4_000 : 2_000);
// Some thumbnails read better mid-interaction (e.g. the calendar heatmap
// showing its tooltip); hover the configured element before the still.
const hoverSelector = HOVER_BEFORE_CAPTURE[chart.vizType];
if (hoverSelector) {
// Hover a mid-chart element rather than the first (often an empty
// corner cell), falling back to the first when there are few.
const cells = page.locator(hoverSelector);
const count = await cells.count().catch(() => 0);
await cells
.nth(Math.floor(count / 2))
.hover({ timeout: 5_000 })
.catch(() => {});
await page.waitForTimeout(500);
}
return page.screenshot();
}
/**
* Captures a chart light and (when a dark variant is wanted) dark. Dark
* rendering relies on the app following prefers-color-scheme (theme mode
* SYSTEM); if the dark render is byte-identical to the light one the app
* ignored the emulation, and the dark file is left untouched rather than
* overwritten with light-theme art.
*/
async function captureChart(
page: Page,
chart: ExampleChart,
output: string,
size: { width: number; height: number },
): Promise<void> {
const outputPath = path.join(FRONTEND_ROOT, output);
const darkPath = path.join(FRONTEND_ROOT, darkSibling(output));
const isNewImage = !fs.existsSync(outputPath);
const wantDark = fs.existsSync(darkPath) || isNewImage;
await page.setViewportSize(size);
await page.emulateMedia({ colorScheme: 'light' });
const lightShot = await renderAndShoot(page, chart, 'light');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, lightShot);
// eslint-disable-next-line no-console
console.log(`captured ${chart.sliceName} (${chart.vizType}) -> ${output}`);
if (wantDark) {
await page.emulateMedia({ colorScheme: 'dark' });
const darkShot = await renderAndShoot(page, chart, 'dark');
if (darkShot.equals(lightShot)) {
// eslint-disable-next-line no-console
console.log(
`SKIPPED dark variant for ${chart.sliceName}: the app ignored the dark color-scheme emulation (is dark theming enabled?)`,
);
} else {
fs.writeFileSync(darkPath, darkShot);
// eslint-disable-next-line no-console
console.log(
`captured ${chart.sliceName} (dark) -> ${darkSibling(output)}`,
);
}
}
if (isNewImage) {
// eslint-disable-next-line no-console
console.log(
`NOTE: ${output} is a new gallery image — register it in the plugin metadata (exampleGallery) to surface it.`,
);
}
}
test.describe('capture viz thumbnails', () => {
test.skip(
!process.env.CAPTURE_THUMBNAILS,
'Thumbnail capture only runs with CAPTURE_THUMBNAILS=1',
);
test('crawls example dashboards and refreshes gallery thumbnails', async ({
page,
}) => {
test.setTimeout(90 * 60_000);
const vizTypeFilter = process.env.VIZ_TYPES
? new Set(process.env.VIZ_TYPES.split(',').map(v => v.trim()))
: null;
const charts = await discoverDashboardCharts(page);
expect(
charts.length,
'no dashboard charts found — are examples loaded?',
).toBeGreaterThan(0);
// Every distinct chart per viz type gets captured: the preferred (or
// alphabetically-first) chart becomes the picker thumbnail, and each
// subsequent distinct chart fills the next declared gallery slot, so
// gallery images show real variety instead of the same chart resized.
const byVizType = new Map<string, ExampleChart[]>();
for (const chart of charts) {
const group = byVizType.get(chart.vizType) ?? [];
group.push(chart);
byVizType.set(chart.vizType, group);
}
const thumbnailSize = { width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE };
const exampleSize = { width: EXAMPLE_WIDTH, height: EXAMPLE_HEIGHT };
const unmapped: string[] = [];
const underfilledGalleries: string[] = [];
const failures: string[] = [];
for (const [vizType, group] of [...byVizType.entries()].sort()) {
if (vizTypeFilter && !vizTypeFilter.has(vizType)) continue;
const thumbOutput = VIZ_TYPE_THUMBNAILS[vizType];
const galleryOutputs = VIZ_TYPE_GALLERY[vizType] ?? [];
if (!thumbOutput && !galleryOutputs.length) {
unmapped.push(vizType);
continue;
}
// Preferred slice (if present) leads; the rest follow alphabetically.
group.sort((a, b) => a.sliceName.localeCompare(b.sliceName));
const preferredIndex = group.findIndex(
c => c.sliceName === PREFERRED_SLICES[vizType],
);
const ordered =
preferredIndex > 0
? [
group[preferredIndex],
...group.slice(0, preferredIndex),
...group.slice(preferredIndex + 1),
]
: group;
// Only advance past the thumbnail's chart when a thumbnail is
// actually captured (e.g. handlebars has no VIZ_TYPE_THUMBNAILS
// entry by design, so its gallery gets the full ordered list).
let nextIndex = 0;
if (thumbOutput) {
const chart = ordered[0];
nextIndex = 1;
try {
await captureChart(page, chart, thumbOutput, thumbnailSize);
} catch (error) {
failures.push(`${vizType} (${chart.sliceName}): ${error}`);
}
}
const missingSlots: string[] = [];
for (const output of galleryOutputs) {
const chart = ordered[nextIndex];
nextIndex += 1;
if (!chart) {
missingSlots.push(output);
continue;
}
try {
await captureChart(page, chart, output, exampleSize);
} catch (error) {
failures.push(`${vizType} gallery (${chart.sliceName}): ${error}`);
}
}
if (missingSlots.length) {
underfilledGalleries.push(
`${vizType}: not enough distinct dashboard charts for ${missingSlots.join(', ')}`,
);
}
}
if (unmapped.length) {
// eslint-disable-next-line no-console
console.log(
`viz types on dashboards with no thumbnail or gallery mapping (add to VIZ_TYPE_THUMBNAILS/VIZ_TYPE_GALLERY if wanted): ${unmapped.join(', ')}`,
);
}
if (underfilledGalleries.length) {
// eslint-disable-next-line no-console
console.log(
`gallery slots left untouched for lack of distinct example charts:\n${underfilledGalleries.join('\n')}`,
);
}
expect(failures, failures.join('\n')).toEqual([]);
});
});
@@ -101,7 +101,7 @@ export const EMBEDDED = {
/** Timeout for iframe to appear in the DOM */
IFRAME_LOAD: 15000, // 15s
/** Timeout for dashboard content to render inside the iframe */
DASHBOARD_RENDER: 30000, // 30s
DASHBOARD_RENDER: 60000, // 60s (embedded dashboards are slow to render on cold CI)
/** Timeout for individual chart cells to finish rendering */
CHART_RENDER: TIMEOUT.CHART_RENDER,
} as const;
@@ -35,8 +35,10 @@ import {
BuildQuery,
} from '@superset-ui/core';
import {
getTotalsMetrics,
isTimeComparison,
timeCompareOperator,
TotalsAggregate,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash-es';
import { TableChartFormData } from './types';
@@ -694,6 +696,19 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
formData.show_totals &&
queryMode === QueryMode.Aggregate,
);
const totalsAggregate: TotalsAggregate =
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
const totalsMetrics =
rawSummaryColumns.length > 0
? rawSummaryColumns.map(columnName => ({
expressionType: 'SIMPLE' as const,
aggregate: totalsAggregate,
column: { column_name: columnName },
label: columnName,
}))
: showAggregateTotals
? getTotalsMetrics(metrics ?? [], totalsAggregate)
: undefined;
if (showAggregateTotals || rawSummaryColumns.length > 0) {
// Create a copy of extras without the AG Grid WHERE clause
@@ -728,14 +743,7 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
extraQueries.push({
...queryObject,
columns: [],
...(rawSummaryColumns.length > 0 && {
metrics: rawSummaryColumns.map(columnName => ({
expressionType: 'SIMPLE' as const,
aggregate: 'SUM' as const,
column: { column_name: columnName },
label: columnName,
})),
}),
...(totalsMetrics ? { metrics: totalsMetrics } : {}),
extras: totalsExtras, // Use extras with AG Grid WHERE removed
row_limit: 0,
row_offset: 0,
@@ -490,11 +490,36 @@ const config: ControlPanelConfig = {
default: false,
renderTrigger: true,
description: t(
'Show a summary row of total aggregations: the selected metrics in aggregate mode, or the sum of numeric columns in raw records mode. Note that row limit does not apply to the result.',
'Show a summary row of total aggregations: the selected metrics in aggregate mode, or an aggregation of numeric columns in raw records mode. Note that row limit does not apply to the result.',
),
},
},
],
[
{
name: 'totals_aggregate',
config: {
type: 'SelectControl',
label: t('Summary aggregation'),
renderTrigger: true,
description: t(
'Aggregation used for the summary row, independent of each ' +
"metric's own aggregation. Only applies to simple metrics " +
'(a metric built from custom SQL keeps its own aggregation ' +
'in the summary row).',
),
default: 'SUM',
clearable: false,
choices: [
['SUM', t('Sum')],
['AVG', t('Average')],
],
visibility: ({ controls }) =>
Boolean(controls?.show_totals?.value),
resetOnHide: false,
},
},
],
[
{
name: 'show_numbered_column',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -19,14 +19,14 @@
import { t } from '@apache-superset/core/translation';
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example1 from './images/Table.jpg';
import example1Dark from './images/Table-dark.jpg';
import example2 from './images/Table2.jpg';
import example2Dark from './images/Table2-dark.jpg';
import example3 from './images/Table3.jpg';
import example3Dark from './images/Table3-dark.jpg';
import thumbnail from './images/custom_thumb_thumbnail.png';
import thumbnailDark from './images/custom_thumb_thumbnail-dark.png';
import example1 from './images/custom_thumb_Table.jpg';
import example1Dark from './images/custom_thumb_Table-dark.jpg';
import example2 from './images/custom_thumb_Table2.jpg';
import example2Dark from './images/custom_thumb_Table2-dark.jpg';
import example3 from './images/custom_thumb_Table3.jpg';
import example3Dark from './images/custom_thumb_Table3-dark.jpg';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { TableChartFormData, TableChartProps } from './types';
@@ -1560,6 +1560,74 @@ describe('plugin-chart-ag-grid-table', () => {
expect(queries[1].columns).toEqual([]);
expect(queries[1].metrics).toEqual(['count']);
});
test('defaults aggregate-mode totals to SUM for a simple metric', () => {
const simpleMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'sales' },
aggregate: 'SUM' as const,
label: 'sum_sales',
};
const { queries } = buildQuery(
{
viz_type: VizType.Table,
datasource: '11__table',
query_mode: QueryMode.Aggregate,
groupby: ['state'],
metrics: [simpleMetric],
show_totals: true,
},
{ ownState: {} },
);
expect(queries[1].metrics).toEqual([
{ ...simpleMetric, aggregate: 'SUM' },
]);
});
test('overrides aggregate-mode totals to AVG for a simple metric when totals_aggregate is set', () => {
const simpleMetric = {
expressionType: 'SIMPLE' as const,
column: { column_name: 'sales' },
aggregate: 'SUM' as const,
label: 'sum_sales',
};
const { queries } = buildQuery(
{
viz_type: VizType.Table,
datasource: '11__table',
query_mode: QueryMode.Aggregate,
groupby: ['state'],
metrics: [simpleMetric],
show_totals: true,
totals_aggregate: 'AVG',
},
{ ownState: {} },
);
// Main query keeps the metric's own aggregation.
expect(queries[0].metrics).toEqual([simpleMetric]);
// Summary query uses the chosen totals aggregate instead.
expect(queries[1].metrics).toEqual([
{ ...simpleMetric, aggregate: 'AVG' },
]);
});
test('applies totals_aggregate to raw-mode summary columns', () => {
const { queries } = buildQuery(
{ ...rawFormData, totals_aggregate: 'AVG' },
{ ownState: { rawSummaryColumns: ['num'] } },
);
expect(queries[1].metrics).toEqual([
{
expressionType: 'SIMPLE',
aggregate: 'AVG',
column: { column_name: 'num' },
label: 'num',
},
]);
});
});
describe('buildQuery - server pagination row limit', () => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

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