Compare commits

..

242 Commits

Author SHA1 Message Date
kasiazjc
aff407d730 fix(listview): fix compact filter pill tests and type error
- Select.test.tsx: migrate selectOption → selectPillOption for compact
  pill UI; update rehydration assertion to use data-test attr; wrap
  clearFilter() call in act() to silence React state warning
- CompactSelectPanel.test.tsx: add rerender after clearFilter() to
  reflect fully-controlled component no longer having internal state
- CompactSelectPanel.tsx: use opt.title for ReactNode labels in
  handleSelect so owner-select options serialize correctly
- CompactFilterTrigger.tsx: prettier formatting
- chartAction.ts: cast response to include warning field to fix TS error
  (ChartDataResponseResult in installed .d.ts predates warning field)
- scripts/oxlint.sh: fix silent exit-1 when oxlint produces no output
  (empty-string && short-circuit was the last command, so it became the
  script's exit code; use if/then/fi instead)
2026-05-26 06:04:25 +00:00
Alexandru Soare
ac778078de feat(mcp): make form_data_key optional in update_chart_preview (#39680) 2026-05-26 06:04:25 +00:00
Alexandru Soare
fc27892a7d chore(mcp): Simplify chart preview response (#40020) 2026-05-26 06:04:25 +00:00
xavier-GitHub76
9c2a98d29f fix(UserListModal): Success notification mentions user and not group (#40284) 2026-05-26 06:04:25 +00:00
dependabot[bot]
49e900ba75 chore(deps): bump markdown from 3.8.1 to 3.10.2 (#40389)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
b2b7d737b5 ci(translations): hard-block translation regressions in CI (#39443)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
4e34961d10 feat(i18n): add Finnish (fi) translations (AI-generated, needs review) (#40390)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Jean Massucatto
0f8bdfffb9 fix(explore): hide value input for unary filter operators (#39924) 2026-05-26 06:04:25 +00:00
Evan Rusackas
d0a72c572c feat(i18n): add Thai (th) translations (AI-generated, needs review) (#40391)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Torsten Stöter
863f8b0b5c docs: remove out-of-place phrase (#40226) 2026-05-26 06:04:25 +00:00
Abdul Rehman
e5e90ed131 fix(frontend): handle null/undefined path in ensureAppRoot (#39940)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-26 06:04:25 +00:00
dependabot[bot]
7523f08433 chore(deps): bump qs from 6.14.2 to 6.15.2 in /superset-websocket/utils/client-ws-app (#40382)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:25 +00:00
dependabot[bot]
43d14d13da chore(deps): bump qs from 6.14.2 to 6.15.2 in /docs (#40383)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
68f0aecc79 chore(ci): limit /app/prefix matrix variant to master merges (#40385)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
83fd7cea81 chore(cypress): remove dead _skip spec files and skipped inline tests (#40384)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
33ee1826f7 fix(ci): stop cancelling Hold Label Check runs (#40380)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
809692fb61 ci(docker): pin QEMU binfmt image to stabilize arm64 builds (#40235)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
9ebc9d42c6 ci(e2e): run backend under gunicorn instead of flask dev server (#40234)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:25 +00:00
Evan Rusackas
25a80cf3ed fix(bigquery): limit result set size to prevent browser memory crashes (#38588)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: ethan-l-geotab <ethanliong@geotab.com>
2026-05-26 06:04:24 +00:00
Evan Rusackas
f194fbe1da feat(i18n): AI-assisted translation backfill tooling + Spanish translations (#39448)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:24 +00:00
yousoph
02d365aba6 fix(dashboard): add top padding to "Create new chart" button in builder pane (#40033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:24 +00:00
Miha Rejec
4eb4d1ce44 fix(i18n): translate DateFilter tooltip for time range values (#40286)
Co-authored-by: Miha Rejec <mihar@comland.si>
2026-05-26 06:04:24 +00:00
dependabot[bot]
c59e928fd2 chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 in /superset-websocket (#39425)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
0f1f274037 chore(deps): bump sqlglot from 28.10.0 to 30.8.0 (#40186)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
d99f2caf0a chore(deps): bump geostyler from 18.5.1 to 18.6.0 in /superset-frontend (#40323)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
4aefa4a627 chore(deps-dev): bump ts-jest from 29.4.10 to 29.4.11 in /superset-websocket (#40363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
3b06d452c8 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40365)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
1148ad14f4 chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
c0564c77af chore(deps): bump react-arborist from 3.6.1 to 3.7.0 in /superset-frontend (#40371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
ee03b18bfa chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40372)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
a1bbdd06ca chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40366)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
Joe Li
4f7164681f fix(select): replace cached options with search results in AsyncSelect (#40039)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:24 +00:00
Đỗ Trọng Hải
c4d632a0b1 feat(sec): delays version-bumping PR to avoid prematurely usage of compromised packages (#39783)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-26 06:04:24 +00:00
Ali Gouta
bd1751ae3f fix(chart): fix label and description translation on UI and enhance french translations (#40229)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-26 06:04:24 +00:00
Evan Rusackas
59a236a566 chore(oxlint): enable import/newline-after-import + react/no-unstable-nested-components (#40319)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:24 +00:00
Beto Dealmeida
89b87591a0 feat(semantic layers): form for SL with a single SV (#40280) 2026-05-26 06:04:24 +00:00
Mehmet Salih Yavuz
466df0e657 fix(mcp): use name URL param so AI-generated SQL Lab titles render (#40288) 2026-05-26 06:04:24 +00:00
dependabot[bot]
8d8b76f3a0 chore(deps-dev): bump @swc/plugin-emotion from 14.9.0 to 14.10.0 in /superset-frontend (#40368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
f9131a7784 chore(deps-dev): bump ts-jest from 29.4.10 to 29.4.11 in /superset-frontend (#40369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
Đỗ Trọng Hải
3c0f4e8852 chore(backend/build): upgrade Gunicorn from v22 to v25 (#38788)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-26 06:04:24 +00:00
Đỗ Trọng Hải
39cd510e97 fix(docker): add missing service-worker.js into built container image (#39596)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
582ffc3de1 chore(deps-dev): bump webpack from 5.107.0 to 5.107.1 in /docs (#40364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
Shaitan
343a754108 docs: expand out-of-scope vulnerability definitions (#40332) 2026-05-26 06:04:24 +00:00
dependabot[bot]
198a25ed35 chore(deps-dev): bump webpack from 5.106.2 to 5.107.1 in /superset-frontend (#40370)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
Miha Rejec
2a73a3298a fix(i18n): add Slovenian translation for 'Range type' in DateFilter (#40287)
Co-authored-by: Miha Rejec <mihar@comland.si>
2026-05-26 06:04:24 +00:00
David Kopelent
c71de697cf feat(i18n): add missing Slovak translations (#40219) 2026-05-26 06:04:24 +00:00
Alexandru Soare
12a3cd97fd fix(preview): fix chart preview bugs (#40063) 2026-05-26 06:04:24 +00:00
Amin Ghadersohi
471cd89d5f feat(mcp): add series_limit to generate_chart XY config (#40307)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:24 +00:00
chaselynisabella
b467f9ab95 feat(path): support metric-based color scales & line width by metric (#39165) 2026-05-26 06:04:24 +00:00
Amin Ghadersohi
34f847fff7 fix(mcp): hide write tools from users without write permissions (#40098)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
5d3b8309ac chore(deps-dev): bump baseline-browser-mapping from 2.10.29 to 2.10.31 in /superset-frontend (#40320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
cff42bae8f chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
297fb5211e chore(deps): bump @googleapis/sheets from 13.0.1 to 13.0.2 in /superset-frontend (#40324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
a2e4e5b62c chore(deps-dev): update sqlalchemy-drill requirement from <2,>=1.1.4 to >=1.1.10,<2 (#40310)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:24 +00:00
dependabot[bot]
15f7a7c9d8 chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#40325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
9a55927575 fix(TableView): reset pagination when data reduces below current page (#34562)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
JUST.in DO IT
2abe47cdfa fix(view query): Update style for code viewer container (#39635)
Co-authored-by: Copilot <copilot@github.com>
2026-05-26 06:04:23 +00:00
Jay Masiwal
1d873ea96b fix(frontend): update safeStringify to surface [Circular] and DRY plugin code (#39156) 2026-05-26 06:04:23 +00:00
Evan Rusackas
527f127f93 fix(datasets): isolate filter state to fix concurrent /dataset race (#39685)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Elizabeth Thompson
13e32fb3ff fix(reports): guard null dashboard height in Playwright screenshots (#40179) 2026-05-26 06:04:23 +00:00
Mehmet Salih Yavuz
189a55549b feat(mcp): make config optional in generate_explore_link (#39559) 2026-05-26 06:04:23 +00:00
Mehmet Salih Yavuz
afef786419 feat(mcp): include applied dashboard filters in get_chart_info (#39620)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Mehmet Salih Yavuz
c2b1e1e539 fix(mcp): eager-load dataset.metrics to prevent Excel export DetachedInstanceError (#39483)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Beto Dealmeida
e9e2a93105 fix(semantic layers): coerce filter types (#40222) 2026-05-26 06:04:23 +00:00
Mehmet Salih Yavuz
43f6edf2d2 feat(mcp): add find_users tool and owner filter columns for listings (#39679)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Alexandru Soare
6941f69396 fix(recommandation): Fix chart recommandation (#39886) 2026-05-26 06:04:23 +00:00
Mehmet Salih Yavuz
2797b4e3ed feat(mcp): chart formatting options across all supported chart types (#39887) 2026-05-26 06:04:23 +00:00
dependabot[bot]
a3ea617aa4 chore(deps): bump markdown-to-jsx from 9.8.0 to 9.8.1 in /superset-frontend (#40316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:23 +00:00
dependabot[bot]
66090905e5 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#40313)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:23 +00:00
dependabot[bot]
18c2da79b4 chore(deps-dev): bump oxlint from 1.65.0 to 1.66.0 in /superset-frontend (#40318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
aefa459e89 fix(charts): handle PostgreSQL INTERVAL type in bar and pie charts (#34513)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
5523a416da fix(sqllab): keep saved-query list working when Jinja dataset(id) references a deleted dataset (#39703)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Maxime Beauchemin
af069f93ff fix(tests): fix flaky FileHandler test by awaiting LaunchQueue consumer in afterEach (#39508)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: sadpandajoe <jcli38@gmail.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
fa8cfa1f9b ci(deps): bump lower bound on pip dependabot PRs (#40308)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Maxime Beauchemin
3415a61087 fix(list-view): preserve user name in filter pill after navigation (#39505)
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
b0024d7a36 chore(sql-lab): finish SqlLab typed-dispatch migration for SaveDatasetModal (#40040)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
3b96e6f471 chore(superset-ui-switchboard): forward-compat fixes for TypeScript 6.0 (Phase E) (#40028)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
6842bb3186 chore(sql-lab): migrate useDispatch to useAppDispatch (#40037)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Beto Dealmeida
97dd0fb58a feat(semantic layers): add metadata on additive metrics (#40279) 2026-05-26 06:04:23 +00:00
Evan Rusackas
e8b6a9f674 fix(dashboard-import): remap chartsInScope on import (#26338) (#40140)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
519606e93a test(dashboard-import): pin native filter scope rootPath preservation (#19944) (#40135)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
712b29df55 test(datasets): regression test for Jinja not rendered on sync columns (#25839) (#40224)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
81991e5696 test(charts): regression for last-modified sort order (#27500) (#40231)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
51cb17c85b test(reports): regression for alerts CSV missing chart time filters (#25538) (#40232)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
1385c05ed4 test(helpers): regression for humanize locale activation (#28331) (#40233)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Evan Rusackas
982c0208b3 chore(codeowners): add @rusackas as translations maintainer (#40295)
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:23 +00:00
dependabot[bot]
d58252b7a7 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.3 to 8.59.4 in /superset-websocket (#40250)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:23 +00:00
dependabot[bot]
4ba113e9b4 chore(deps): bump content-disposition from 1.1.0 to 2.0.0 in /superset-frontend (#40109)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:23 +00:00
Shaitan
38f1bef50a fix(reports): enforce server-side recipient on chart/dashboard report subscriptions (#38847)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
Mike Bridge
4cc71f49e6 fix(dataset-editor): drop null warning_markdown from extra JSON serialisation (#39706)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
dependabot[bot]
04aa096a73 chore(deps-dev): bump react-resizable and @types/react-resizable in /superset-frontend (#40110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:23 +00:00
SkinnyPigeon
86580d3693 docs(reports): playwright setup clarification (#40168) 2026-05-26 06:04:22 +00:00
Evan Rusackas
5348a68510 ci(docs): soft-fail badge localization on transient fetch errors (#40236)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:22 +00:00
Evan Rusackas
b093a0357c test(sql-parser): pin WITH+UNION as non-mutating across dialects (#25659) (#40138)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
0b0b887b4a chore(deps-dev): bump typescript-eslint from 8.59.3 to 8.59.4 in /superset-websocket (#40251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
ac3d3f687b chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.3 to 8.59.4 in /superset-frontend (#40256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
57c44bf1d4 chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40262)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
45849c4116 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40265)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
15054d4298 chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40272)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
b89eca9141 chore(deps-dev): bump webpack from 5.106.2 to 5.107.0 in /docs (#40291)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
6f8d9e61a9 chore(deps-dev): bump ts-jest from 29.4.9 to 29.4.10 in /superset-websocket (#40290)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
jesperct
a5002f7709 fix(echarts): preserve dataZoom range across setOption(notMerge) (#40173) 2026-05-26 06:04:22 +00:00
Alexandru Soare
60cc8ccab4 fix(mcp): Skip misleading trend analysis for categorical ASCII charts (#39761) 2026-05-26 06:04:22 +00:00
Alexandru Soare
b774a5018d fix(mcp): raise right error (#39964) 2026-05-26 06:04:22 +00:00
Alexandru Soare
48bd635065 feat(mcp): Add mcp_call_id to tool responses for server log correlation (#39776) 2026-05-26 06:04:22 +00:00
Alexandru Soare
8261f40705 fix(mcp): Block destructive DDL (DROP, TRUNCATE, ALTER) in execute_sql (#39621) 2026-05-26 06:04:22 +00:00
Alexandru Soare
149501c879 fix(mcp): changed_on_humanized null in write tool responses (generate_dashboard, generate_chart) (#39488) 2026-05-26 06:04:22 +00:00
Jean Massucatto
bf7bd149ff fix(sqllab): handle scientific notation in big number JSON responses (#39994) 2026-05-26 06:04:22 +00:00
dependabot[bot]
bfaac143be chore(deps-dev): bump @types/node from 25.8.0 to 25.9.1 in /superset-websocket (#40249)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
5983d542e3 chore(deps-dev): bump @typescript-eslint/parser from 8.59.3 to 8.59.4 in /superset-websocket (#40252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
729499dc43 chore(deps-dev): bump typescript-eslint from 8.59.3 to 8.59.4 in /docs (#40254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
719572264f chore(deps): bump baseline-browser-mapping from 2.10.30 to 2.10.31 in /docs (#40255)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
7738dd8f9e chore(deps): bump codecov/codecov-action from 6.0.0 to 6.0.1 (#40269)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
d62f92a685 chore(deps): bump mapbox-gl from 3.23.1 to 3.24.0 in /superset-frontend (#40258)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
93b0e2ab2d chore(deps-dev): bump ts-jest from 29.4.9 to 29.4.10 in /superset-frontend (#40260)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
d49030169c chore(deps-dev): bump @typescript-eslint/parser from 8.59.3 to 8.59.4 in /superset-frontend (#40263)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
7f16e9eab7 chore(deps-dev): bump tsx from 4.22.0 to 4.22.3 in /superset-frontend (#40267)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
1892c16b97 chore(deps-dev): bump @types/node from 25.8.0 to 25.9.1 in /superset-frontend (#40268)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
b5f5def641 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40271)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
Evan Rusackas
e1a407d68e test(sql-parser): pin TimescaleDB hyperfunctions parse on postgresql (#32028) (#40142)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:22 +00:00
Evan Rusackas
26ab78695c docs: hide Component Playground top-level nav item (#40247)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:22 +00:00
madhushreeag
571b997c08 fix(roles): prevent 404 and silent user removal on large role edits (#40178)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
502cd76d69 chore(deps): bump swagger-ui-react from 5.32.5 to 5.32.6 in /docs (#40056)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:22 +00:00
Evan Rusackas
18694e8bcf chore(deps): coordinated bump jest 30.3→30.4 + jest-environment-jsdom 29→30 (#40206)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:22 +00:00
dependabot[bot]
0d7655b712 chore(deps): bump fs-extra from 11.3.2 to 11.3.5 in /superset-frontend (#39936)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-26 06:04:22 +00:00
dependabot[bot]
4a5c76b358 chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 0.10.0 to 0.10.1 in /superset-frontend (#39902)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-26 06:04:22 +00:00
Evan Rusackas
c8f6a606d2 test(sql-parser): pin quoted identifiers with spaces are not subqueries (#32541, #32684) (#40143)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
Beto Dealmeida
cb853fe5b1 fix: OAuth2 trigger (#40097) 2026-05-26 06:04:21 +00:00
Elizabeth Thompson
b56442ef74 fix(mcp): exclude self-referencing filter columns from get_schema output (#39826)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-05-26 06:04:21 +00:00
alex
987cd1e91d fix(deckgl): emit usable cross-filter values from polygon and geojson clicks (#39906)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
Evan Rusackas
2fd5492ee0 docs: cut 6.1.0 versions for user_docs, admin_docs, developer_docs, components (#40126)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
0bf1958186 chore(deps): update dompurify requirement from ^3.4.3 to ^3.4.5 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40213)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
88203bdb63 chore(deps): update reselect requirement from ^5.1.1 to ^5.2.0 in /superset-frontend/packages/superset-ui-core (#40214)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
00858d0af8 chore(deps): update dompurify requirement from ^3.4.2 to ^3.4.5 in /superset-frontend/packages/superset-ui-core (#40216)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
888cf905cf chore(deps): bump webpack-dev-server from 5.2.2 to 5.2.4 in /docs (#40227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
706c45fa92 chore(deps-dev): bump oxlint from 1.63.0 to 1.64.0 in /superset-frontend (#40160)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
jesperct
e33a9973d6 fix(echarts): suppress phantom x-axis label at axis edge when no time grain (#39972) 2026-05-26 06:04:21 +00:00
Jean Massucatto
092bcd0da8 fix(explore): prevent unnecessary scrollbars during chart rendering (#39291) 2026-05-26 06:04:21 +00:00
Richard Fogaca Nienkotter
00bd9d2ac1 fix(deckgl): render all MultiPolygon parts in Polygon chart (#40100) 2026-05-26 06:04:21 +00:00
Vitor Avila
c7c3d411c6 fix(OAuth2): Re-query the OAuth2 token to avoid stale reference (#40071) 2026-05-26 06:04:21 +00:00
Evan Rusackas
211f7bd87c chore(deps): coordinated bump ag-grid-community + ag-grid-react 35.2.1→35.3.0 (#40205)
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
cb89f9de0f chore(deps): bump baseline-browser-mapping from 2.10.29 to 2.10.30 in /docs (#40211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
SkinnyPigeon
89b3ae845c feat: Allow specific mcp tools to be disabled (#39835) 2026-05-26 06:04:21 +00:00
Evan Rusackas
7d0a3364af test(prophet): pin yhat_lower can be negative for negative series (#21734) (#40141)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
Evan Rusackas
87c848c2f1 test(security): regression test for session cookie after logout (#24713) (#40201)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
Evan Rusackas
2531a166bd test(api): regression test for Admin empty dashboard/chart list (#25890) (#40202)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
Mafi
c7d8bc55c1 fix(sqllab): execute prequeries on streaming connection to fix PostgreSQL CSV export (#40194)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
357ed59076 chore(deps-dev): bump ip-address from 10.1.0 to 10.2.0 in /superset-frontend (#40199)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
a4040c7778 chore(deps-dev): bump eslint from 10.3.0 to 10.4.0 in /superset-websocket (#40208)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
60061b9ee9 chore(deps): bump reselect from 5.1.1 to 5.2.0 in /docs (#40209)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
096681eb03 chore(deps): bump antd from 6.4.2 to 6.4.3 in /docs (#40210)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
d4ecb1ba6f chore(deps): bump caniuse-lite from 1.0.30001792 to 1.0.30001793 in /docs (#40212)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
b4a831f6fc chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#40215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
6ec5e05d9b chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#40217)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
dependabot[bot]
b5176e17fd chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 (#40218)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:21 +00:00
Alejandro Solares
9ca6ccbe3a fix(deps): patch fast-xml-parser CVE-2026-33036 and CVE-2026-33349 (#40118) 2026-05-26 06:04:21 +00:00
dependabot[bot]
bf215f722c chore(deps): update dompurify requirement from ^3.4.1 to ^3.4.2 in /superset-frontend/packages/superset-ui-core (#39808)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
d44ed6ed82 chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/plugins/plugin-chart-handlebars (#40015)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
60d2755b65 chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/plugins/plugin-chart-pivot-table (#40018)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
9d6f99adec chore(deps-dev): update jest requirement from ^30.3.0 to ^30.4.2 in /superset-frontend/packages/generator-superset (#40019)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
001157a777 chore(deps): update dompurify requirement from ^3.4.1 to ^3.4.3 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
1bc90a06ee chore(deps): bump serialize-javascript and terser-webpack-plugin in /superset-frontend/cypress-base (#40174)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
aa29c98ee9 chore(deps): bump minimatch from 3.1.3 to 3.1.5 in /superset-frontend/cypress-base (#40198)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
66a0c92c96 chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#40154)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-26 06:04:20 +00:00
Evan Rusackas
347f9fffad fix(date_parser): suppress noisy parsedatetime DEBUG logs (#33365) (#40144)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Evan Rusackas
3f71b283b0 fix(rls): align view permission name with REST API canonical name (#33744) (#40145)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
ec3525c0e8 chore(deps): bump minimatch from 3.1.2 to 3.1.5 in /superset-embedded-sdk (#40176)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
f1ce42c5b5 chore(deps): bump axios from 1.15.0 to 1.16.1 in /docs (#40177)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
d109a04e7c chore(deps-dev): bump @types/node from 25.7.0 to 25.8.0 in /superset-frontend (#40157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
f83be777c4 chore(deps-dev): update sqlalchemy-exasol requirement from <3.0,>=2.4.0 to >=2.4.0,<8.0 (#40182)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
3a26426431 chore(deps): bump fast-uri from 3.0.6 to 3.1.2 in /superset-frontend (#40175)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
bf935d5541 chore(deps-dev): update clickhouse-connect requirement from <1.0,>=0.13.0 to >=0.13.0,<2.0 (#40184)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
02aa17bd68 chore(deps-dev): bump hdbcli from 2.4.162 to 2.28.20 (#40185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
aa6f0c1ad3 chore(deps): bump flask-migrate from 3.1.0 to 4.1.0 (#40187)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
7d07ab790b chore(deps): bump greenlet from 3.1.1 to 3.5.0 (#40188)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
Evan Rusackas
b0cd86adb9 chore(docs): rename default docs plugin to user_docs for consistent versioned dir naming (#40171)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Elizabeth Thompson
5bba832131 fix(reports): narrow spinner checks to viewport and tighten exception handling (#39895)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Abdul Rehman
eccdbdd677 fix(i18n): correct Czech translation variables for SQL Lab query message (#40166)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
Beto Dealmeida
ee86d902d3 fix: OAuth2 exception should be 403 (#40074) 2026-05-26 06:04:20 +00:00
dependabot[bot]
83493ce39c chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
e579f90dc0 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40152)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
55e653cdcc chore(deps): bump @ant-design/icons from 6.2.2 to 6.2.3 in /superset-frontend (#40112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
87a52523a0 chore(deps): bump fast-xml-builder from 1.1.5 to 1.2.0 in /superset-frontend (#40103)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
cef55d4d4f chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#39821)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-26 06:04:20 +00:00
dependabot[bot]
c5a7f0e7ad chore(deps): bump d3-cloud from 1.2.8 to 1.2.9 in /superset-frontend (#39699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
4ac50b3072 chore(deps-dev): bump @types/node from 25.7.0 to 25.8.0 in /superset-websocket (#40148)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
98d31d8d9b chore(deps): bump immer from 11.1.7 to 11.1.8 in /superset-frontend (#40158)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
82e801ee52 chore(deps): bump react-arborist from 3.5.0 to 3.6.1 in /superset-frontend (#40159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
Richard Fogaca Nienkotter
239d452d5a fix(mcp): defer chart preview command imports (#40164) 2026-05-26 06:04:20 +00:00
dependabot[bot]
584925b68d chore(deps): bump antd from 6.3.7 to 6.4.2 in /docs (#40149)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
30e67f6798 chore(deps-dev): bump webpack-dev-server from 5.2.3 to 5.2.4 in /superset-frontend (#40161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
dependabot[bot]
4e16ac1ccb chore(deps-dev): bump tsx from 4.21.0 to 4.22.0 in /superset-frontend (#40162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 06:04:20 +00:00
Shaitan
cc749b1723 fix: escape SQL identifiers in db engine spec prequeries and metadata queries (#39840)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Michael S. Molina
0e170e1387 fix(extensions): add cache headers and strip Vary: Cookie for extension static assets (#40120)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Shaitan
2fd22e8e98 fix(dataset): validate datasource access during import (#39998)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:20 +00:00
Shaitan
329f8e2400 fix(query): restrict query cancellation to the query owner (#39996)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:19 +00:00
Shaitan
1256e7a867 fix(database): extend shillelagh URI pattern to cover all driver variants (#39995)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 06:04:19 +00:00
kasiazjc
897a1d7d2c fix(listview): address kgabryje review — regressions, ARIA, cleanup
🔴 Regressions fixed:
- CompactSelectPanel: bump async fetch cap 50→200 (pagination workaround)
- CompactSelectPanel: accept panelStyle prop forwarded from filter popupStyle
- CardSortSelect: fix hardcoded hasValue=false; show active sort in pill label;
  onClear resets to default sort option
- index.tsx: remove double updateFilterValue on datetime/numerical_range clear
- index.tsx: tooltip cold-load fallback — resolve label from static selects
  when cache is empty (URL round-trip strips label from internalFilters)
- index.tsx: format unix timestamps in date tooltip as "MMM D, YYYY HH:mm"
  instead of raw ms numbers

🟡 Correctness / UX:
- CompactFilterTrigger: move × out of <button> into sibling ClearButton with
  aria-label="Clear {filter} filter" — fixes nested-interactive ARIA violation,
  adds proper tab stop, meets WCAG 2.5.5 target size (min 24×24)
- CompactFilterTrigger: replace cloneElement injection with render prop
  children: (props: { isOpen, onClose }) => ReactNode — removes single-child
  constraint and eliminates FilterPopoverContent's isOpen hack prop

🟢 Cleanup:
- CompactSelectPanel: extract SEARCH_THRESHOLD constant; remove redundant
  selectedOption local state (use value prop directly); add ArrowUp/ArrowDown
  key navigation between listbox options
- index.tsx: extract clearFilterAtIndex helper; eliminate duplicated clear logic
  in clearFilterById and select onClear
- FilterPopoverContent: remove unused isOpen? prop
2026-05-26 06:04:19 +00:00
kasiazjc
a28632c68b fix(tests): use selectPillOption in DashboardList filter tests
Old combobox selects replaced by compact pill triggers — tests were
still using selectOption (which looks for role=combobox). Switch to
the selectPillOption helper that interacts with the new pill UI.
2026-05-26 06:04:19 +00:00
kasiazjc
c1f0b180bc fix(listview): prettier format CompactFilterTrigger onOpenChange 2026-05-26 06:04:19 +00:00
kasiazjc
fca825625d fix(tests): update tests for compact filter pills UI changes
Update test selectors and expectations across ListView, ChartList,
DashboardList, DatasetList, UsersList, GroupsList, RolesList, and
RowLevelSecurityList tests to match the new compact pill filter UI.

Key changes:
- select-filter-container wrapper added back for backward compat
- card-sort-select data-test added back to CardSortSelect wrapper
- Multiple search filter test updated: only first search renders
- Tests using role=combobox updated to use new selectors
- Tests for hidden search labels updated
- Added selectPillOption helper to testing-library for compact pill UI
2026-05-26 06:04:19 +00:00
kasiazjc
673b135adc fix(listview): reset tooltipOpen when dropdown closes to prevent sticky tooltip 2026-05-26 06:04:19 +00:00
kasiazjc
51cec9c24f fix(listview): show tooltip whenever tooltipTitle is set, not just when hasValue 2026-05-26 06:04:19 +00:00
kasiazjc
65012bcad8 feat(listview): sort pill matches filter pills, placed before Clear all
Rewrite CardSortSelect to use CompactFilterTrigger + CompactSelectPanel —
same pill visual, same dropdown list, same hover behaviour as filter pills.
No label, checkmark on current selection, no clear icon (sort always has
a value). Move it before the Clear all button in the controls row.
2026-05-26 06:04:19 +00:00
kasiazjc
8dc9ae5930 fix(listview): one search box per page, fixed 176px width, CardSortSelect pinned right
- Only the first search filter renders; subsequent search filters on pages
  like UsersList (which have 5 search inputs) are skipped to keep one
  search box per page.
- Search container uses fixed width (sizeUnit*44 = 176px) with flex-shrink:0
  instead of flex:1, so it never resizes when a scrollbar appears or
  pills wrap (fixes Sophie's resize-on-scroll bug).
- CardSortSelect in card/thumbnail view gets margin-left:auto so it pins
  to the right edge of the controls row instead of orphaning on a new line.
2026-05-26 06:04:19 +00:00
kasiazjc
d6ced441de fix(listview): apply prettier formatting to pass CI pre-commit checks 2026-05-26 06:04:19 +00:00
kasiazjc
0dfd3e4045 fix(listview): remove default React import, fix prettier formatting 2026-05-26 06:04:19 +00:00
kasiazjc
3118daa63a docs: remove committed screenshots — use GitHub image upload for PR previews 2026-05-26 06:04:19 +00:00
kasiazjc
5e3419fe28 Revert "docs: remove screenshots from repo — should not be committed to codebase"
This reverts commit 064bdb45c23836c27a992dafc3f2b00b0ecce469.
2026-05-26 06:04:19 +00:00
kasiazjc
77779d7bda docs: remove screenshots from repo — should not be committed to codebase 2026-05-26 06:04:18 +00:00
kasiazjc
1ec5abc60e fix(listview): tooltip stays visible — control open state explicitly
Tooltip was receiving mouseLeave suppressed by the Dropdown overlay,
causing it to stick after the mouse moved away. Use controlled open
prop: tooltip is only shown when hasValue && !dropdown open && hover
is active. onOpenChange guards against showing when dropdown is open.
mouseLeaveDelay=0 ensures instant hide on mouse leave.
2026-05-26 06:04:18 +00:00
kasiazjc
94aa03bd3d fix(listview): fix search input clipping and match antd option height
Remove overflow:hidden from PanelContainer so the Input focus ring
is not clipped at the panel boundary.

Set OptionItem line-height to theme.lineHeight and derive the vertical
padding from antd's exact formula (controlHeight - fontSize * lineHeight) / 2,
matching the height of antd Select dropdown options in Explore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:55:46 +00:00
kasiazjc
ade1a0e5e2 fix(listview): close dropdown on resize; add OptionList padding
CompactFilterTrigger: close dropdown on window resize — AntD Dropdown
does not reposition on resize so the panel detaches from the pill.

CompactSelectPanel: add paddingXXS top/bottom to OptionList so the
first/last items have breathing room inside overflow:hidden when there
is no search row (small static selects), preventing outline/background
from being clipped against the panel edge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:52:04 +00:00
kasiazjc
a704330400 docs: retake screenshots — tooltip, dropdown, active state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:50:58 +00:00
kasiazjc
6c49ad74f9 fix(listview): remove duplicate top padding on panel container
PanelContainer paddingXXS (4px) top + SearchRow sizeUnit*2 (8px) top
= 12px gap. Remove PanelContainer top padding so SearchRow controls
the top spacing exclusively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:45:10 +00:00
kasiazjc
345d87b4b0 docs: update compact filter pills screenshots
New screenshots show: always-visible Clear all button (disabled when inactive),
Explore-style dropdown options, search input contained within panel, and
edge-to-edge option rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:44:30 +00:00
kasiazjc
d013003cf1 fix(listview): match search top padding to left/right for consistency
SearchRow top padding was 0 while left/right was sizeUnit*2 (8px).
Make them equal so the search input has uniform breathing room.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:44:03 +00:00
kasiazjc
75d731398e Revert "refactor(listview): CompactSelectPanel reuses Select/AsyncSelect"
This reverts commit 6a16e7dca4.
2026-05-20 14:42:05 +00:00
kasiazjc
6a16e7dca4 refactor(listview): CompactSelectPanel reuses Select/AsyncSelect
Replace custom option rendering, debounced search, race-condition
guards, and loading states with Select/AsyncSelect from
@superset-ui/core/components. The trigger is hidden via a
zero-height wrapper; the dropdown renders inside a container div
via getPopupContainer, giving the same visual as Explore's select
dropdowns. Also exposes the 'open' prop in AntdExposedProps so
Select/AsyncSelect can be controlled externally.

Removes ~130 lines of reimplemented debounce/race-condition/
filter logic and eliminates the outline hover hack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:41:18 +00:00
kasiazjc
eb945f8289 fix(listview): search input contained within dropdown panel
Wrap Input in SearchRow with horizontal padding instead of margins on
.ant-input-affix-wrapper. width:100% + margin overflowed the container;
padding on a wrapper div keeps the input fully inside. Restore
overflow:hidden (needed for borderRadiusLG clipping) — it no longer
clips the input since the wrapper contains it correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:38:30 +00:00
kasiazjc
96234d2cfe fix(listview): match Explore Select dropdown height + fix clear-all after refresh
CompactSelectPanel: remove overflow:hidden from panel (was clipping search
input); remove min-height from OptionItem (5px padding alone gives the
correct 24px height matching AntD Select optionPadding, not 42px).

index.tsx: clearFilters/clearFilterById now call updateFilterValue directly
as a safety net so URL always updates even when filter refs are stale
(e.g. after page refresh where internalFilters is URL-hydrated).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:35:16 +00:00
kasiazjc
541cfd989c style(listview): match Select dropdown option style from Explore
Use exact visual pattern from @superset-ui/core Select:
- borderRadiusLG on container (was borderRadius)
- paddingXXS top/bottom on container (matches ant-select-dropdown)
- borderRadiusSM on each OptionItem (rounded highlight, not edge-to-edge)
- 5px vertical padding on items (matches optionPadding token default)
- outline: 2px solid colorPrimary on hover (Superset's own Select override)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:16:06 +00:00
kasiazjc
3c1f1d5535 feat(listview): tooltip on disabled Clear all button
Show 'No filters applied' tooltip on hover when the Clear all button
is disabled, so users understand why it is not interactive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:11:24 +00:00
kasiazjc
97c22497f4 fix(listview): search input grows to fill available space
Replace fixed 176px width on the search filter container with flex:1
so it expands into the space not taken by the pill filters.
min-width: 160px prevents it collapsing on narrow screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:10:11 +00:00
kasiazjc
f28a8f6f78 fix(listview): Clear all always visible, disabled when no active filters
Match Superset dashboard FilterBar pattern (isClearAllEnabled): always
render the button so users can discover it, just disable it when nothing
is selected. Prevents layout shift and builds muscle memory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:06:53 +00:00
kasiazjc
b868d3c7bf feat(listview): add Clear all button to compact filter controls
Show a 'Clear all' text button at the end of the filter pill row when
one or more filters are active. Disappears when all filters are inactive.
Matches dashboard native filter bar behaviour.

Also update ListView tests to use new compact-filter-pill buttons
instead of the old combobox queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:06:23 +00:00
kasiazjc
765d9d39a9 docs: add compact filter pills screenshots for PR
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:48:11 +00:00
kasiazjc
d520d461b3 fix(listview): read tooltip label from DOM textContent, not ReactNode
Owner options have a styled ReactNode label — typeof checks fail and we
fell back to String(value) = "1". Instead, capture the rendered text via
e.currentTarget.textContent at click/keydown time (always the visible
string regardless of label type) and use it as the normalized label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:46:36 +00:00
kasiazjc
926b9b2311 feat(listview): compact filter pills for all CRUD list views
Replace full-width inline filter blocks with compact pill triggers across
all ListView CRUD views. Each non-search filter becomes a pill that opens
a focused dropdown on click. Async selects use a debounced options panel
matching the DrillBy submenu pattern. Date/range filters get an Apply
button. Active pills show colored border, dot indicator, hover tooltip
with selected value, and inline clear button.

- CompactFilterTrigger: pill + Ant Design Dropdown, injects isOpen/onClose
  into panel children via cloneElement; Tooltip on pill for active state;
  destroyPopupOnHide intentionally omitted to preserve filter refs
- CompactSelectPanel: options list with debounced async fetch, race-condition
  guard, keyboard nav (Enter/Space), proper ARIA (listbox/option/aria-selected,
  aria-label on listbox)
- FilterPopoverContent: Apply button wrapper for DateRange/NumericalRange;
  inner label visually hidden but screen-reader accessible
- index: expose clearFilterById in ref type to match ListView contract
- Tests: CompactSelectPanel.test.tsx and FilterPopoverContent.test.tsx added;
  datetime_range and numerical_range pill tests added to index.test.tsx;
  tooltip test in CompactFilterTrigger.test.tsx made non-brittle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:35:23 +00:00
kasiazjc
934443e09f fix(listview): cache tooltip label in UIFilters state to survive URL round-trip
URL serialization via use-query-params strips the label from the selected
SelectOption, leaving only {value: 1}. Reading the label back from
internalFilters after hydration therefore gives undefined.

Cache the string label in tooltipLabels state at the moment of selection
(before URL round-trip). Clear on onClear, clearFilters, and clearFilterById.
tooltipTitle now reads from the cache instead of internalFilters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:55:37 +00:00
kasiazjc
7d5b0e35e2 fix(listview): move Tooltip inside Dropdown so it fires on pill hover
Wrapping the Dropdown in an outer span+Tooltip caused Dropdown to absorb
mouse events before they reached the Tooltip. Move Tooltip to wrap
FilterPill directly inside the Dropdown — Ant Design supports Tooltip
as a Dropdown child and the pill receives hover events cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:48:30 +00:00
kasiazjc
6224bc7aec fix(listview): normalize select option + match AntD dropdown styles
Normalize selected option to plain {label: string, value} before passing
to onSelect — matches original SelectFilter behaviour and prevents
'Converting circular structure to JSON' when emotion-styled ReactNode
labels are serialized to URL query params.

Style OptionItem to match AntD Select dropdown exactly: colorBgElevated
container, fontSize (not fontSizeSM), colorText always (not colorPrimary
on selected), colorFillTertiary hover, borderRadiusSM on items, 32px
height with sizeUnit padding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 16:42:05 +00:00
kasiazjc
1b5a31a203 fix(listview): handle async fetchSelects rejection and null data
Add .catch() to the fetch effect so rejected promises (network errors,
auth failures) don't surface as unhandled rejections. Guard result.data
with ?? [] so null/undefined responses fall back to empty list. Filter
null entries from displayOptions before rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:54:58 +00:00
kasiazjc
7812f64278 fix(listview): remove ViewModeContainer margin-top that offset grid/list icons
The 21px margin-top on ViewModeContainer existed to visually align with
filter label text above each input. Labels are now hidden (pills have no
label), so the offset pushed the icons below the pill midline. Removing
it lets align-items: center on .header do the right thing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:46:43 +00:00
kasiazjc
12621e3e97 fix(listview): centre filter row with view-mode toggle icons
Add align-items: center to .header so the grid/list toggle and the
filter pills sit on the same midline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:42:51 +00:00
kasiazjc
81adabf667 refactor(listview): remove dead destructured props from compact filter index
optionFilterProps, paginate, and popupStyle were destructured in the filter
map but are no longer used after SelectFilter was replaced with
CompactSelectPanel. Remove them to eliminate dead code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:45:35 +00:00
kasiazjc
bff26e9256 refactor(listview): compact filter review fixes — type safety, a11y, race conditions
- index.tsx: remove redundant filterValue variable (merged into initialValue),
  remove no-op onClose={() => {}} from CompactSelectPanel (it's injected via
  cloneElement, the explicit pass was misleading), fix `any` type in
  clearFilters forEach, pass popupType="dialog" for datetime_range and
  numerical_range triggers
- CompactFilterTrigger: replace as Record<string,unknown> cloneElement cast
  with typed FilterPanelInjectedProps interface; add popupType prop (default
  'listbox', 'dialog' for form panels) wired to aria-haspopup so screen readers
  get the correct popup role per ARIA spec
- CompactSelectPanel: add cancelled flag to fetchSelects effect to prevent
  stale-closure state updates when deps change mid-flight; add
  debouncedSetSearch.cancel() cleanup on unmount; add role="listbox" to
  OptionList and role="option" + aria-selected + tabIndex + onKeyDown(Enter/
  Space) to OptionItem for keyboard navigation
- FilterPopoverContent: replace label { display: none } with visually-hidden
  CSS (position:absolute + clip) so FormLabel remains accessible to screen
  readers (datepicker/number inputs retain their accessible name context)
  while still hidden visually
- Tests: add 6 new tests covering isOpen/onClose injection via cloneElement,
  aria-haspopup default and dialog values, dropdown close aria-expanded reset,
  select filter pill rendering, and active state when value is present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:44:23 +00:00
kasiazjc
8fa074d3a1 fix(listview): pin search filter container height to controlHeight
Force search-filter-container to controlHeight so it aligns flush with
the pill buttons (same height, align-items: center on the controls row
then centers everything correctly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:37:34 +00:00
kasiazjc
f984153936 fix(listview): compact filters UX audit — focus, date apply, alignment
Fix autoFocus stealing focus from search: inject isOpen via cloneElement,
use InputRef.input.focus pattern from DrillBySubmenu (100ms delay, preventScroll).
Debounce remote fetch triggers with Constants.FAST_DEBOUNCE. Reset panel
search on close.

Add FilterPopoverContent wrapper with Apply button for date/numerical range
filters so cloneElement-injected onClose closes the dropdown on submit.
Hides inner FormLabel via CSS since the pill already shows the label.

Match DrillBySubmenu styling: iconSize="l"/colorIcon for search prefix,
colorTextDisabled for empty state, min-height: 35px for option rows,
container padding replaces SearchRow border, .ant-input-affix-wrapper
margin-bottom for visual separation, min-width: 220px, box-shadow: none
on the input.

Pass external loading prop to CompactSelectPanel. Change .controls to
align-items: center and hide search-filter-container label via CSS.
Add aria-label to pill when label is a string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:09:02 +00:00
kasiazjc
c09295aa52 fix(listview): clear filter — keep dropdown mounted, direct state reset
Remove destroyPopupOnHide so CompactSelectPanel stays mounted when the
dropdown closes, keeping its ref live. Add updateFilterValue(index, undefined)
directly in all onClear handlers as a safety net so the ListView filter
state always resets even if the ref is stale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:52:29 +00:00
kasiazjc
002cb30a44 refine(listview): compact filters — match border radius/height, dropdown panel, hover tooltips
Use theme.borderRadius and theme.controlHeight on filter pills to match
the Search input and Select controls on the same row. Replace the
Popover+SelectFilter approach with a new CompactSelectPanel that renders a
clean options list (search + checkmark) opened via Dropdown — no
intermediate widget. Add tooltipTitle prop to CompactFilterTrigger so
hovering a filled pill shows the selected value. Align .controls with
flex-end so pills sit flush with the bottom of the search input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:50:42 +00:00
kasiazjc
6540353960 feat(listview): compact filter pills with popover for CRUD views
Replace full-width inline filter blocks with compact pill triggers that
open a Popover on click. Search filter stays inline. Adds active state
indicator (dot + border + weight) and inline clear button. Single new
component, no new deps. Controls gap tightened to 10px for pill rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:29:59 +00:00
247 changed files with 2089 additions and 19800 deletions

View File

@@ -36,7 +36,7 @@ runs:
echo "PYTHON_VERSION=${{ inputs.python-version }}" >> $GITHUB_ENV
fi
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: ${{ inputs.cache }}

View File

@@ -26,7 +26,7 @@ runs:
- name: Set up QEMU
if: ${{ inputs.build == 'true' }}
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
with:
# Pin the binfmt image to a specific QEMU release. The default
# (`tonistiigi/binfmt:latest`) is a moving target, and drift across
@@ -39,12 +39,12 @@ runs:
- name: Set up Docker Buildx
if: ${{ inputs.build == 'true' }}
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Try to login to DockerHub
if: ${{ inputs.login-to-dockerhub == 'true' }}
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ inputs.dockerhub-user }}
password: ${{ inputs.dockerhub-token }}

View File

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

View File

@@ -10,7 +10,7 @@ updates:
schedule:
interval: "daily"
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
ignore:
@@ -59,7 +59,7 @@ updates:
open-pull-requests-limit: 30
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "pip"
@@ -76,7 +76,7 @@ updates:
- pip
- dependabot
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: ".github/actions"
@@ -85,7 +85,7 @@ updates:
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/docs/"
@@ -110,7 +110,7 @@ updates:
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-websocket/"
@@ -121,7 +121,7 @@ updates:
- dependabot
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-websocket/utils/client-ws-app/"
@@ -133,7 +133,7 @@ updates:
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
# Now for all of our plugins and packages!
@@ -147,7 +147,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
@@ -159,7 +159,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
@@ -171,7 +171,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
@@ -186,7 +186,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
@@ -198,7 +198,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
@@ -210,7 +210,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
@@ -222,7 +222,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
@@ -234,7 +234,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-table/"
@@ -249,7 +249,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
@@ -261,7 +261,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
@@ -273,7 +273,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
@@ -285,7 +285,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
@@ -297,7 +297,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
@@ -309,7 +309,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
@@ -321,7 +321,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
@@ -333,7 +333,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
@@ -345,7 +345,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
@@ -357,7 +357,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
@@ -373,7 +373,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/generator-superset/"
@@ -385,7 +385,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
@@ -397,7 +397,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-core/"
@@ -414,7 +414,7 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-switchboard/"
@@ -426,4 +426,4 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
cooldown:
default-days: 7
default-days: 5

43
.github/workflows/cancel_duplicates.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,11 +95,7 @@ jobs:
# in the context of push (using multi-platform build), we need to pull the image locally
- name: Docker pull
if: github.event_name == 'push' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker)
run: |
for i in 1 2 3; do
docker pull $IMAGE_TAG && break
[ $i -lt 3 ] && sleep 30
done
run: docker pull $IMAGE_TAG
- name: Print docker stats
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker

View File

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

View File

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

View File

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

350
.github/workflows/ephemeral-env.yml vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -20,9 +20,7 @@ jobs:
- name: Check for latest tag
id: latest-tag
run: |
source ./scripts/tag_latest_release.sh $(echo ${GITHUB_EVENT_RELEASE_TAG_NAME}) --dry-run
env:
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
source ./scripts/tag_latest_release.sh $(echo ${{ github.event.release.tag_name }}) --dry-run
- name: Configure Git
run: |

View File

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

View File

@@ -102,7 +102,7 @@ jobs:
- name: Install Superset Showtime
if: steps.auth.outputs.authorized == 'true'
run: |
echo "::notice::Maintainer ${GITHUB_ACTOR} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
echo "::notice::Maintainer ${{ github.actor }} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
pip install --upgrade superset-showtime
showtime version

View File

@@ -27,9 +27,6 @@ concurrency:
group: docs-deploy-asf-site
cancel-in-progress: true
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
@@ -48,13 +45,7 @@ jobs:
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
build-deploy:
needs: config
# For workflow_run triggers, only deploy when the triggering run originated
# from this repository (not a fork), ensuring the checked-out code and any
# local actions executed with deploy credentials are trusted.
if: >-
needs.config.outputs.has-secrets &&
(github.event_name != 'workflow_run' ||
github.event.workflow_run.head_repository.full_name == github.repository)
if: needs.config.outputs.has-secrets
name: Build & Deploy
runs-on: ubuntu-24.04
steps:

View File

@@ -16,9 +16,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_sha || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
linkinator:
# See docs here: https://github.com/marketplace/actions/linkinator
@@ -28,8 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
@@ -102,8 +97,7 @@ jobs:
# Only runs if integration tests succeeded
if: >
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository
github.event.workflow_run.conclusion == 'success'
name: Build (after integration tests)
runs-on: ubuntu-24.04
defaults:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,7 +143,7 @@ jobs:
if: >-
github.event_name == 'pull_request' &&
steps.regression.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: translation-regression
path: |

View File

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

View File

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

View File

@@ -12,16 +12,11 @@ jobs:
steps:
- name: Welcome Message
uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0
uses: actions/first-interaction@v3
continue-on-error: true
with:
repo_token: ${{ github.token }}
issue_message: |-
Congrats on opening your first issue and thank you for contributing to Superset! :tada: :heart:
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
pr_message: |-
repo-token: ${{ github.token }}
pr-message: |-
Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart:
Please read our [New Contributor Welcome & Expectations](https://github.com/apache/superset/wiki/New-Contributor-Welcome-&-Expectations) guide.
We hope to see you in our [Slack](https://apache-superset.slack.com/) community too! Not signed up? Use our [Slack App](http://bit.ly/join-superset-slack) to self-register.

View File

@@ -158,14 +158,3 @@ repos:
language: system
files: ^superset/config\.py$
pass_filenames: false
- id: zizmor
name: zizmor (GHA security audit)
entry: zizmor
language: python
additional_dependencies: [zizmor==1.25.2]
files: ^\.github/
types: [yaml]
pass_filenames: false
# Advisory until pre-existing findings are resolved; remove
# --no-exit-codes to make this hook blocking.
args: [--no-exit-codes, .github/]

View File

@@ -113,7 +113,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
# Some bash scripts needed throughout the layers
COPY --chmod=755 docker/*.sh /app/docker/
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN pip install --no-cache-dir --upgrade uv
# Using uv as it's faster/simpler than pip
RUN uv venv /app/.venv

View File

@@ -70,9 +70,9 @@
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.40",
"@swc/core": "^1.15.33",
"antd": "^6.4.3",
"baseline-browser-mapping": "^2.10.32",
"baseline-browser-mapping": "^2.10.31",
"caniuse-lite": "^1.0.30001793",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
@@ -128,11 +128,7 @@
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"swagger-client": "3.37.3",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"yaml": "1.10.3",
"uuid": "11.1.1"
"swagger-client": "3.37.3"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -4033,86 +4033,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz#b05d715b04c4fd47baf59288233da85a683cc0bc"
integrity sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==
"@swc/core-darwin-arm64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz#d84134fb80417d41128739f0b9014542e3ed9dd3"
integrity sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==
"@swc/core-darwin-x64@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz#3180daef5c1e47b435f8edd084509e0a5c0d883b"
integrity sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==
"@swc/core-darwin-x64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz#0badb9834071f1c6005986571d4a96359c1d7cd0"
integrity sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==
"@swc/core-linux-arm-gnueabihf@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz#18fcd3c70e48fdfae07c9f18751b1409ce1e5e84"
integrity sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==
"@swc/core-linux-arm-gnueabihf@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz#b7577a825b59d98b6a9a5c991d842046efe1c34a"
integrity sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==
"@swc/core-linux-arm64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz#26304933922f2a8e3194770e404403fc25a19c89"
integrity sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==
"@swc/core-linux-arm64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz#304c48321494a18c67b2913c273b08674ee70d8c"
integrity sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==
"@swc/core-linux-arm64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz#3402dfba04ba7b8ea81f243e2f8fa2c336b54d03"
integrity sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==
"@swc/core-linux-arm64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz#d116cbc04ccb4f4ee810da6bca79d4423605dbcd"
integrity sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==
"@swc/core-linux-ppc64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz#b3df9065cad352328c1eeef08a28fc9fe98785aa"
integrity sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==
"@swc/core-linux-ppc64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz#f5354dba36db9414305bab344c817d57b8b457c2"
integrity sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==
"@swc/core-linux-s390x-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz#58e5b601f641dde81b30626ef66a668701ec918f"
integrity sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==
"@swc/core-linux-s390x-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz#016df9f4c9d7fd65b85ca9c558c5aec341f06da0"
integrity sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==
"@swc/core-linux-x64-gnu@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz#cf057dce0c148c53f2d30152baaf60ea29e5d59c"
integrity sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==
"@swc/core-linux-x64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz#49f36558ede072e71999aa37f123367daed2a662"
integrity sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==
"@swc/core-linux-x64-musl@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz#21fb1a4d0193e9bbcd1469ecd36166d2e96e4006"
integrity sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==
"@swc/core-linux-x64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz#b096665f5cfeee2612325f301da5c1590b10d8f3"
integrity sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==
"@swc/core-win32-arm64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz#1dba23b2b0db86b3d6d65da2abd627cc607a1fbc"
integrity sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==
"@swc/core-win32-arm64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz#f3101263a0dbaa173ec47638c9719d0b89838bd2"
integrity sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==
"@swc/core-win32-ia32-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz#b2da1e33165d469467b1046a2189db468da488eb"
integrity sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==
"@swc/core-win32-ia32-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz#eb981ef5613d42c9220559bdb0c8bc58cf6c3eb9"
integrity sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==
"@swc/core-win32-x64-msvc@1.15.40":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz#3563f7e8ce8708f5fda43eb8e0956ef11e0da320"
integrity sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==
"@swc/core-win32-x64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz#a2fed9956933027ceb368857bac4bb4ee203d47c"
integrity sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==
"@swc/core@^1.15.40", "@swc/core@^1.7.39":
version "1.15.40"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.40.tgz#941c949aa88c0d8d291f102f519f3c2c77701b90"
integrity sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==
"@swc/core@^1.15.33", "@swc/core@^1.7.39":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.33.tgz#2a6571c8aca961925f14beae52b3f43c18370fc6"
integrity sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.26"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.40"
"@swc/core-darwin-x64" "1.15.40"
"@swc/core-linux-arm-gnueabihf" "1.15.40"
"@swc/core-linux-arm64-gnu" "1.15.40"
"@swc/core-linux-arm64-musl" "1.15.40"
"@swc/core-linux-ppc64-gnu" "1.15.40"
"@swc/core-linux-s390x-gnu" "1.15.40"
"@swc/core-linux-x64-gnu" "1.15.40"
"@swc/core-linux-x64-musl" "1.15.40"
"@swc/core-win32-arm64-msvc" "1.15.40"
"@swc/core-win32-ia32-msvc" "1.15.40"
"@swc/core-win32-x64-msvc" "1.15.40"
"@swc/core-darwin-arm64" "1.15.33"
"@swc/core-darwin-x64" "1.15.33"
"@swc/core-linux-arm-gnueabihf" "1.15.33"
"@swc/core-linux-arm64-gnu" "1.15.33"
"@swc/core-linux-arm64-musl" "1.15.33"
"@swc/core-linux-ppc64-gnu" "1.15.33"
"@swc/core-linux-s390x-gnu" "1.15.33"
"@swc/core-linux-x64-gnu" "1.15.33"
"@swc/core-linux-x64-musl" "1.15.33"
"@swc/core-win32-arm64-msvc" "1.15.33"
"@swc/core-win32-ia32-msvc" "1.15.33"
"@swc/core-win32-x64-msvc" "1.15.33"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -5568,10 +5568,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.32, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.32"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz#b6b553a4285fdd606327a617de36a5351e3aaa64"
integrity sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==
baseline-browser-mapping@^2.10.31, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.31"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz#9c6825f052601ce6974a90dd49683b1726887b0b"
integrity sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==
batch@0.6.1:
version "0.6.1"
@@ -9676,10 +9676,10 @@ locate-path@^7.1.0:
dependencies:
p-locate "^6.0.0"
lodash-es@4.18.1, lodash-es@^4.17.21:
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.debounce@^4, lodash.debounce@^4.0.8:
version "4.0.8"
@@ -9701,7 +9701,12 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@4.17.21, lodash@4.18.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
lodash@4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.18.1:
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
@@ -14721,10 +14726,15 @@ utils-merge@1.0.1:
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@11.1.1, uuid@8.3.2, "uuid@^11.1.0 || ^12 || ^13 || ^14.0.0", uuid@^8.3.2:
version "11.1.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad"
integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==
uuid@8.3.2, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
"uuid@^11.1.0 || ^12 || ^13 || ^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d"
integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==
uvu@^0.5.0:
version "0.5.6"
@@ -15129,9 +15139,9 @@ ws@^7.3.1:
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.18.0, ws@^8.2.3:
version "8.20.1"
resolved "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz"
integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==
version "8.18.3"
resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz"
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
wsl-utils@^0.1.0:
version "0.1.0"
@@ -15199,7 +15209,12 @@ yaml-ast-parser@0.0.43:
resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz"
integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
yaml@1.10.2, yaml@1.10.3, yaml@^1.10.0:
yaml@1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^1.10.0:
version "1.10.3"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3"
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==

View File

@@ -39,7 +39,7 @@ dependencies = [
"apache-superset-core",
"backoff>=1.8.0",
"celery>=5.3.6, <6.0.0",
"click>=8.4.0",
"click>=8.0.3",
"click-option-group",
"colorama",
"flask-cors>=6.0.0, <7.0",
@@ -103,7 +103,7 @@ dependencies = [
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
"sqlglot>=30.8.0, <31",
# newer pandas needs 0.9+
"tabulate>=0.10.0, <1.0",
"tabulate>=0.9.0, <1.0",
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
@@ -139,7 +139,7 @@ denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb>=1.5.2,<2", "duckdb-engine>=0.17.0"]
duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]

View File

@@ -60,7 +60,7 @@ cffi==2.0.0
# pynacl
charset-normalizer==3.4.2
# via requests
click==8.4.1
click==8.2.1
# via
# apache-superset (pyproject.toml)
# celery
@@ -208,7 +208,7 @@ kombu==5.5.3
# via celery
limits==5.1.0
# via flask-limiter
mako==1.3.12
mako==1.3.11
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -421,7 +421,7 @@ sqlglot==30.8.0
# apache-superset-core
sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.10.0
tabulate==0.9.0
# via apache-superset (pyproject.toml)
trio==0.30.0
# via
@@ -451,7 +451,7 @@ tzdata==2025.2
# pandas
url-normalize==2.2.1
# via requests-cache
urllib3==2.7.0
urllib3==2.6.3
# via
# -r requirements/base.in
# requests

View File

@@ -130,7 +130,7 @@ charset-normalizer==3.4.2
# via
# -c requirements/base-constraint.txt
# requests
click==8.4.1
click==8.2.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -219,7 +219,7 @@ docstring-parser==0.17.0
# via cyclopts
docutils==0.22.2
# via rich-rst
duckdb==1.5.3
duckdb==1.4.2
# via
# apache-superset
# duckdb-engine
@@ -346,7 +346,6 @@ google-auth==2.43.0
# google-api-core
# google-auth-oauthlib
# google-cloud-bigquery
# google-cloud-bigquery-storage
# google-cloud-core
# pandas-gbq
# pydata-google-auth
@@ -361,7 +360,7 @@ google-cloud-bigquery==3.27.0
# apache-superset
# pandas-gbq
# sqlalchemy-bigquery
google-cloud-bigquery-storage==2.26.0
google-cloud-bigquery-storage==2.19.1
# via pandas-gbq
google-cloud-core==2.4.1
# via google-cloud-bigquery
@@ -507,7 +506,7 @@ limits==5.1.0
# via
# -c requirements/base-constraint.txt
# flask-limiter
mako==1.3.12
mako==1.3.11
# via
# -c requirements/base-constraint.txt
# alembic
@@ -702,7 +701,7 @@ proto-plus==1.25.0
# via
# google-api-core
# google-cloud-bigquery-storage
protobuf==5.29.6
protobuf==4.25.8
# via
# google-api-core
# google-cloud-bigquery-storage
@@ -840,7 +839,7 @@ python-dotenv==1.2.2
# pydantic-settings
python-ldap==3.4.4
# via apache-superset
python-multipart==0.0.29
python-multipart==0.0.20
# via mcp
pytz==2025.2
# via
@@ -1007,7 +1006,7 @@ statsd==4.0.1
# via apache-superset
syntaqlite==0.1.0
# via apache-superset
tabulate==0.10.0
tabulate==0.9.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1072,7 +1071,7 @@ url-normalize==2.2.1
# via
# -c requirements/base-constraint.txt
# requests-cache
urllib3==2.7.0
urllib3==2.6.3
# via
# -c requirements/base-constraint.txt
# botocore

View File

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

View File

@@ -1,24 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
module.exports = {
// @superset-ui/switchboard ships ES module syntax, so it must be
// transformed by babel rather than ignored as a node_modules dependency.
transformIgnorePatterns: ["/node_modules/(?!@superset-ui/switchboard)"],
};

View File

@@ -1,91 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
validateEmbeddedDashboardId,
validateSupersetDomain,
findUnsafeSandboxExtras,
} from "./index";
describe("validateEmbeddedDashboardId", () => {
it("accepts a canonical uuid", () => {
expect(() =>
validateEmbeddedDashboardId("f4787a4f-2541-4f8a-9b5e-1e2d3c4b5a6f")
).not.toThrow();
});
it("accepts a simple hexadecimal id", () => {
expect(() => validateEmbeddedDashboardId("abc123")).not.toThrow();
});
it.each([
["../../evil"],
["a/b"],
["x?foo=bar"],
["x#frag"],
["a@b.com"],
["foo bar"],
["http://evil.com"],
[""],
["%2e%2e"],
])("rejects an id with an unexpected format: %p", (id) => {
expect(() => validateEmbeddedDashboardId(id)).toThrow();
});
});
describe("validateSupersetDomain", () => {
it.each([
["https://superset.example.com"],
["http://localhost:8088"],
// sub-path deployments are valid; the origin is what matters downstream
["https://example.com/superset"],
])("accepts a valid absolute URL: %p", (domain) => {
expect(() => validateSupersetDomain(domain)).not.toThrow();
});
it.each([
["superset.example.com"], // missing protocol
["not a url"],
[""],
["/relative/path"],
])("rejects a malformed domain: %p", (domain) => {
expect(() => validateSupersetDomain(domain)).toThrow(
"Invalid supersetDomain format"
);
});
});
describe("findUnsafeSandboxExtras", () => {
it("returns the tokens that relax iframe isolation", () => {
expect(
findUnsafeSandboxExtras([
"allow-forms",
"allow-top-navigation",
"allow-popups",
"allow-top-navigation-by-user-activation",
])
).toEqual(["allow-top-navigation", "allow-top-navigation-by-user-activation"]);
});
it("returns an empty array when all tokens are safe", () => {
expect(
findUnsafeSandboxExtras(["allow-forms", "allow-popups", "allow-downloads"])
).toEqual([]);
});
});

View File

@@ -50,9 +50,7 @@ export type UiConfigType = {
};
export type EmbedDashboardParams = {
/** The id provided by the embed configuration UI in Superset.
* This is the UUID generated by Superset's embed configuration and is
* expected to contain only hexadecimal characters and hyphens. */
/** The id provided by the embed configuration UI in Superset */
id: string;
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
supersetDomain: string;
@@ -114,48 +112,6 @@ export type EmbeddedDashboard = {
setThemeMode: (mode: ThemeMode) => void;
};
/**
* Validates that an embedded dashboard id has the expected format
* (the UUID produced by Superset's embed configuration). Throws on
* anything containing characters that are not part of that format.
*/
export function validateEmbeddedDashboardId(id: string): void {
if (typeof id !== 'string' || !/^[a-f0-9-]+$/i.test(id)) {
throw new Error('Invalid dashboard id format');
}
}
/**
* Validates that supersetDomain is a parseable absolute URL (it must include
* a protocol, e.g. https://superset.example.com). Throws otherwise. The
* domain's origin is what gets used as the postMessage targetOrigin, so it
* has to resolve to a well-formed origin.
*/
export function validateSupersetDomain(supersetDomain: string): void {
try {
// eslint-disable-next-line no-new
new URL(supersetDomain);
} catch {
throw new Error('Invalid supersetDomain format');
}
}
// Sandbox tokens that materially relax the iframe's isolation (for example,
// letting the embedded frame navigate the top-level page). They remain
// supported via iframeSandboxExtras for callers that genuinely need them.
const UNSAFE_SANDBOX_EXTRAS = [
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
];
/**
* Returns any caller-provided sandbox tokens that relax the iframe's
* isolation, so they can be surfaced and not enabled unintentionally.
*/
export function findUnsafeSandboxExtras(extras: string[]): string[] {
return extras.filter(token => UNSAFE_SANDBOX_EXTRAS.includes(token));
}
/**
* Embeds a Superset dashboard into the page using an iframe.
*/
@@ -172,8 +128,6 @@ export async function embedDashboard({
referrerPolicy,
resolvePermalinkUrl,
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
validateEmbeddedDashboardId(id);
function log(...info: unknown[]) {
if (debug) {
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
@@ -185,7 +139,6 @@ export async function embedDashboard({
if (supersetDomain.endsWith('/')) {
supersetDomain = supersetDomain.slice(0, -1);
}
validateSupersetDomain(supersetDomain);
function calculateConfig() {
let configNumber = 0;
@@ -242,13 +195,6 @@ export async function embedDashboard({
iframe.sandbox.add('allow-forms'); // for forms to submit
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
// additional sandbox props
const unsafeSandboxExtras = findUnsafeSandboxExtras(iframeSandboxExtras);
if (unsafeSandboxExtras.length > 0) {
console.warn(
'[superset-embedded-sdk] iframeSandboxExtras includes tokens that ' +
`relax the iframe's isolation: ${unsafeSandboxExtras.join(', ')}`,
);
}
iframeSandboxExtras.forEach((key: string) => {
iframe.sandbox.add(key);
});
@@ -270,9 +216,7 @@ export async function embedDashboard({
// we know the content window isn't null because we are in the load event handler.
iframe.contentWindow!.postMessage(
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: 'port transfer' },
// Use the normalized origin (not the raw domain, which may carry a
// sub-path for sub-path deployments) as the postMessage targetOrigin.
new URL(supersetDomain).origin,
supersetDomain,
[theirPort],
);
log('sent message channel to the iframe');

View File

@@ -8020,9 +8020,9 @@
"peer": true
},
"node_modules/tmp": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"engines": {
"node": ">=14.14"
}
@@ -14601,9 +14601,9 @@
"peer": true
},
"tmp": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ=="
},
"to-regex-range": {
"version": "5.0.1",

View File

@@ -194,7 +194,7 @@
"@storybook/test": "^8.6.18",
"@storybook/test-runner": "^0.17.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
"@swc/core": "^1.15.33",
"@swc/plugin-emotion": "^14.10.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
@@ -229,7 +229,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.32",
"baseline-browser-mapping": "^2.10.31",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
@@ -249,7 +249,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.2",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.1",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
@@ -296,7 +296,7 @@
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.5.0",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"engines": {
@@ -7768,9 +7768,9 @@
}
},
"node_modules/@npmcli/arborist/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8034,9 +8034,9 @@
}
},
"node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8194,9 +8194,9 @@
}
},
"node_modules/@npmcli/package-json/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8470,9 +8470,9 @@
}
},
"node_modules/@nx/devkit/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12310,9 +12310,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz",
"integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -12328,18 +12328,18 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.40",
"@swc/core-darwin-x64": "1.15.40",
"@swc/core-linux-arm-gnueabihf": "1.15.40",
"@swc/core-linux-arm64-gnu": "1.15.40",
"@swc/core-linux-arm64-musl": "1.15.40",
"@swc/core-linux-ppc64-gnu": "1.15.40",
"@swc/core-linux-s390x-gnu": "1.15.40",
"@swc/core-linux-x64-gnu": "1.15.40",
"@swc/core-linux-x64-musl": "1.15.40",
"@swc/core-win32-arm64-msvc": "1.15.40",
"@swc/core-win32-ia32-msvc": "1.15.40",
"@swc/core-win32-x64-msvc": "1.15.40"
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-darwin-x64": "1.15.33",
"@swc/core-linux-arm-gnueabihf": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-arm64-musl": "1.15.33",
"@swc/core-linux-ppc64-gnu": "1.15.33",
"@swc/core-linux-s390x-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/core-linux-x64-musl": "1.15.33",
"@swc/core-win32-arm64-msvc": "1.15.33",
"@swc/core-win32-ia32-msvc": "1.15.33",
"@swc/core-win32-x64-msvc": "1.15.33"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -12351,9 +12351,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz",
"integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
"cpu": [
"arm64"
],
@@ -12367,9 +12367,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz",
"integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz",
"integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==",
"cpu": [
"x64"
],
@@ -12383,9 +12383,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz",
"integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz",
"integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==",
"cpu": [
"arm"
],
@@ -12399,9 +12399,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz",
"integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
"cpu": [
"arm64"
],
@@ -12415,9 +12415,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz",
"integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz",
"integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==",
"cpu": [
"arm64"
],
@@ -12431,9 +12431,9 @@
}
},
"node_modules/@swc/core-linux-ppc64-gnu": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz",
"integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz",
"integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==",
"cpu": [
"ppc64"
],
@@ -12447,9 +12447,9 @@
}
},
"node_modules/@swc/core-linux-s390x-gnu": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz",
"integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz",
"integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==",
"cpu": [
"s390x"
],
@@ -12463,9 +12463,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz",
"integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
"cpu": [
"x64"
],
@@ -12479,9 +12479,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz",
"integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz",
"integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==",
"cpu": [
"x64"
],
@@ -12495,9 +12495,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz",
"integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz",
"integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==",
"cpu": [
"arm64"
],
@@ -12511,9 +12511,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz",
"integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz",
"integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==",
"cpu": [
"ia32"
],
@@ -12527,9 +12527,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.40",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz",
"integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz",
"integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==",
"cpu": [
"x64"
],
@@ -12775,9 +12775,9 @@
}
},
"node_modules/@tufjs/models/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17209,9 +17209,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.32",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
"integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
"version": "2.10.31",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
"integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -17402,9 +17402,9 @@
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17416,7 +17416,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@@ -17793,9 +17793,9 @@
}
},
"node_modules/cacache/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22661,9 +22661,9 @@
"license": "MIT"
},
"node_modules/eslint-plugin-react-you-might-not-need-an-effect": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.10.2.tgz",
"integrity": "sha512-cqm9DXcsISYZHnFXT5zPH+ITsMx/bYscmq6zIsbtYvei1vj4dZ+BxN9LgoMmjEdm7sTaWxKVRY5IqQRQvau/GQ==",
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.10.1.tgz",
"integrity": "sha512-IK0s/+ShN0bkur5moKCu/lfx2D/9uIeozje8Wv2/XnYdmswa17pDg02aUuytEPb8Gf0eueiQFf/QsvOHHcvujg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22848,9 +22848,9 @@
}
},
"node_modules/eslint-plugin-testing-library/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -23258,15 +23258,15 @@
"license": "Apache-2.0"
},
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
@@ -23285,7 +23285,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
@@ -26595,9 +26595,9 @@
}
},
"node_modules/ignore-walk/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -26856,9 +26856,9 @@
"license": "MIT"
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -35946,9 +35946,9 @@
}
},
"node_modules/multimatch/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -36615,9 +36615,9 @@
}
},
"node_modules/nx/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -38409,9 +38409,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"dev": true,
"funding": [
{
@@ -38429,7 +38429,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -39002,9 +39002,9 @@
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@@ -39423,9 +39423,9 @@
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -44969,9 +44969,9 @@
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
@@ -47104,9 +47104,9 @@
"license": "MIT"
},
"node_modules/vm2": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.11.5.tgz",
"integrity": "sha512-RSrkBiwrj6FRU+QdqNs6KG0XdlvJCjpQ4GXiqmMbrhmwfu5k/XIMpAer0L8f6iuf0uJ3a4T1xJN126Q8yf0VIA==",
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.11.3.tgz",
"integrity": "sha512-DO1TTKuOc+veL11VNOvJwRab80mghFKE40Av3bl6pdXs11bdiDMuR73owy+dS2EsTZEvRUeBkkBuDVRjV/RgEw==",
"license": "MIT",
"dependencies": {
"acorn": "^8.15.0",
@@ -47889,9 +47889,9 @@
}
},
"node_modules/webpack-sources": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
"integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz",
"integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -48303,9 +48303,9 @@
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -48924,9 +48924,9 @@
}
},
"packages/generator-superset/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -49973,7 +49973,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"peerDependencies": {
"@apache-superset/core": "*",

View File

@@ -277,7 +277,7 @@
"@storybook/test": "^8.6.18",
"@storybook/test-runner": "^0.17.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
"@swc/core": "^1.15.33",
"@swc/plugin-emotion": "^14.10.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
@@ -312,7 +312,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.32",
"baseline-browser-mapping": "^2.10.31",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
@@ -332,7 +332,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.2",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.1",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
@@ -379,7 +379,7 @@
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.5.0",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {

View File

@@ -30,7 +30,7 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.1.2",
"yosay": "^3.0.0"
},
"devDependencies": {

View File

@@ -29,7 +29,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"peerDependencies": {
"@apache-superset/core": "*",

View File

@@ -239,6 +239,8 @@ export default function transformProps(
formatter,
show: showLabels,
color: theme.colorText,
textBorderColor: theme.colorBgBase,
textBorderWidth: 1,
};
const legendData = keys.sort((a: string, b: string) => {
if (!legendSort) return 0;

View File

@@ -21,8 +21,8 @@ import { TreePathInfo } from '../types';
export const COLOR_SATURATION = [0.7, 0.4];
export const LABEL_FONTSIZE = 11;
export const BORDER_WIDTH = 0;
export const GAP_WIDTH = 0;
export const BORDER_WIDTH = 2;
export const GAP_WIDTH = 2;
export const extractTreePathInfo = (
treePathInfo: TreePathInfo[] | undefined,

View File

@@ -214,8 +214,7 @@ export default function transformProps(
colorAlpha: OpacityEnum.SemiTransparent,
color: theme.colorText,
borderColor: theme.colorBgBase,
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
borderWidth: 2,
},
label: {
...labelProps,

View File

@@ -72,15 +72,6 @@ describe('Funnel transformProps', () => {
}),
);
});
test('does not apply a text border to segment labels', () => {
// A white textBorder washes out the dark text on light-colored segments.
const result = transformProps(chartProps as EchartsFunnelChartProps);
const { label } = (result.echartOptions.series as any)[0];
expect(label.color).toBe(supersetTheme.colorText);
expect(label.textBorderColor).toBeUndefined();
expect(label.textBorderWidth).toBeUndefined();
});
});
describe('formatFunnelLabel', () => {

View File

@@ -18,7 +18,6 @@
*/
import { ChartProps } from '@superset-ui/core';
import { supersetTheme } from '@apache-superset/core/theme';
import { OpacityEnum } from '../../src/constants';
import { EchartsTreemapChartProps } from '../../src/Treemap/types';
import transformProps from '../../src/Treemap/transformProps';
@@ -75,44 +74,4 @@ describe('Treemap transformProps', () => {
}),
);
});
test('should not render gaps between treemap nodes when filtered', () => {
const filteredChartProps = new ChartProps({
...chartProps,
filterState: { selectedValues: ['Sylvester,bar1'] },
});
expect(
transformProps(filteredChartProps as EchartsTreemapChartProps),
).toEqual(
expect.objectContaining({
echartOptions: expect.objectContaining({
series: [
expect.objectContaining({
data: expect.arrayContaining([
expect.objectContaining({
children: expect.arrayContaining([
expect.objectContaining({
name: 'Arnold',
children: expect.arrayContaining([
expect.objectContaining({
name: 'bar2',
itemStyle: expect.objectContaining({
borderWidth: 0,
gapWidth: 0,
colorAlpha: OpacityEnum.SemiTransparent,
}),
label: expect.objectContaining({}),
}),
]),
}),
]),
}),
]),
}),
],
}),
}),
);
});
});

View File

@@ -29,7 +29,7 @@
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.24.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"
},
"peerDependencies": {

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ClientErrorObject, SupersetError } from '@superset-ui/core';
import { FC } from 'react';
import { useChartOwnerNames } from 'src/hooks/apiResources';
@@ -33,7 +32,7 @@ export type Props = {
stackTrace?: string;
} & Omit<ClientErrorObject, 'error'>;
const DEFAULT_CHART_ERROR = t('Data error');
const DEFAULT_CHART_ERROR = 'Data error';
export const ChartErrorMessage: FC<Props> = ({ chartId, error, ...props }) => {
// fetches the chart owners and adds them to the extra data of the error message

View File

@@ -1,102 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { CardSortSelect } from './CardSortSelect';
const options = [
{ desc: false, id: 'title', label: 'Alphabetical', value: 'alphabetical' },
{
desc: true,
id: 'changed_on',
label: 'Recently modified',
value: 'recently_modified',
},
{
desc: false,
id: 'changed_on',
label: 'Least recently modified',
value: 'least_recently_modified',
},
];
test('pill always shows "Sort" label with no value suffix and no clear button', () => {
render(
<CardSortSelect
options={options}
onChange={jest.fn()}
initialSort={[{ id: 'title', desc: false }]}
/>,
);
expect(screen.getByText('Sort')).toBeInTheDocument();
expect(screen.queryByText(/sort.*alphabetical/i)).not.toBeInTheDocument();
expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
expect(screen.getByTestId('compact-filter-pill')).toHaveAttribute(
'aria-expanded',
'false',
);
});
test('no clear button even when a non-default sort is active', () => {
render(
<CardSortSelect
options={options}
onChange={jest.fn()}
initialSort={[{ id: 'changed_on', desc: true }]}
/>,
);
expect(screen.getByText('Sort')).toBeInTheDocument();
expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
});
test('clicking a sort option from the panel calls onChange with the correct id and desc', async () => {
const onChange = jest.fn();
render(
<CardSortSelect
options={options}
onChange={onChange}
initialSort={[{ id: 'title', desc: false }]}
/>,
);
await userEvent.click(screen.getByTestId('compact-filter-pill'));
expect(screen.getByText('Recently modified')).toBeInTheDocument();
await userEvent.click(screen.getByText('Recently modified'));
expect(onChange).toHaveBeenCalledWith([{ id: 'changed_on', desc: true }]);
// Pill label stays "Sort" — value is in tooltip, not the label
expect(screen.getByText('Sort')).toBeInTheDocument();
});
test('selecting a different option from the panel calls onChange with correct args', async () => {
const onChange = jest.fn();
render(
<CardSortSelect
options={options}
onChange={onChange}
initialSort={[{ id: 'title', desc: false }]}
/>,
);
await userEvent.click(screen.getByTestId('compact-filter-pill'));
await userEvent.click(screen.getByText('Least recently modified'));
expect(onChange).toHaveBeenCalledWith([{ id: 'changed_on', desc: false }]);
});

View File

@@ -52,6 +52,8 @@ export const CardSortSelect = ({
const selectOptions = options.map(o => ({ label: o.label, value: o.value }));
const isNonDefault = currentValue.value !== options[0]?.value;
const handleSelect = (option: SelectOption | undefined) => {
if (!option) return;
const original = options.find(o => o.value === option.value);
@@ -61,13 +63,27 @@ export const CardSortSelect = ({
}
};
const handleClear = () => {
const first = options[0];
if (first) {
setCurrentValue({ label: first.label, value: first.value });
onChange([{ id: first.id, desc: first.desc }]);
}
};
// Show the active sort value in the label so users can see the current sort
// without hovering — matches the previous inline-select UX.
const pillLabel = isNonDefault
? `${t('Sort')}: ${String(currentValue.label)}`
: t('Sort');
return (
<span data-test="card-sort-select">
<CompactFilterTrigger
label={t('Sort')}
hasValue={false}
onClear={() => {}}
tooltipTitle={String(currentValue.label)}
label={pillLabel}
hasValue={isNonDefault}
onClear={handleClear}
tooltipTitle={isNonDefault ? String(currentValue.label) : undefined}
>
{({ isOpen, onClose }) => (
<CompactSelectPanel

View File

@@ -65,22 +65,26 @@ test('renders as inactive pill with down chevron when hasValue is false', () =>
expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
});
test('renders active state with clear icon when hasValue is true', () => {
test('renders active state with clear button when hasValue is true', () => {
renderTrigger({ hasValue: true });
expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /clear owner filter/i }),
).toBeInTheDocument();
});
test('clear icon has descriptive aria-label matching the filter name', () => {
test('clear button has descriptive aria-label matching the filter name', () => {
renderTrigger({ hasValue: true });
const clearIcon = screen.getByTestId('compact-filter-clear');
expect(clearIcon).toHaveAttribute('aria-label', 'Clear Owner filter');
const clearBtn = screen.getByTestId('compact-filter-clear');
expect(clearBtn).toHaveAttribute('aria-label', 'Clear Owner filter');
});
test('clear icon is rendered inside the pill button', () => {
test('clear button is a separate element from the pill button', () => {
renderTrigger({ hasValue: true });
const pill = screen.getByTestId('compact-filter-pill');
const clearIcon = screen.getByTestId('compact-filter-clear');
expect(pill).toContainElement(clearIcon);
const clearBtn = screen.getByTestId('compact-filter-clear');
// Buttons must not be nested
expect(pill).not.toContainElement(clearBtn);
expect(clearBtn).not.toContainElement(pill);
});
test('toggles aria-expanded when pill is clicked', async () => {
@@ -91,11 +95,11 @@ test('toggles aria-expanded when pill is clicked', async () => {
expect(pill).toHaveAttribute('aria-expanded', 'true');
});
test('calls onClear when clear icon is clicked', async () => {
test('calls onClear when clear button is clicked', async () => {
const onClear = jest.fn();
renderTrigger({ hasValue: true, onClear } as any);
const clearIcon = screen.getByTestId('compact-filter-clear');
await userEvent.click(clearIcon);
const clearBtn = screen.getByRole('button', { name: /clear owner filter/i });
await userEvent.click(clearBtn);
expect(onClear).toHaveBeenCalledTimes(1);
});
@@ -106,7 +110,9 @@ test('does not render tooltip wrapper when tooltipTitle is absent', () => {
test('shows active state indicators when hasValue and tooltipTitle are set', () => {
renderTrigger({ hasValue: true, tooltipTitle: 'Some Owner' });
expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /clear owner filter/i }),
).toBeInTheDocument();
expect(screen.getByTestId('compact-filter-pill')).toHaveAttribute(
'aria-expanded',
'false',

View File

@@ -16,14 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
useEffect,
useRef,
useState,
type ReactNode,
type MouseEvent,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { useEffect, useState, type ReactNode, type MouseEvent } from 'react';
import { useTheme, styled, css } from '@apache-superset/core/theme';
import { Dropdown, Tooltip, Icons } from '@superset-ui/core/components';
@@ -45,6 +38,11 @@ interface CompactFilterTriggerProps {
popupType?: 'listbox' | 'dialog';
}
const TriggerWrapper = styled.span`
display: inline-flex;
align-items: center;
`;
const FilterPill = styled.button<{ $active: boolean }>`
${({ theme, $active }) => css`
display: inline-flex;
@@ -60,22 +58,11 @@ const FilterPill = styled.button<{ $active: boolean }>`
font-weight: ${$active ? 600 : 400};
cursor: pointer;
white-space: nowrap;
line-height: 1;
transition:
border-color 0.2s,
background 0.2s,
color 0.2s;
/* AntD anticon spans carry vertical-align: -0.125em from global styles.
align-self centers the span within the pill; the inner flex+align-items
centers the svg within the span. */
.anticon {
display: flex;
align-items: center;
align-self: center;
line-height: 0;
}
&:hover {
border-color: ${theme.colorPrimary};
background: ${$active ? theme.colorPrimaryBgHover : theme.colorFillAlter};
@@ -98,6 +85,33 @@ const ActiveDot = styled.span`
`}
`;
// Meets WCAG 2.5.5 target size (24×24 minimum) with explicit dimensions.
const ClearButton = styled.button`
${({ theme }) => css`
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
min-height: 24px;
margin-left: ${theme.sizeUnit / 2}px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
color: ${theme.colorPrimary};
border-radius: ${theme.borderRadiusSM}px;
&:hover {
background: ${theme.colorPrimaryBg};
}
&:focus-visible {
outline: 2px solid ${theme.colorPrimary};
outline-offset: 2px;
}
`}
`;
export default function CompactFilterTrigger({
label,
hasValue,
@@ -109,12 +123,6 @@ export default function CompactFilterTrigger({
const [open, setOpen] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false);
const theme = useTheme();
// Tracks whether tooltip should be suppressed after dropdown close.
// Brave (and some other browsers) fire a synthetic mouseover on newly-exposed
// elements when a popup disappears, triggering Tooltip onOpenChange(true)
// without real user intent. We suppress until the cursor actually leaves the
// pill (onMouseLeave), which is the first reliable "hover reset" signal.
const tooltipSuppressedRef = useRef(false);
// Close dropdown on window resize — AntD Dropdown doesn't reposition
// itself on resize so the panel ends up detached from the pill.
@@ -129,70 +137,63 @@ export default function CompactFilterTrigger({
e.stopPropagation();
onClear();
setOpen(false);
tooltipSuppressedRef.current = true;
setTooltipOpen(false);
};
const clearAriaLabel =
typeof label === 'string' ? `Clear ${label} filter` : 'Clear filter';
return (
<Dropdown
open={open}
onOpenChange={visible => {
setOpen(visible);
if (!visible) {
tooltipSuppressedRef.current = true;
setTooltipOpen(false);
}
}}
trigger={['click']}
popupRender={() =>
children({ isOpen: open, onClose: () => setOpen(false) })
}
placement="bottomLeft"
destroyPopupOnHide
>
<Tooltip
title={tooltipTitle}
open={!!tooltipTitle && !open && tooltipOpen}
<TriggerWrapper>
<Dropdown
open={open}
onOpenChange={visible => {
if (visible && tooltipSuppressedRef.current) return;
setTooltipOpen(visible && !!tooltipTitle && !open);
setOpen(visible);
if (!visible) setTooltipOpen(false);
}}
mouseEnterDelay={0.5}
mouseLeaveDelay={0}
trigger={['click']}
popupRender={() =>
children({ isOpen: open, onClose: () => setOpen(false) })
}
placement="bottomLeft"
>
<FilterPill
$active={hasValue}
type="button"
data-test="compact-filter-pill"
aria-haspopup={popupType}
aria-expanded={open}
aria-label={typeof label === 'string' ? label : undefined}
onMouseLeave={() => {
tooltipSuppressedRef.current = false;
}}
<Tooltip
title={tooltipTitle}
open={!!tooltipTitle && !open && tooltipOpen}
onOpenChange={visible =>
setTooltipOpen(visible && !!tooltipTitle && !open)
}
mouseEnterDelay={0.5}
mouseLeaveDelay={0}
>
{hasValue && <ActiveDot />}
<span>{label}</span>
{hasValue ? (
<Icons.CloseOutlined
iconSize="s"
iconColor={theme.colorPrimary}
onClick={handleClear}
data-test="compact-filter-clear"
aria-label={
typeof label === 'string'
? t('Clear %s filter', label)
: undefined
<FilterPill
$active={hasValue}
type="button"
data-test="compact-filter-pill"
aria-haspopup={popupType}
aria-expanded={open}
aria-label={typeof label === 'string' ? label : undefined}
>
{hasValue && <ActiveDot />}
<span>{label}</span>
<Icons.DownOutlined
iconSize="xs"
iconColor={
hasValue ? theme.colorPrimary : theme.colorTextSecondary
}
/>
) : (
<Icons.DownOutlined
iconSize="s"
iconColor={theme.colorTextSecondary}
/>
)}
</FilterPill>
</Tooltip>
</Dropdown>
</FilterPill>
</Tooltip>
</Dropdown>
{hasValue && (
<ClearButton
type="button"
data-test="compact-filter-clear"
aria-label={clearAriaLabel}
onClick={handleClear}
>
<Icons.CloseOutlined iconSize="s" iconColor={theme.colorPrimary} />
</ClearButton>
)}
</TriggerWrapper>
);
}

View File

@@ -59,7 +59,7 @@ interface CompactSelectPanelProps {
loading?: boolean;
}
const PanelContainer = styled.div`
const PanelContainer = styled.div<{ $panelStyle?: CSSProperties }>`
${({ theme }) => css`
min-width: 220px;
max-width: 320px;

View File

@@ -0,0 +1,112 @@
/**
* 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 {
useState,
useMemo,
forwardRef,
useImperativeHandle,
RefObject,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { Dayjs } from 'dayjs';
import { useLocale } from 'src/hooks/useLocale';
import { extendedDayjs } from '@superset-ui/core/utils/dates';
import {
AntdThemeProvider,
Loading,
FormLabel,
RangePicker,
} from '@superset-ui/core/components';
import type { BaseFilter, FilterHandler } from './types';
import { FilterContainer } from './Base';
import { RANGE_WIDTH } from '../utils';
interface DateRangeFilterProps extends BaseFilter {
onSubmit: (val: number[] | string[]) => void;
name: string;
dateFilterValueType?: 'unix' | 'iso';
}
type ValueState = [number, number] | [string, string] | null;
function DateRangeFilter(
{
Header,
initialValue,
onSubmit,
dateFilterValueType = 'unix',
}: DateRangeFilterProps,
ref: RefObject<FilterHandler>,
) {
const [value, setValue] = useState<ValueState | null>(initialValue ?? null);
const dayjsValue = useMemo((): [Dayjs, Dayjs] | null => {
if (!value || (Array.isArray(value) && !value.length)) return null;
return [extendedDayjs(value[0]), extendedDayjs(value[1])];
}, [value]);
const locale = useLocale();
useImperativeHandle(ref, () => ({
clearFilter: () => {
setValue(null);
onSubmit([]);
},
}));
if (locale === null) {
return <Loading position="inline-centered" />;
}
return (
<AntdThemeProvider locale={locale}>
<FilterContainer
data-test="date-range-filter-container"
vertical
justify="center"
align="start"
width={RANGE_WIDTH}
>
<FormLabel>{Header}</FormLabel>
<RangePicker
placeholder={[t('Start date'), t('End date')]}
showTime
value={dayjsValue}
onCalendarChange={(dayjsRange: [Dayjs, Dayjs]) => {
if (!dayjsRange?.[0]?.valueOf() || !dayjsRange?.[1]?.valueOf()) {
setValue(null);
onSubmit([]);
return;
}
const changeValue =
dateFilterValueType === 'iso'
? [dayjsRange[0].toISOString(), dayjsRange[1].toISOString()]
: [
dayjsRange[0]?.valueOf() ?? 0,
dayjsRange[1]?.valueOf() ?? 0,
];
setValue(changeValue as ValueState);
onSubmit(changeValue);
}}
/>
</FilterContainer>
</AntdThemeProvider>
);
}
export default forwardRef(DateRangeFilter);

View File

@@ -32,9 +32,6 @@ const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.sizeUnit * 2}px;
background: ${theme.colorBgElevated};
border-radius: ${theme.borderRadiusLG}px;
box-shadow: ${theme.boxShadowSecondary};
/* Visually hide the redundant label — the pill already shows it, but keep it
accessible to screen readers so filter inputs have a named context. */

View File

@@ -0,0 +1,269 @@
/**
* 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 { createRef, act } from 'react';
import {
render,
screen,
selectPillOption,
waitFor,
} from 'spec/helpers/testing-library';
import { ListViewFilterOperator } from '../types';
import UIFilters from './index';
import SelectFilter from './Select';
import type { FilterHandler } from './types';
const mockUpdateFilterValue = jest.fn();
beforeEach(() => {
mockUpdateFilterValue.mockClear();
});
test('select filter with ReactNode label uses option title when serializing selection', async () => {
// Regression for sc-104554: the chart-list Owner filter renders options
// with ReactNode labels (name + email). The value passed to
// updateFilterValue is serialized into URL / filter state and re-used to
// render the filter pill on return. It must carry the plain-text name
// (from `title`) and not fall back to the numeric user id.
const ReactNodeLabel = (
<div>
<span>John Doe</span>
<span>john@example.com</span>
</div>
);
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: ReactNodeLabel,
value: 42,
title: 'John Doe',
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
fetchSelects,
paginate: true,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectPillOption('John Doe', 'Owner');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'John Doe',
value: 42,
});
});
});
test('select filter falls back to stringified value when no string label or title is available', async () => {
const fetchSelects = jest.fn().mockResolvedValue({
data: [
{
label: <span>123</span>,
value: 123,
},
],
totalCount: 1,
});
const filters = [
{
Header: 'Something',
key: 'something',
id: 'something',
input: 'select' as const,
operator: ListViewFilterOperator.RelationOneMany,
unfilteredLabel: 'All',
fetchSelects,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectPillOption('123', 'Something');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: '123',
value: 123,
});
});
});
test('plain select with string label passes label through unchanged', async () => {
// Happy-path coverage for the typeof-string branch in onChange, exercised
// through the non-async Select wrapper (selects array, no fetchSelects).
const filters = [
{
Header: 'Status',
key: 'status',
id: 'status',
input: 'select' as const,
operator: ListViewFilterOperator.Equals,
unfilteredLabel: 'All',
selects: [
{ label: 'Published', value: 7 },
{ label: 'Draft', value: 8 },
],
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectPillOption('Published', 'Status');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'Published',
value: 7,
});
});
});
test('plain select with ReactNode label uses option title when serializing selection', async () => {
// Parallel coverage to the AsyncSelect ReactNode-with-title test, against
// the non-async Select wrapper. Guards against the two wrappers ever
// diverging on antd's two-arg onChange shape.
const ReactNodeLabel = (
<div>
<span>Jane Roe</span>
<span>jane@example.com</span>
</div>
);
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
selects: [{ label: ReactNodeLabel, value: 99, title: 'Jane Roe' }],
},
];
render(
<UIFilters
filters={filters}
internalFilters={[]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await selectPillOption('Jane Roe', 'Owner');
await waitFor(() => {
expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
label: 'Jane Roe',
value: 99,
});
});
});
test('clearFilter notifies onSelect with undefined and isClear=true', () => {
// The isClear flag is what allows the parent (Filters/index) to suppress
// onFilterUpdate side-effects when the user clears the filter rather than
// picking a new value. Lock that contract in.
const mockOnSelect = jest.fn();
const ref = createRef<FilterHandler>();
render(
<SelectFilter
Header="Owner"
initialValue={{ label: 'John Doe', value: 42 }}
onSelect={mockOnSelect}
selects={[{ label: 'John Doe', value: 42, title: 'John Doe' }]}
ref={ref}
/>,
);
act(() => {
ref.current?.clearFilter();
});
expect(mockOnSelect).toHaveBeenCalledWith(undefined, true);
});
test('rehydrates filter pill from initialValue with plain-string label', async () => {
// In the compact pill UI the rehydrated label is surfaced as the tooltip
// (visible on hover) rather than inline text. We verify the pill is in
// active state — the clear button is rendered — which confirms the
// SelectOption value object was correctly rehydrated.
const filters = [
{
Header: 'Owner',
key: 'owner',
id: 'owners',
input: 'select' as const,
operator: ListViewFilterOperator.RelationManyMany,
unfilteredLabel: 'All',
fetchSelects: jest.fn().mockResolvedValue({ data: [], totalCount: 0 }),
paginate: true,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[
{
id: 'owners',
operator: ListViewFilterOperator.RelationManyMany,
value: { label: 'John Doe', value: 42 },
},
]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
await waitFor(() => {
expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,154 @@
/**
* 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 {
useState,
useMemo,
forwardRef,
useImperativeHandle,
type RefObject,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { Select, AsyncSelect, FormLabel } from '@superset-ui/core/components';
import { ListViewFilter as Filter, SelectOption } from '../types';
import type { BaseFilter, FilterHandler } from './types';
import { FilterContainer } from './Base';
import { SELECT_WIDTH } from '../utils';
interface SelectFilterProps extends BaseFilter {
fetchSelects?: Filter['fetchSelects'];
name?: string;
onSelect: (selected: SelectOption | undefined, isClear?: boolean) => void;
optionFilterProps?: string[];
paginate?: boolean;
selects: Filter['selects'];
loading?: boolean;
dropdownStyle?: React.CSSProperties;
}
function SelectFilter(
{
Header,
name,
fetchSelects,
initialValue,
onSelect,
optionFilterProps,
selects = [],
loading = false,
dropdownStyle,
}: SelectFilterProps,
ref: RefObject<FilterHandler>,
) {
const [selectedOption, setSelectedOption] = useState(initialValue);
const onChange = (selected: SelectOption, option?: SelectOption) => {
// antd's `onChange` (with `labelInValue`) passes the `{label, value}`
// labeled-value as the first arg and the full option (which carries
// `title` and any other fields) as the second. Options may supply a
// ReactNode label (e.g. OwnerSelectLabel for the chart list Owner
// filter). Since this object is serialized into the URL and rehydrated
// as the filter pill on return, we need a plain string. Prefer `title`
// (set by callers to the human-readable name) before falling back to
// the value.
onSelect(
selected
? {
label:
typeof selected.label === 'string'
? selected.label
: (option?.title ?? String(selected.value)),
value: selected.value,
}
: undefined,
);
setSelectedOption(selected);
};
const onClear = () => {
onSelect(undefined, true);
setSelectedOption(undefined);
};
useImperativeHandle(ref, () => ({
clearFilter: () => {
onClear();
},
}));
const fetchAndFormatSelects = useMemo(
() => async (inputValue: string, page: number, pageSize: number) => {
if (fetchSelects) {
const selectValues = await fetchSelects(inputValue, page, pageSize);
return {
data: selectValues.data,
totalCount: selectValues.totalCount,
};
}
return {
data: [],
totalCount: 0,
};
},
[fetchSelects],
);
const placeholder = t('Choose...');
return (
<FilterContainer
data-test="select-filter-container"
width={SELECT_WIDTH}
vertical
justify="center"
align="start"
>
<FormLabel>{Header}</FormLabel>
{fetchSelects ? (
<AsyncSelect
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select"
onChange={onChange}
onClear={onClear}
options={fetchAndFormatSelects}
optionFilterProps={optionFilterProps}
placeholder={placeholder}
dropdownStyle={dropdownStyle}
showSearch
value={selectedOption}
/>
) : (
<Select
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
data-test="filters-select"
labelInValue
onChange={onChange}
onClear={onClear}
options={selects}
placeholder={placeholder}
dropdownStyle={dropdownStyle}
showSearch
value={selectedOption}
loading={loading}
/>
)}
</FilterContainer>
);
}
export default forwardRef(SelectFilter);

View File

@@ -1,251 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createRef, act } from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { NO_TIME_RANGE, SupersetClient } from '@superset-ui/core';
import TimeRangeFilter from './TimeRange';
import type { FilterHandler } from './types';
// Suppress debounced evaluation — the initial useEffect handles the committed
// value; the debounced path is an optimistic UX enhancement, not a contract.
jest.mock('src/explore/exploreUtils', () => ({
...jest.requireActual('src/explore/exploreUtils'),
useDebouncedEffect: jest.fn(),
}));
jest.mock('src/explore/components/controls/DateFilterControl/utils', () => ({
FRAME_OPTIONS: [
{ label: 'No filter', value: 'No filter' },
{ label: 'Custom', value: 'Custom' },
],
guessFrame: jest.fn().mockReturnValue('Custom'),
// 'No filter' is the string value of NO_TIME_RANGE constant
useDefaultTimeFilter: jest.fn().mockReturnValue('No filter'),
}));
jest.mock(
'src/explore/components/controls/DateFilterControl/components',
() => ({
AdvancedFrame: () => <div data-test="advanced-frame" />,
CalendarFrame: () => <div data-test="calendar-frame" />,
CommonFrame: () => <div data-test="common-frame" />,
CustomFrame: ({ value }: { value: string }) => (
<div data-test="custom-frame">{value}</div>
),
}),
);
jest.mock(
'src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame',
() => ({
CurrentCalendarFrame: () => <div data-testid="current-calendar-frame" />,
}),
);
const VALID_RANGE = '2024-01-01 : 2024-01-31';
// Default successful response that fetchTimeRange and the Apply handler both use
const MOCK_TIME_RANGE_RESULT = {
json: {
result: [{ since: '2024-01-01T00:00:00', until: '2024-01-31T23:59:59' }],
},
};
let getSpy: jest.SpyInstance;
beforeEach(() => {
getSpy = jest
.spyOn(SupersetClient, 'get')
.mockResolvedValue(MOCK_TIME_RANGE_RESULT as any);
});
afterEach(() => {
getSpy.mockRestore();
});
function renderFilter(
props: Partial<{
value: string;
onSubmit: jest.Mock;
onClose: jest.Mock;
}> = {},
) {
const onSubmit = props.onSubmit ?? jest.fn();
const onClose = props.onClose ?? jest.fn();
return render(
<TimeRangeFilter
value={props.value ?? VALID_RANGE}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
}
test('renders range type label, actual time range section, and footer buttons', () => {
renderFilter();
expect(screen.getByText('Range type')).toBeInTheDocument();
expect(screen.getByText('Actual time range')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument();
});
test('shows the custom frame when guessFrame returns Custom', () => {
renderFilter();
expect(screen.getByTestId('custom-frame')).toBeInTheDocument();
});
test('Apply is disabled until the API validates the initial value', async () => {
// Block resolution so we can observe disabled state
let resolve: (v: typeof MOCK_TIME_RANGE_RESULT) => void;
getSpy.mockReturnValue(
new Promise(res => {
resolve = res;
}),
);
renderFilter();
const apply = screen.getByRole('button', { name: /apply/i });
expect(apply).toBeDisabled();
act(() => {
resolve!(MOCK_TIME_RANGE_RESULT);
});
await waitFor(() => {
expect(apply).not.toBeDisabled();
});
});
test('Apply is enabled when the API returns a valid result', async () => {
renderFilter();
const apply = screen.getByRole('button', { name: /apply/i });
await waitFor(() => {
expect(apply).not.toBeDisabled();
});
});
test('Apply is disabled when the API returns an error response', async () => {
getSpy.mockRejectedValue(new Error('Bad request'));
renderFilter();
const apply = screen.getByRole('button', { name: /apply/i });
// Give fetchTimeRange time to reject and set validTimeRange=false
await waitFor(() => {
expect(apply).toBeDisabled();
});
});
test('Cancel button calls onClose', async () => {
const onClose = jest.fn();
renderFilter({ onClose });
await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(onClose).toHaveBeenCalledTimes(1);
});
test('Apply calls onSubmit([since, until]) and onClose when API succeeds', async () => {
const onSubmit = jest.fn();
const onClose = jest.fn();
renderFilter({ onSubmit, onClose });
const apply = screen.getByRole('button', { name: /apply/i });
await waitFor(() => {
expect(apply).not.toBeDisabled();
});
await userEvent.click(apply);
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith([
'2024-01-01T00:00:00',
'2024-01-31T23:59:59',
]);
});
expect(onClose).toHaveBeenCalledTimes(1);
});
test('Apply calls onClose but not onSubmit when the API call throws', async () => {
const onSubmit = jest.fn();
const onClose = jest.fn();
// fetchTimeRange succeeds (for validTimeRange), but the Apply API call fails
getSpy
.mockResolvedValueOnce(MOCK_TIME_RANGE_RESULT as any) // fetchTimeRange in useEffect
.mockRejectedValueOnce(new Error('network')); // Apply button API call
renderFilter({ onSubmit, onClose });
const apply = screen.getByRole('button', { name: /apply/i });
await waitFor(() => {
expect(apply).not.toBeDisabled();
});
await userEvent.click(apply);
await waitFor(() => {
expect(onClose).toHaveBeenCalledTimes(1);
});
expect(onSubmit).not.toHaveBeenCalled();
});
test('Apply with NO_TIME_RANGE calls onSubmit(undefined) and onClose without an API call', async () => {
const onSubmit = jest.fn();
const onClose = jest.fn();
render(
<TimeRangeFilter
value={NO_TIME_RANGE}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
const apply = screen.getByRole('button', { name: /apply/i });
await waitFor(() => {
expect(apply).not.toBeDisabled();
});
const callsBefore = getSpy.mock.calls.length;
await userEvent.click(apply);
expect(onSubmit).toHaveBeenCalledWith(undefined);
expect(onClose).toHaveBeenCalledTimes(1);
// No extra API call for NO_TIME_RANGE — the button short-circuits
expect(getSpy.mock.calls.length).toBe(callsBefore);
});
test('clearFilter via ref calls onSubmit(undefined)', async () => {
const onSubmit = jest.fn();
const ref = createRef<FilterHandler>();
render(
<TimeRangeFilter
ref={ref}
value={VALID_RANGE}
onSubmit={onSubmit}
onClose={jest.fn()}
/>,
);
act(() => {
ref.current?.clearFilter();
});
expect(onSubmit).toHaveBeenCalledWith(undefined);
});

View File

@@ -1,291 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
type RefObject,
} from 'react';
import { t } from '@apache-superset/core/translation';
import {
NO_TIME_RANGE,
SupersetClient,
fetchTimeRange,
} from '@superset-ui/core';
import rison from 'rison';
import { css, styled, useTheme } from '@apache-superset/core/theme';
import {
Button,
Constants,
Divider,
Icons,
Select,
} from '@superset-ui/core/components';
import { useDebouncedEffect } from 'src/explore/exploreUtils';
import {
FRAME_OPTIONS,
guessFrame,
useDefaultTimeFilter,
} from 'src/explore/components/controls/DateFilterControl/utils';
import {
AdvancedFrame,
CalendarFrame,
CommonFrame,
CustomFrame,
} from 'src/explore/components/controls/DateFilterControl/components';
import { CurrentCalendarFrame } from 'src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame';
import type { FrameType } from 'src/explore/components/controls/DateFilterControl/types';
import type { FilterHandler } from './types';
interface TimeRangeFilterProps {
value?: string;
onSubmit: (value: [string, string] | undefined) => void;
onClose: () => void;
}
const StyledRangeType = styled(Select)`
width: 272px;
`;
const ContentWrapper = styled.div`
${({ theme }) => css`
width: 600px;
padding: ${theme.sizeUnit * 3}px;
background: ${theme.colorBgElevated};
border-radius: ${theme.borderRadiusLG}px;
box-shadow: ${theme.boxShadowSecondary};
.ant-row {
margin-top: 8px;
}
.ant-picker {
padding: 4px 17px 4px;
border-radius: 4px;
}
.ant-divider-horizontal {
margin: 16px 0;
}
.control-label {
font-size: ${theme.fontSizeSM}px;
line-height: 16px;
margin: 8px 0;
}
.section-title {
font-style: normal;
font-weight: ${theme.fontWeightStrong};
font-size: 15px;
line-height: 24px;
margin-bottom: 8px;
}
.control-anchor-to {
margin-top: 16px;
}
.control-anchor-to-datetime {
width: 217px;
}
.footer {
text-align: right;
}
`}
`;
const IconWrapper = styled.span`
span {
margin-right: ${({ theme }) => 2 * theme.sizeUnit}px;
vertical-align: middle;
}
.text {
vertical-align: middle;
}
.error {
color: ${({ theme }) => theme.colorError};
}
`;
function TimeRangeFilter(
{ value: valueProp, onSubmit, onClose }: TimeRangeFilterProps,
ref: RefObject<FilterHandler>,
) {
const defaultTimeFilter = useDefaultTimeFilter();
const value = valueProp ?? defaultTimeFilter;
const theme = useTheme();
// guessedFrame is only used for the initial useState — value is stable at
// mount because CompactFilterTrigger uses destroyPopupOnHide, so the panel
// always mounts fresh with the current committed value.
const guessedFrame = useMemo(() => guessFrame(value), [value]);
const [frame, setFrame] = useState<FrameType>(guessedFrame);
const [timeRangeValue, setTimeRangeValue] = useState(value);
const [evalResponse, setEvalResponse] = useState(value);
const [validTimeRange, setValidTimeRange] = useState(false);
const [lastFetched, setLastFetched] = useState(value);
// Evaluate the committed value shown in "Actual time range".
useEffect(() => {
if (value === NO_TIME_RANGE) {
setEvalResponse(NO_TIME_RANGE);
setValidTimeRange(true);
return;
}
fetchTimeRange(value).then(({ value: actual, error }) => {
if (error) {
setEvalResponse(error ?? '');
setValidTimeRange(false);
} else {
setEvalResponse(actual ?? value);
setValidTimeRange(true);
}
setLastFetched(value);
});
}, [value]);
// Debounced evaluation of the in-progress selection (drives "Actual time range").
useDebouncedEffect(
() => {
if (timeRangeValue === NO_TIME_RANGE) {
setEvalResponse(NO_TIME_RANGE);
setLastFetched(NO_TIME_RANGE);
setValidTimeRange(true);
return;
}
if (lastFetched !== timeRangeValue) {
fetchTimeRange(timeRangeValue).then(({ value: actual, error }) => {
if (error) {
setEvalResponse(error ?? '');
setValidTimeRange(false);
} else {
setEvalResponse(actual ?? '');
setValidTimeRange(true);
}
setLastFetched(timeRangeValue);
});
}
},
Constants.SLOW_DEBOUNCE,
[timeRangeValue],
);
useImperativeHandle(ref, () => ({
clearFilter: () => {
onSubmit(undefined);
},
}));
function onChangeFrame(val: FrameType) {
if (val === NO_TIME_RANGE) {
setTimeRangeValue(NO_TIME_RANGE);
}
setFrame(val);
}
return (
<ContentWrapper>
<div className="control-label">{t('Range type')}</div>
<StyledRangeType
ariaLabel={t('Range type')}
options={FRAME_OPTIONS}
value={frame}
onChange={onChangeFrame}
/>
{frame !== 'No filter' && <Divider />}
{frame === 'Common' && (
<CommonFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Calendar' && (
<CalendarFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Current' && (
<CurrentCalendarFrame
value={timeRangeValue}
onChange={setTimeRangeValue}
/>
)}
{frame === 'Advanced' && (
<AdvancedFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'Custom' && (
<CustomFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
<Divider />
<div>
<div className="section-title">{t('Actual time range')}</div>
{validTimeRange && (
<div>
{evalResponse === NO_TIME_RANGE ? t('No filter') : evalResponse}
</div>
)}
{!validTimeRange && (
<IconWrapper className="warning">
<Icons.ExclamationCircleOutlined iconColor={theme.colorError} />
<span className="text error">{evalResponse}</span>
</IconWrapper>
)}
</div>
<Divider />
<div className="footer">
<Button buttonStyle="secondary" cta key="cancel" onClick={onClose}>
{t('CANCEL')}
</Button>
<Button
buttonStyle="primary"
cta
disabled={!validTimeRange}
key="apply"
onClick={async () => {
if (timeRangeValue === NO_TIME_RANGE) {
onSubmit(undefined);
onClose();
return;
}
// fetchTimeRange returns a formatted display string ("X ≤ col < Y"),
// not the raw since/until strings. Call the API directly to get them.
try {
const response = await SupersetClient.get({
endpoint: `/api/v1/time_range/?q=${rison.encode_uri(timeRangeValue)}`,
});
const since: string | undefined =
response?.json?.result[0]?.since;
const until: string | undefined =
response?.json?.result[0]?.until;
if (since !== undefined && until !== undefined) {
onSubmit([since, until]);
}
} catch {
// leave filter unchanged on error
}
onClose();
}}
>
{t('APPLY')}
</Button>
</div>
</ContentWrapper>
);
}
export default forwardRef(TimeRangeFilter);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { ListViewFilterOperator } from '../types';
import UIFilters from './index';
@@ -218,7 +217,7 @@ test('datetime_range filter renders as CompactFilterTrigger with dialog aria-has
expect(screen.getByText('Time range')).toBeInTheDocument();
});
test('datetime_range pill shows active state when a time range string is set', () => {
test('datetime_range pill shows active state when value is set', () => {
const filters = [
{
Header: 'Time range',
@@ -236,21 +235,19 @@ test('datetime_range pill shows active state when a time range string is set', (
{
id: 'time_range',
operator: ListViewFilterOperator.Between,
value: 'Last week',
value: ['2024-01-01', '2024-12-31'],
},
]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
// Clear icon is inside the pill (not a separate button)
const pill = screen.getByTestId('compact-filter-pill');
const clearIcon = screen.getByTestId('compact-filter-clear');
expect(clearIcon).toBeInTheDocument();
expect(pill).toContainElement(clearIcon);
expect(
screen.getByRole('button', { name: /clear time range filter/i }),
).toBeInTheDocument();
});
test('datetime_range pill is inactive when value is NO_TIME_RANGE', () => {
test('datetime_range tooltip formats unix timestamps as human-readable dates', () => {
const filters = [
{
Header: 'Time range',
@@ -258,27 +255,34 @@ test('datetime_range pill is inactive when value is NO_TIME_RANGE', () => {
id: 'time_range',
input: 'datetime_range' as const,
operator: ListViewFilterOperator.Between,
dateFilterValueType: 'unix' as const,
},
];
render(
// Jan 1 2024 00:00:00 UTC in ms
const start = 1704067200000;
const end = 1735689599000;
const { container } = render(
<UIFilters
filters={filters}
internalFilters={[
{
id: 'time_range',
operator: ListViewFilterOperator.Between,
value: 'No filter',
value: [start, end],
},
]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
// Should NOT contain raw unix timestamp numbers in the rendered output
expect(container.textContent).not.toContain(String(start));
expect(container.textContent).not.toContain(String(end));
});
test('datetime_range pill shows the time range string as tooltip title', () => {
test('datetime_range tooltip leaves ISO strings as-is', () => {
const filters = [
{
Header: 'Time range',
@@ -296,15 +300,17 @@ test('datetime_range pill shows the time range string as tooltip title', () => {
{
id: 'time_range',
operator: ListViewFilterOperator.Between,
value: 'Last month',
value: ['2024-01-01T00:00:00.000Z', '2024-12-31T23:59:59.000Z'],
},
]}
updateFilterValue={mockUpdateFilterValue}
/>,
);
// Pill is active and clear icon is inside
expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
// Pill is active (clear button present)
expect(
screen.getByRole('button', { name: /clear time range filter/i }),
).toBeInTheDocument();
});
test('numerical_range filter renders as CompactFilterTrigger with dialog aria-haspopup', () => {
@@ -362,70 +368,6 @@ test('numerical_range pill shows active state when value is set', () => {
).toBeInTheDocument();
});
test('datetime_range onClear calls updateFilterValue with undefined directly', async () => {
const updateFilterValue = jest.fn();
const filters = [
{
Header: 'Time range',
key: 'time_range',
id: 'time_range',
input: 'datetime_range' as const,
operator: ListViewFilterOperator.Between,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[
{
id: 'time_range',
operator: ListViewFilterOperator.Between,
value: 'Last week',
},
]}
updateFilterValue={updateFilterValue}
/>,
);
const clearIcon = screen.getByTestId('compact-filter-clear');
await userEvent.click(clearIcon);
expect(updateFilterValue).toHaveBeenCalledWith(0, undefined);
});
test('numerical_range onClear calls updateFilterValue with undefined directly', async () => {
const updateFilterValue = jest.fn();
const filters = [
{
Header: 'Age range',
key: 'age_range',
id: 'age_range',
input: 'numerical_range' as const,
operator: ListViewFilterOperator.Between,
},
];
render(
<UIFilters
filters={filters}
internalFilters={[
{
id: 'age_range',
operator: ListViewFilterOperator.Between,
value: [18, 65],
},
]}
updateFilterValue={updateFilterValue}
/>,
);
const clearBtn = screen.getByRole('button', {
name: /clear age range filter/i,
});
await userEvent.click(clearBtn);
expect(updateFilterValue).toHaveBeenCalledWith(0, undefined);
});
test('renders only the first search filter when multiple search filters are configured', () => {
const filters = [
{

View File

@@ -20,15 +20,14 @@ import {
createRef,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
RefObject,
} from 'react';
import { extendedDayjs } from '@superset-ui/core/utils/dates';
import { withTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import type {
ListViewFilterValue as FilterValue,
@@ -37,10 +36,9 @@ import type {
SelectOption,
} from '../types';
import type { FilterHandler } from './types';
import { NO_TIME_RANGE } from '@superset-ui/core';
import SearchFilter from './Search';
import DateRangeFilter from './DateRange';
import NumericalRangeFilter from './NumericalRange';
import TimeRangeFilter from './TimeRange';
import CompactFilterTrigger from './CompactFilterTrigger';
import CompactSelectPanel from './CompactSelectPanel';
import FilterPopoverContent from './FilterPopoverContent';
@@ -70,63 +68,6 @@ function UIFilters(
{},
);
// Evaluated human-readable labels for datetime_range pills (e.g. "2024-05-01 : 2024-05-31").
const [timeRangeTooltips, setTimeRangeTooltips] = useState<
Record<number, string>
>({});
// On cold load, URL params restore values but not labels for fetchSelects filters.
// Fetch the first page of options and cache the matching label so the tooltip works.
useEffect(() => {
filters.forEach((filter, index) => {
if (filter.input !== 'select' || !filter.fetchSelects) return;
if (tooltipLabels[index]) return;
const val = internalFilters?.[index]?.value as SelectOption | undefined;
if (!val?.value) return;
filter.fetchSelects('', 0, 500).then(result => {
const match = result?.data?.find(
(s: SelectOption) => s.value === val.value,
);
if (match) {
const lbl =
typeof match.label === 'string'
? match.label
: String(match.value ?? '');
setTooltipLabels(prev => ({ ...prev, [index]: lbl }));
}
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [internalFilters]);
// Build datetime_range tooltips from the resolved [start, end] array value.
// Handles both ISO strings and unix-ms numbers.
useEffect(() => {
filters.forEach((filter, index) => {
if (filter.input !== 'datetime_range') return;
const val = internalFilters?.[index]?.value;
if (Array.isArray(val) && val.length === 2) {
const fmt = (v: unknown) => {
const d = new Date(v as string | number);
return isNaN(d.getTime())
? String(v)
: d.toISOString().replace('T', ' ').slice(0, 19);
};
const tooltip = `${fmt(val[0])} ${fmt(val[1])}`;
setTimeRangeTooltips(prev =>
prev[index] === tooltip ? prev : { ...prev, [index]: tooltip },
);
} else {
setTimeRangeTooltips(prev => {
if (!(index in prev)) return prev;
const next = { ...prev };
delete next[index];
return next;
});
}
});
}, [filters, internalFilters]);
const clearFilterAtIndex = useCallback(
(index: number) => {
filterRefs[index]?.current?.clearFilter?.();
@@ -148,7 +89,6 @@ function UIFilters(
updateFilterValue(index, undefined);
});
setTooltipLabels({});
setTimeRangeTooltips({});
},
clearFilterById: (id: string) => {
const index = filters.findIndex(f => f.id === id);
@@ -158,226 +98,197 @@ function UIFilters(
},
}));
// Search always leads the filter bar regardless of declaration order.
// Only the first search filter renders; subsequent ones are skipped (see note below).
// Only the first search filter renders inline; subsequent ones are skipped
// to keep one search box per page (multi-field search pages like Users would
// otherwise show several input boxes in the header).
// NOTE: This means secondary search fields (e.g. Email/Username on Users,
// Group Key on RLS) are not currently accessible via the filter bar. Those
// pages previously relied on multiple inline inputs. This is a known UX
// trade-off — revisit if admin workflows require additional search fields.
let searchFilterRendered = false;
// Render in two passes: search first, then all other filter types.
const renderFilter = (_: (typeof filters)[number], index: number) => {
const {
Header,
fetchSelects,
key,
id,
input,
selects,
toolTipDescription,
onFilterUpdate,
loading,
min,
max,
autoComplete,
inputName,
popupStyle,
dateFilterValueType,
} = filters[index];
const initialValue = internalFilters?.[index]?.value;
if (input === 'select') {
const selectValue = initialValue as SelectOption | undefined;
// Prefer cached label (survives URL round-trips where only the value
// is preserved). Fall back to the static selects list for cold loads.
const cachedLabel = tooltipLabels[index];
const staticFallback = cachedLabel
? undefined
: selects?.find(s => s.value === selectValue?.value)?.label;
const tooltipTitle = !!selectValue
? cachedLabel ||
(typeof staticFallback === 'string' ? staticFallback : undefined)
: t('Choose...');
return (
<span key={key} data-test="select-filter-container">
<CompactFilterTrigger
label={Header}
hasValue={!!selectValue}
tooltipTitle={tooltipTitle}
onClear={() => clearFilterAtIndex(index)}
>
{({ isOpen, onClose }) => (
<CompactSelectPanel
ref={filterRefs[index]}
selects={selects}
fetchSelects={fetchSelects}
value={initialValue as SelectOption | undefined}
loading={loading ?? false}
isOpen={isOpen}
onClose={onClose}
panelStyle={popupStyle}
onSelect={(
option: SelectOption | undefined,
isClear?: boolean,
) => {
if (option && !isClear) {
setTooltipLabels(prev => ({
...prev,
[index]:
typeof option.label === 'string'
? option.label
: String(option.value ?? ''),
}));
}
if (onFilterUpdate && !isClear) {
onFilterUpdate(option);
}
updateFilterValue(index, option);
}}
/>
)}
</CompactFilterTrigger>
</span>
);
}
if (input === 'search' && typeof Header === 'string') {
if (searchFilterRendered) return null;
searchFilterRendered = true;
return (
<SearchFilter
ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
key={key}
name={inputName ?? id}
toolTipDescription={toolTipDescription}
onSubmit={(value: string) => {
if (onFilterUpdate) {
onFilterUpdate(value);
}
updateFilterValue(index, value);
}}
autoComplete={autoComplete}
/>
);
}
if (input === 'datetime_range') {
// dateFilterValueType absent or 'unix': column stores unix ms (e.g. Query History start_time).
// 'iso': column stores ISO date strings (e.g. UsersList created_on, ActionLog dttm).
const isUnixType = !dateFilterValueType || dateFilterValueType === 'unix';
// initialValue may be [ms, ms] (unix), ["iso","iso"] (iso), or legacy string.
// Always reconstruct panelValue as "ISO : ISO" so the TimeRange panel
// can parse it as a Custom date range regardless of storage type.
let resolvedIsoRange: [string, string] | null = null;
if (Array.isArray(initialValue) && initialValue.length === 2) {
if (typeof initialValue[0] === 'number') {
resolvedIsoRange = [
new Date(initialValue[0]).toISOString(),
new Date(initialValue[1] as number).toISOString(),
];
} else if (typeof initialValue[0] === 'string') {
resolvedIsoRange = initialValue as [string, string];
}
}
const legacyStringVal =
!resolvedIsoRange &&
typeof initialValue === 'string' &&
initialValue !== NO_TIME_RANGE
? initialValue
: null;
const hasTimeValue = !!(resolvedIsoRange || legacyStringVal);
const panelValue =
resolvedIsoRange?.join(' : ') ?? legacyStringVal ?? undefined;
return (
<CompactFilterTrigger
key={key}
label={Header}
hasValue={hasTimeValue}
tooltipTitle={
hasTimeValue ? (timeRangeTooltips[index] ?? panelValue) : undefined
return (
<>
{filters.map(
(
{
Header,
fetchSelects,
key,
id,
input,
selects,
toolTipDescription,
onFilterUpdate,
loading,
dateFilterValueType,
min,
max,
autoComplete,
inputName,
popupStyle,
},
index,
) => {
const initialValue = internalFilters?.[index]?.value;
if (input === 'select') {
const selectValue = initialValue as SelectOption | undefined;
// Prefer cached label (survives URL round-trips where only the value
// is preserved). Fall back to the static selects list for cold loads.
const cachedLabel = tooltipLabels[index];
const staticFallback = cachedLabel
? undefined
: selects?.find(s => s.value === selectValue?.value)?.label;
const tooltipTitle = !!selectValue
? cachedLabel ||
(typeof staticFallback === 'string'
? staticFallback
: undefined)
: undefined;
return (
<span key={key} data-test="select-filter-container">
<CompactFilterTrigger
label={Header}
hasValue={!!selectValue}
tooltipTitle={tooltipTitle}
onClear={() => clearFilterAtIndex(index)}
>
{({ isOpen, onClose }) => (
<CompactSelectPanel
ref={filterRefs[index]}
selects={selects}
fetchSelects={fetchSelects}
value={initialValue as SelectOption | undefined}
loading={loading ?? false}
isOpen={isOpen}
onClose={onClose}
panelStyle={popupStyle}
onSelect={(
option: SelectOption | undefined,
isClear?: boolean,
) => {
if (option && !isClear) {
setTooltipLabels(prev => ({
...prev,
[index]:
typeof option.label === 'string'
? option.label
: String(option.value ?? ''),
}));
}
if (onFilterUpdate && !isClear) {
onFilterUpdate(option);
}
updateFilterValue(index, option);
}}
/>
)}
</CompactFilterTrigger>
</span>
);
}
popupType="dialog"
onClear={() => {
updateFilterValue(index, undefined);
}}
>
{({ onClose }) => (
<TimeRangeFilter
ref={filterRefs[index]}
value={panelValue}
onClose={onClose}
onSubmit={value => {
if (!value) {
updateFilterValue(index, undefined);
} else if (isUnixType) {
// Convert ISO strings to unix ms for numeric columns
updateFilterValue(index, [
new Date(value[0]).getTime(),
new Date(value[1]).getTime(),
]);
} else {
updateFilterValue(index, value);
}
}}
/>
)}
</CompactFilterTrigger>
);
}
if (input === 'numerical_range') {
const hasRangeValue =
Array.isArray(initialValue) &&
initialValue.some(v => v !== null && v !== undefined);
const rangeTooltip = hasRangeValue
? (initialValue as (number | null | undefined)[])
.filter(v => v !== null && v !== undefined)
.join(' ')
: undefined;
return (
<CompactFilterTrigger
key={key}
label={Header}
hasValue={hasRangeValue}
tooltipTitle={rangeTooltip}
popupType="dialog"
onClear={() => {
updateFilterValue(index, undefined);
}}
>
{({ onClose }) => (
<FilterPopoverContent onClose={onClose}>
<NumericalRangeFilter
if (input === 'search' && typeof Header === 'string') {
if (searchFilterRendered) return null;
searchFilterRendered = true;
return (
<SearchFilter
ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
min={min}
max={max}
name={id}
onSubmit={value => updateFilterValue(index, value)}
/>
</FilterPopoverContent>
)}
</CompactFilterTrigger>
);
}
return null;
};
key={key}
name={inputName ?? id}
toolTipDescription={toolTipDescription}
onSubmit={(value: string) => {
if (onFilterUpdate) {
onFilterUpdate(value);
}
return (
<>
{/* Search first */}
{filters.map((_, index) =>
filters[index].input === 'search'
? renderFilter(filters[index], index)
: null,
)}
{/* Then all other filter types */}
{filters.map((_, index) =>
filters[index].input !== 'search'
? renderFilter(filters[index], index)
: null,
updateFilterValue(index, value);
}}
autoComplete={autoComplete}
/>
);
}
if (input === 'datetime_range') {
const hasDateValue =
Array.isArray(initialValue) && initialValue.some(Boolean);
const dateTooltip = hasDateValue
? (initialValue as (string | number)[])
.filter(Boolean)
.map(v => {
if (typeof v === 'number') {
// unix milliseconds → human-readable date
return extendedDayjs(v).format('MMM D, YYYY HH:mm');
}
// ISO string — already readable
return String(v);
})
.join(' ')
: undefined;
return (
<CompactFilterTrigger
key={key}
label={Header}
hasValue={hasDateValue}
tooltipTitle={dateTooltip}
popupType="dialog"
onClear={() => {
filterRefs[index]?.current?.clearFilter?.();
}}
>
{({ onClose }) => (
<FilterPopoverContent onClose={onClose}>
<DateRangeFilter
ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
name={id}
onSubmit={value => updateFilterValue(index, value)}
dateFilterValueType={dateFilterValueType || 'unix'}
/>
</FilterPopoverContent>
)}
</CompactFilterTrigger>
);
}
if (input === 'numerical_range') {
const hasRangeValue =
Array.isArray(initialValue) &&
initialValue.some(v => v !== null && v !== undefined);
const rangeTooltip = hasRangeValue
? (initialValue as (number | null | undefined)[])
.filter(v => v !== null && v !== undefined)
.join(' ')
: undefined;
return (
<CompactFilterTrigger
key={key}
label={Header}
hasValue={hasRangeValue}
tooltipTitle={rangeTooltip}
popupType="dialog"
onClear={() => {
filterRefs[index]?.current?.clearFilter?.();
}}
>
{({ onClose }) => (
<FilterPopoverContent onClose={onClose}>
<NumericalRangeFilter
ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
min={min}
max={max}
name={id}
onSubmit={value => updateFilterValue(index, value)}
/>
</FilterPopoverContent>
)}
</CompactFilterTrigger>
);
}
return null;
},
)}
</>
);

View File

@@ -75,33 +75,20 @@ const ListViewStyles = styled.div`
column-gap: ${theme.sizeUnit * 2}px;
row-gap: ${theme.sizeUnit * 2}px;
/* Search input — fixed width/height matching pill height, label hidden */
[data-test='search-filter-container'] {
width: ${theme.sizeUnit * 44}px;
flex-shrink: 0;
height: ${theme.controlHeight}px;
align-self: center;
/* Hide the FormLabel Flex wrapper entirely so it doesn't affect
the column's justify-content centering calculation. */
> .ant-flex {
display: none;
}
justify-content: center;
label {
display: none;
}
.ant-input-affix-wrapper {
width: 100%;
height: 100%;
}
}
/* Select filter pill wrappers — make them proper flex items so the
inline-flex button inside doesn't introduce line-box quirks. */
[data-test='select-filter-container'] {
display: flex;
align-items: center;
align-self: center;
}
}
}

View File

@@ -468,12 +468,9 @@ export function saveDashboardRequest(
);
const cleanedData: JsonObject = {
...data,
...(certified_by !== undefined && {
certified_by,
certification_details: certified_by
? (certification_details ?? '')
: '',
}),
certified_by: certified_by || '',
certification_details:
certified_by && certification_details ? certification_details : '',
css: css || '',
dashboard_title: dashboard_title || t('[ untitled dashboard ]'),
owners: ensureIsArray(owners as JsonObject[]).map((o: JsonObject) =>

View File

@@ -28,7 +28,6 @@ import {
Select,
AsyncSelect,
} from '@superset-ui/core/components';
import { getUserDisplayLabel } from 'src/features/users/utils';
import { FormValues, GroupModalProps } from './types';
import { createGroup, fetchUserOptions, updateGroup } from './utils';
@@ -95,7 +94,7 @@ function GroupListModal({
users:
group?.users?.map(user => ({
value: user.id,
label: getUserDisplayLabel(user),
label: user.username,
})) || [],
};

View File

@@ -19,7 +19,6 @@
import { t } from '@apache-superset/core/translation';
import { SupersetClient } from '@superset-ui/core';
import rison from 'rison';
import { getUserDisplayLabel } from 'src/features/users/utils';
import { FormValues } from './types';
export const createGroup = async (values: FormValues) => {
@@ -65,7 +64,7 @@ export const fetchUserOptions = async (
return {
data: results.map((user: any) => ({
value: user.id,
label: getUserDisplayLabel(user),
label: user.username,
})),
totalCount: response.json?.count ?? 0,
};

View File

@@ -63,16 +63,6 @@ jest.mock('@superset-ui/core', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('RoleListEditModal', () => {
beforeEach(() => {
(SupersetClient.get as jest.Mock).mockResolvedValue({
json: { count: 0, result: [] },
});
});
afterEach(() => {
jest.clearAllMocks();
});
const mockRole = {
id: 1,
name: 'Admin',
@@ -157,8 +147,8 @@ describe('RoleListEditModal', () => {
// Wait for user hydration to complete so setFieldsValue has populated
// the form with the fetched users before submitting.
await screen.findByText('John Doe');
await screen.findByText('Jane Smith');
await screen.findByText('johndoe');
await screen.findByText('janesmith');
fireEvent.change(screen.getByTestId('role-name-input'), {
target: { value: 'Updated Role' },
@@ -251,19 +241,16 @@ describe('RoleListEditModal', () => {
test('preserves missing IDs as numeric fallbacks on partial hydration', async () => {
const mockGet = SupersetClient.get as jest.Mock;
mockGet.mockImplementation(({ endpoint }) => {
if (
endpoint?.includes(
`/api/v1/security/roles/${mockRole.id}/permissions/`,
)
) {
if (endpoint?.includes('/api/v1/security/permissions-resources/')) {
// Only return permission id=10, not id=20
return Promise.resolve({
json: {
count: 1,
result: [
{
id: 10,
permission_name: 'can_read',
view_menu_name: 'Dashboard',
permission: { name: 'can_read' },
view_menu: { name: 'Dashboard' },
},
],
},
@@ -297,11 +284,7 @@ describe('RoleListEditModal', () => {
mockToasts.addDangerToast.mockClear();
const mockGet = SupersetClient.get as jest.Mock;
mockGet.mockImplementation(({ endpoint }) => {
if (
endpoint?.includes(
`/api/v1/security/roles/${mockRole.id}/permissions/`,
)
) {
if (endpoint?.includes('/api/v1/security/permissions-resources/')) {
return Promise.reject(new Error('network error'));
}
if (endpoint?.includes('/api/v1/security/groups/')) {
@@ -371,26 +354,24 @@ describe('RoleListEditModal', () => {
};
mockGet.mockImplementation(({ endpoint }) => {
if (endpoint?.includes(`/api/v1/security/roles/${roleA.id}/permissions/`)) {
if (endpoint?.includes('/api/v1/security/permissions-resources/')) {
const query = rison.decode(endpoint.split('?q=')[1]) as Record<
string,
unknown
>;
const filters = query.filters as Array<{
col: string;
opr: string;
value: number[];
}>;
const ids = filters?.[0]?.value || [];
const result = ids.map((id: number) => ({
id,
permission: { name: `perm_${id}` },
view_menu: { name: `view_${id}` },
}));
return Promise.resolve({
json: {
result: roleA.permission_ids.map(pid => ({
id: pid,
permission_name: `perm_${pid}`,
view_menu_name: `view_${pid}`,
})),
},
});
}
if (endpoint?.includes(`/api/v1/security/roles/${roleB.id}/permissions/`)) {
return Promise.resolve({
json: {
result: roleB.permission_ids.map(pid => ({
id: pid,
permission_name: `perm_${pid}`,
view_menu_name: `view_${pid}`,
})),
},
json: { count: result.length, result },
});
}
return Promise.resolve({ json: { count: 0, result: [] } });
@@ -407,7 +388,7 @@ describe('RoleListEditModal', () => {
await waitFor(() => {
const permCall = mockGet.mock.calls.find(([c]) =>
c.endpoint.includes(`/api/v1/security/roles/${roleA.id}/permissions/`),
c.endpoint.includes('/api/v1/security/permissions-resources/'),
);
expect(permCall).toBeTruthy();
});
@@ -427,16 +408,26 @@ describe('RoleListEditModal', () => {
await waitFor(() => {
const permCalls = mockGet.mock.calls.filter(([c]) =>
c.endpoint.includes(`/api/v1/security/roles/${roleB.id}/permissions/`),
c.endpoint.includes('/api/v1/security/permissions-resources/'),
);
expect(permCalls.length).toBeGreaterThan(0);
// Should request role B's IDs, not role A's
const query = rison.decode(
permCalls[0][0].endpoint.split('?q=')[1],
) as Record<string, unknown>;
const filters = query.filters as Array<{
col: string;
opr: string;
value: number[];
}>;
expect(filters[0].value).toEqual(roleB.permission_ids);
});
unmount();
mockGet.mockReset();
});
test('fetches permissions via role endpoint and groups by id for hydration', async () => {
test('fetches permissions and groups by id for hydration', async () => {
const mockGet = SupersetClient.get as jest.Mock;
mockGet.mockResolvedValue({
json: {
@@ -451,11 +442,8 @@ describe('RoleListEditModal', () => {
expect(mockGet).toHaveBeenCalled();
});
// Permissions should be fetched via the role's permissions endpoint (no ID list in URL)
const permissionCall = mockGet.mock.calls.find(([call]) =>
call.endpoint.includes(
`/api/v1/security/roles/${mockRole.id}/permissions/`,
),
call.endpoint.includes('/api/v1/security/permissions-resources/'),
)?.[0];
const groupsCall = mockGet.mock.calls.find(([call]) =>
call.endpoint.includes('/api/v1/security/groups/'),
@@ -467,17 +455,26 @@ describe('RoleListEditModal', () => {
throw new Error('Expected hydration calls to be defined');
}
// Permission endpoint has no query params (role ID is in the path)
expect(permissionCall.endpoint).toBe(
`/api/v1/security/roles/${mockRole.id}/permissions/`,
);
// Groups still use the id-in filter
const permissionQuery = permissionCall.endpoint.match(/\?q=(.+)/);
const groupsQuery = groupsCall.endpoint.match(/\?q=(.+)/);
expect(permissionQuery).toBeTruthy();
expect(groupsQuery).toBeTruthy();
if (!groupsQuery) {
throw new Error('Expected groups query params to be present');
if (!permissionQuery || !groupsQuery) {
throw new Error('Expected query params to be present');
}
expect(rison.decode(permissionQuery[1])).toEqual({
page_size: 100,
page: 0,
filters: [
{
col: 'id',
opr: 'in',
value: mockRole.permission_ids,
},
],
});
expect(rison.decode(groupsQuery[1])).toEqual({
page_size: 100,
page: 0,

View File

@@ -30,11 +30,9 @@ import {
import {
BaseModalProps,
RoleForm,
RolePermissions,
SelectOption,
} from 'src/features/roles/types';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { SupersetClient } from '@superset-ui/core';
import { fetchPaginatedData } from 'src/utils/fetchOptions';
import { type UserObject } from 'src/pages/UsersList/types';
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
@@ -51,7 +49,6 @@ import {
updateRoleUsers,
formatPermissionLabel,
} from './utils';
import { getUserDisplayLabel } from 'src/features/users/utils';
export interface RoleListEditModalProps extends BaseModalProps {
role: RoleObject;
@@ -165,38 +162,34 @@ function RoleListEditModal({
return;
}
let cancelled = false;
setLoadingRolePermissions(true);
permissionFetchSucceeded.current = false;
const filters = [{ col: 'id', opr: 'in', value: stablePermissionIds }];
SupersetClient.get({
endpoint: `/api/v1/security/roles/${id}/permissions/`,
})
.then(response => {
if (cancelled) return;
fetchPaginatedData({
endpoint: `/api/v1/security/permissions-resources/`,
pageSize: 100,
setData: (data: SelectOption[]) => {
permissionFetchSucceeded.current = true;
const result: RolePermissions[] = response.json.result ?? [];
setRolePermissions(
result.map(p => ({
value: p.id,
label: formatPermissionLabel(p.permission_name, p.view_menu_name),
})),
);
})
.catch(() => {
if (!cancelled) {
addDangerToast(t('There was an error loading permissions.'));
}
})
.finally(() => {
if (!cancelled) {
setLoadingRolePermissions(false);
}
});
return () => {
cancelled = true;
};
setRolePermissions(data);
},
filters,
setLoadingState: (loading: boolean) => setLoadingRolePermissions(loading),
loadingKey: 'rolePermissions',
addDangerToast,
errorMessage: t('There was an error loading permissions.'),
mapResult: (permission: {
id: number;
permission: { name: string };
view_menu: { name: string };
}) => ({
value: permission.id,
label: formatPermissionLabel(
permission.permission.name,
permission.view_menu.name,
),
}),
});
}, [addDangerToast, id, stablePermissionIds]);
useEffect(() => {
@@ -233,7 +226,7 @@ function RoleListEditModal({
if (!loadingRoleUsers && formRef.current) {
const userOptions = roleUsers.map(user => ({
value: user.id,
label: getUserDisplayLabel(user),
label: user.username,
}));
formRef.current.setFieldsValue({
roleUsers: userOptions,
@@ -322,7 +315,7 @@ function RoleListEditModal({
roleUsers:
roleUsers?.map(user => ({
value: user.id,
label: getUserDisplayLabel(user),
label: user.username,
})) || [],
roleGroups: group_ids.map(groupId => ({
value: groupId,

View File

@@ -44,15 +44,6 @@ export const deleteUser = async (userId: number) =>
endpoint: `/api/v1/security/users/${userId}`,
});
export const getUserDisplayLabel = (user: {
first_name?: string;
last_name?: string;
username?: string;
}): string =>
[user.first_name, user.last_name].filter(Boolean).join(' ') ||
user.username ||
t('N/A');
export const atLeastOneRoleOrGroup =
(fieldToCheck: 'roles' | 'groups') =>
({

View File

@@ -31,7 +31,6 @@ import 'dayjs/locale/pt';
import 'dayjs/locale/pt-br';
import 'dayjs/locale/ru';
import 'dayjs/locale/ko';
import 'dayjs/locale/cs';
import 'dayjs/locale/sk';
import 'dayjs/locale/sl';
import 'dayjs/locale/nl';
@@ -51,7 +50,6 @@ export const LOCALE_MAPPING = {
pt_BR: () => import('antd/locale/pt_BR'),
ru: () => import('antd/locale/ru_RU'),
ko: () => import('antd/locale/ko_KR'),
cs: () => import('antd/locale/cs_CZ'),
sk: () => import('antd/locale/sk_SK'),
sl: () => import('antd/locale/sl_SI'),
nl: () => import('antd/locale/nl_NL'),

View File

@@ -96,21 +96,16 @@ export const fetchPaginatedData = async ({
}
const totalPages = Math.ceil(totalItems / pageSize);
const concurrencyLimit = 5;
const allResults = [...firstPageResults];
for (let batch = 1; batch < totalPages; batch += concurrencyLimit) {
const batchEnd = Math.min(batch + concurrencyLimit, totalPages);
// eslint-disable-next-line no-await-in-loop
const batchResults = await Promise.all(
Array.from({ length: batchEnd - batch }, (_, i) =>
fetchPage(batch + i),
),
);
allResults.push(...batchResults.flatMap(res => res.results));
}
const requests = Array.from({ length: totalPages - 1 }, (_, i) =>
fetchPage(i + 1),
);
const remainingResults = await Promise.all(requests);
setData(allResults);
setData([
...firstPageResults,
...remainingResults.flatMap(res => res.results),
]);
} catch (err) {
addDangerToast(t(errorMessage));
} finally {

View File

@@ -15,7 +15,7 @@
"jsonwebtoken": "^9.0.3",
"lodash": "^4.18.1",
"winston": "^3.19.0",
"ws": "^8.21.0"
"ws": "^8.20.1"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
@@ -6428,9 +6428,9 @@
"dev": true
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -11207,9 +11207,9 @@
"dev": true
},
"ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"requires": {}
},
"y18n": {

View File

@@ -23,7 +23,7 @@
"jsonwebtoken": "^9.0.3",
"lodash": "^4.18.1",
"winston": "^3.19.0",
"ws": "^8.21.0"
"ws": "^8.20.1"
},
"devDependencies": {
"@eslint/js": "^9.25.1",

View File

@@ -185,16 +185,6 @@ class ExportDashboardsCommand(ExportModelsCommand):
# Add theme UUID for proper cross-system imports
payload["theme_uuid"] = str(model.theme.uuid) if model.theme else None
# Include role assignments (DASHBOARD_RBAC). Role IDs are
# environment-local, so emit names — the import side resolves them
# back to roles in the destination environment. The key is omitted
# entirely when there are no role restrictions; older import code
# treats "missing" as "no restriction" and an empty list could
# confuse importers that distinguish the two states.
role_names = sorted(role.name for role in (model.roles or []))
if role_names:
payload["roles"] = role_names
payload["version"] = EXPORT_VERSION
# Check if the TAGGING_SYSTEM feature is enabled

View File

@@ -281,11 +281,6 @@ def import_dashboard( # noqa: C901
# Note: theme_id handling moved to higher level import logic
# Pop roles before handing config to import_from_dict — it's a
# relationship, not a column, and the standard SQLAlchemy import path
# doesn't resolve role *names* into role objects. We re-attach below.
role_names = config.pop("roles", None)
for key, new_name in JSON_KEYS.items():
if config.get(key) is not None:
value = config.pop(key)
@@ -301,25 +296,4 @@ def import_dashboard( # noqa: C901
if (user := get_user()) and user not in dashboard.owners:
dashboard.owners.append(user)
# Re-attach DASHBOARD_RBAC role assignments by name. Role IDs are
# environment-local; names are how exports cross environments. Roles
# that don't exist in the destination are skipped with a warning
# rather than failing the import — admins may need to create them
# before the access restriction takes effect.
if isinstance(role_names, list) and role_names:
resolved_roles = []
for name in role_names:
role = security_manager.find_role(name)
if role is not None:
resolved_roles.append(role)
else:
logger.warning(
"Dashboard '%s': role %r referenced in export does not "
"exist in this environment; access restriction will not "
"be applied for that role",
dashboard.dashboard_title,
name,
)
dashboard.roles = resolved_roles
return dashboard

View File

@@ -22,7 +22,6 @@ from urllib import request
import pandas as pd
from flask import current_app as app
from pandas.errors import OutOfBoundsDatetime
from sqlalchemy import BigInteger, Boolean, Date, DateTime, Float, String, Text
from sqlalchemy.exc import MultipleResultsFound
from sqlalchemy.sql.visitors import VisitableType
@@ -203,39 +202,6 @@ def import_dataset( # noqa: C901
return dataset
def _convert_temporal_columns(df: pd.DataFrame, dtype: dict[str, Any]) -> None:
"""Convert Date/DateTime columns in-place, coercing only out-of-bounds values."""
for column_name, sqla_type in dtype.items():
if isinstance(sqla_type, (Date, DateTime)):
try:
df[column_name] = pd.to_datetime(df[column_name])
except OutOfBoundsDatetime:
# Row-level fallback: coerce only OOB values; re-raise for malformed
# strings. Whole-column errors="coerce" would silently swallow
# malformed values that happen to share a column with an OOB date.
original = df[column_name].copy()
result = []
for val in original:
if pd.isna(val):
result.append(pd.NaT)
continue
try:
result.append(pd.to_datetime(val))
except OutOfBoundsDatetime:
result.append(pd.NaT)
# Other exceptions (e.g. malformed strings) propagate
converted = pd.Series(result, index=original.index)
n_coerced = int(converted.isna().sum() - original.isna().sum())
if n_coerced > 0:
logger.warning(
"Coerced %d out-of-bounds datetime value(s) "
"in column '%s' to NaT",
n_coerced,
column_name,
)
df[column_name] = converted
def load_data(data_uri: str, dataset: SqlaTable, database: Database) -> None:
"""
Load data from a data URI into a dataset.
@@ -256,7 +222,10 @@ def load_data(data_uri: str, dataset: SqlaTable, database: Database) -> None:
df = pd.read_csv(data, encoding="utf-8")
dtype = get_dtype(df, dataset)
_convert_temporal_columns(df, dtype)
# convert temporal columns
for column_name, sqla_type in dtype.items():
if isinstance(sqla_type, (Date, DateTime)):
df[column_name] = pd.to_datetime(df[column_name])
# reuse session when loading data if possible, to make import atomic
if database.sqlalchemy_uri == app.config.get("SQLALCHEMY_DATABASE_URI"):

View File

@@ -30,7 +30,6 @@ from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.extensions import feature_flag_manager
from superset.models.core import Database
from superset.models.dashboard import dashboard_slices
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES
from superset.tags.models import Tag, TaggedObject
from superset.utils import json
from superset.utils.core import check_is_safe_zip
@@ -401,49 +400,3 @@ def get_resource_mappings_batched(
mapping.update({str(x.uuid): value_func(x) for x in batch})
offset += batch_size
return mapping
def find_existing_for_import(model_cls: type[Any], uuid: str) -> Any | None:
"""Look up an existing row by UUID for an import, including soft-deleted matches.
Bypasses the soft-delete visibility filter so a soft-deleted row with
the matching UUID is returned, not hidden. Side-effect-free: returns
the row as-is whether it's live or soft-deleted (or ``None`` if no
row exists). The caller is responsible for deciding what to do with
a soft-deleted match — typically calling
:func:`clear_soft_deleted_for_import` to remove it before re-import,
but only after the caller has validated overwrite/permission decisions.
Splitting the lookup from the destructive cleanup keeps the
destructive action explicit at the call site, so a future change
that adds a permission check on the overwrite path doesn't
silently leave a "duck around it via soft-delete" backdoor.
"""
return (
db.session.query(model_cls)
.execution_options(**{SKIP_VISIBILITY_FILTER_CLASSES: {model_cls}})
.filter_by(uuid=uuid)
.first()
)
def clear_soft_deleted_for_import(existing: Any) -> None:
"""Hard-delete a soft-deleted row to free its UUID for re-import.
Uses ``db.session.delete()`` rather than a raw Core ``DELETE`` so
the ORM ``after_delete`` event listeners fire. Cleanup that depends
on those listeners would otherwise be skipped — notably tag rows in
``tagged_object`` (cleaned up by ``ObjectUpdater.after_delete`` in
``superset/tags/core.py``; the table's ``object_id`` is a plain
integer, not a foreign key, so the database cannot cascade them)
and dataset permission-view rows (cleaned up by
``SqlaTable.after_delete`` in ``superset/connectors/sqla/models.py``).
Caller contract: ``existing`` must be a soft-deleted row returned
from :func:`find_existing_for_import`. Callers should run their
overwrite / permission validation *before* invoking this so the
destructive action only happens once the import path is committed
to proceeding.
"""
db.session.delete(existing)
db.session.flush()

View File

@@ -1,98 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Base class shared by all soft-delete restore commands."""
from functools import partial
from typing import Any, ClassVar, Generic, TypeVar
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.exceptions import SupersetSecurityException
from superset.models.helpers import SoftDeleteMixin
from superset.utils.decorators import on_error, transaction
T = TypeVar("T", bound=SoftDeleteMixin)
class BaseRestoreCommand(BaseCommand, Generic[T]):
"""Base class for soft-delete restore commands.
Subclasses provide the entity-specific bindings as class variables —
no method override required:
- ``dao``: the DAO class (e.g. ``ChartDAO``)
- ``not_found_exc``: raised when the row doesn't exist OR isn't
soft-deleted
- ``forbidden_exc``: raised when the caller doesn't have ownership
- ``restore_failed_exc``: re-raised by the transactional wrapper
when an underlying SQLAlchemy error aborts the commit
The transactional wrapper is applied by this class's ``run()``
using ``restore_failed_exc`` as the rethrow type, so each subclass
just declares the four ClassVars and is done. There is no
subclass-managed decorator contract — earlier iterations of this
PR required subclasses to override ``run()`` purely to add a
``@transaction`` decorator, which was fragile (every new entity
rollout had to remember).
The model returned from ``validate()`` is the soft-deleted row,
type-narrowed via ``Generic[T]``. ``run()`` calls ``model.restore()``
on it (the method comes from ``SoftDeleteMixin``).
"""
dao: ClassVar[Any]
not_found_exc: ClassVar[type[Exception]]
forbidden_exc: ClassVar[type[Exception]]
restore_failed_exc: ClassVar[type[Exception]]
def __init__(self, model_uuid: str) -> None:
self._model_uuid = model_uuid
def run(self) -> None:
# Build the transactional wrapper at call time so ``on_error`` can
# reference ``self.restore_failed_exc`` — a per-subclass ClassVar
# that isn't available when this method is defined on the base.
@transaction(on_error=partial(on_error, reraise=self.restore_failed_exc))
def _perform() -> None:
model = self.validate()
model.restore()
_perform()
def validate(self) -> T: # type: ignore[override]
# ``skip_visibility_filter=True`` is the *only* bypass — the
# entity's RBAC ``base_filter`` stays in effect, matching the
# behavior of ``find_by_ids`` on the existing delete paths.
# Restore should not see rows the user cannot see in the live
# UI; ownership is then verified by ``raise_for_ownership``.
model = self.dao.find_by_id(
self._model_uuid,
id_column="uuid",
skip_visibility_filter=True,
)
if model is None:
raise self.not_found_exc(f"No row with uuid={self._model_uuid!r}")
if model.deleted_at is None:
raise self.not_found_exc(
f"Row with uuid={self._model_uuid!r} is not soft-deleted; "
"nothing to restore"
)
try:
security_manager.raise_for_ownership(model)
except SupersetSecurityException as ex:
raise self.forbidden_exc() from ex
return model

View File

@@ -51,7 +51,7 @@ from sqlalchemy.orm.query import Query
from superset.advanced_data_type.plugins.internet_address import internet_address
from superset.advanced_data_type.plugins.internet_port import internet_port
from superset.advanced_data_type.types import AdvancedDataType
from superset.constants import CHANGE_ME_GUEST_TOKEN_JWT_SECRET, CHANGE_ME_SECRET_KEY
from superset.constants import CHANGE_ME_SECRET_KEY
from superset.jinja_context import BaseTemplateProcessor
from superset.key_value.types import JsonKeyValueCodec
from superset.stats_logger import DummyStatsLogger
@@ -2354,7 +2354,7 @@ GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = {
# Embedded config options
GUEST_ROLE_NAME = "Public"
GUEST_TOKEN_JWT_SECRET = CHANGE_ME_GUEST_TOKEN_JWT_SECRET
GUEST_TOKEN_JWT_SECRET = "test-guest-secret-change-me" # noqa: S105
GUEST_TOKEN_JWT_ALGO = "HS256" # noqa: S105
GUEST_TOKEN_HEADER_NAME = "X-GuestToken" # noqa: S105
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutes

View File

@@ -28,7 +28,6 @@ NULL_STRING = "<NULL>"
EMPTY_STRING = "<empty string>"
CHANGE_ME_SECRET_KEY = "CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET" # noqa: S105
CHANGE_ME_GUEST_TOKEN_JWT_SECRET = "test-guest-secret-change-me" # noqa: S105
# UUID for the examples database
EXAMPLES_DB_UUID = "a2dc77af-e654-49bb-b321-40f6b559a1ee"
@@ -175,7 +174,6 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
"put_filters": "write",
"put_colors": "write",
"sync_permissions": "write",
"restore": "write",
}
EXTRA_FORM_DATA_APPEND_KEYS = {

View File

@@ -48,7 +48,6 @@ from superset.daos.exceptions import (
DAOFindFailedError,
)
from superset.extensions import db
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES, SoftDeleteMixin
T = TypeVar("T", bound=CoreModel)
@@ -61,7 +60,6 @@ class ColumnOperatorEnum(str, Enum):
ne = "ne"
sw = "sw"
ew = "ew"
ct = "ct"
in_ = "in"
nin = "nin"
gt = "gt"
@@ -86,12 +84,11 @@ operator_map: Dict[ColumnOperatorEnum, Any] = {
ColumnOperatorEnum.ne: lambda col, val: col != val,
ColumnOperatorEnum.sw: lambda col, val: col.like(f"{val}%"),
ColumnOperatorEnum.ew: lambda col, val: col.like(f"%{val}"),
ColumnOperatorEnum.ct: lambda col, val: col.ilike(f"%{val}%"),
ColumnOperatorEnum.in_: lambda col, val: col.in_(
val if isinstance(val, (list, tuple)) else [val]
),
ColumnOperatorEnum.nin: lambda col, val: (
~col.in_(val if isinstance(val, (list, tuple)) else [val])
ColumnOperatorEnum.nin: lambda col, val: ~col.in_(
val if isinstance(val, (list, tuple)) else [val]
),
ColumnOperatorEnum.gt: lambda col, val: col > val,
ColumnOperatorEnum.gte: lambda col, val: col >= val,
@@ -110,7 +107,6 @@ TYPE_OPERATOR_MAP = {
ColumnOperatorEnum.ne,
ColumnOperatorEnum.sw,
ColumnOperatorEnum.ew,
ColumnOperatorEnum.ct,
ColumnOperatorEnum.in_,
ColumnOperatorEnum.nin,
ColumnOperatorEnum.like,
@@ -185,17 +181,11 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
cls,
model_id_or_uuid: str,
skip_base_filter: bool = False,
*,
skip_visibility_filter: bool = False,
) -> T | None:
"""
Find a model by id or uuid, if defined applies `base_filter`
"""
query = db.session.query(cls.model_cls)
if skip_visibility_filter:
query = query.execution_options(
**{SKIP_VISIBILITY_FILTER_CLASSES: {cls.model_cls}}
)
if cls.base_filter and not skip_base_filter:
data_model = SQLAInterface(cls.model_cls, db.session)
query = cls.base_filter( # pylint: disable=not-callable
@@ -259,8 +249,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
value: str | int,
skip_base_filter: bool = False,
query_options: list[Any] | None = None,
*,
skip_visibility_filter: bool = False,
) -> T | None:
"""
Private method to find a model by any column value.
@@ -269,7 +257,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
column_name: Name of the column to search by
value: Value to search for
skip_base_filter: Whether to skip base filtering
skip_visibility_filter: Whether to skip the soft-delete visibility filter
query_options: SQLAlchemy query options (e.g., joinedload,
subqueryload) to apply to the query for eager loading
@@ -277,10 +264,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
Model instance or None if not found
"""
query = db.session.query(cls.model_cls)
if skip_visibility_filter:
query = query.execution_options(
**{SKIP_VISIBILITY_FILTER_CLASSES: {cls.model_cls}}
)
query = cls._apply_base_filter(query, skip_base_filter)
if query_options:
@@ -307,8 +290,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
skip_base_filter: bool = False,
id_column: str | None = None,
query_options: list[Any] | None = None,
*,
skip_visibility_filter: bool = False,
) -> T | None:
"""
Find a model by ID using specified or default ID column.
@@ -319,20 +300,12 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
id_column: Column name to use (defaults to cls.id_column_name)
query_options: SQLAlchemy query options (e.g., joinedload,
subqueryload) to apply to the query for eager loading
skip_visibility_filter: Keyword-only. Whether to skip the
soft-delete visibility filter
Returns:
Model instance or None if not found
"""
column = id_column or cls.id_column_name
return cls._find_by_column(
column,
model_id,
skip_base_filter,
query_options,
skip_visibility_filter=skip_visibility_filter,
)
return cls._find_by_column(column, model_id, skip_base_filter, query_options)
@classmethod
def find_by_ids(
@@ -340,8 +313,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
model_ids: Sequence[str | int],
skip_base_filter: bool = False,
id_column: str | None = None,
*,
skip_visibility_filter: bool = False,
) -> list[T]:
"""
Find a List of models by a list of ids, if defined applies `base_filter`
@@ -350,8 +321,6 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
:param skip_base_filter: If true, skip applying the base filter
:param id_column: Optional column name to use for ID lookup
(defaults to id_column_name)
:param skip_visibility_filter: Keyword-only. If true, skip the
soft-delete visibility filter so soft-deleted rows are returned
"""
column = id_column or cls.id_column_name
id_col = getattr(cls.model_cls, column, None)
@@ -378,12 +347,7 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
if not converted_ids:
return []
query = db.session.query(cls.model_cls)
if skip_visibility_filter:
query = query.execution_options(
**{SKIP_VISIBILITY_FILTER_CLASSES: {cls.model_cls}}
)
query = query.filter(id_col.in_(converted_ids))
query = db.session.query(cls.model_cls).filter(id_col.in_(converted_ids))
query = cls._apply_base_filter(query, skip_base_filter)
try:
@@ -465,51 +429,25 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
return item # type: ignore
@classmethod
def soft_delete(cls, items: list[T]) -> None:
"""Mark items as soft-deleted by setting ``deleted_at``.
Only valid for models that include ``SoftDeleteMixin``.
:param items: The items to soft-delete
def delete(cls, items: list[T]) -> None:
"""
for item in items:
item.soft_delete()
Delete the specified items including their associated relationships.
@classmethod
def hard_delete(cls, items: list[T]) -> None:
"""Permanently remove rows from the database.
Note that bulk deletion via `delete` is not invoked in the base class as this
does not dispatch the ORM `after_delete` event which may be required to augment
additional records loosely defined via implicit relationships. Instead ORM
objects are deleted one-by-one via `Session.delete`.
Note that bulk deletion via ``delete`` is not invoked in the base
class as this does not dispatch the ORM ``after_delete`` event which
may be required to augment additional records loosely defined via
implicit relationships. Instead ORM objects are deleted one-by-one
via ``Session.delete``.
Subclasses may invoke bulk deletion but are responsible for
instrumenting any post-deletion logic.
Subclasses may invoke bulk deletion but are responsible for instrumenting any
post-deletion logic.
:param items: The items to delete
:see: https://docs.sqlalchemy.org/en/latest/orm/queryguide/dml.html
"""
for item in items:
db.session.delete(item)
@classmethod
def delete(cls, items: list[T]) -> None:
"""Route to soft or hard delete based on whether the model supports
soft delete.
For models that include ``SoftDeleteMixin``, this calls
``soft_delete()``. For all other models, this calls ``hard_delete()``
(the original behaviour).
:param items: The items to delete
"""
if cls.model_cls is not None and issubclass(cls.model_cls, SoftDeleteMixin):
cls.soft_delete(items)
else:
cls.hard_delete(items)
@classmethod
def query(cls, query: Query) -> list[T]:
"""

View File

@@ -30,7 +30,6 @@ from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.extensions import db
from superset.models.core import Database, DatabaseUserOAuth2Tokens
from superset.models.dashboard import Dashboard
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES
from superset.models.slice import Slice
from superset.models.sql_lab import TabState
from superset.utils.core import DatasourceType
@@ -72,8 +71,6 @@ class DatabaseDAO(BaseDAO[Database]):
skip_base_filter: bool = False,
id_column: str | None = None,
query_options: list[Any] | None = None,
*,
skip_visibility_filter: bool = False,
) -> Database | None:
"""
Find a database by id, eagerly loading the SSH tunnel relationship.
@@ -82,10 +79,6 @@ class DatabaseDAO(BaseDAO[Database]):
if query_options:
all_options.extend(query_options)
query = db.session.query(cls.model_cls).options(*all_options)
if skip_visibility_filter:
query = query.execution_options(
**{SKIP_VISIBILITY_FILTER_CLASSES: {cls.model_cls}}
)
query = cls._apply_base_filter(query, skip_base_filter)
column_name = id_column or cls.id_column_name

View File

@@ -1,28 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""DAO for FAB Role model."""
from __future__ import annotations
from flask_appbuilder.security.sqla.models import Role
from superset.daos.base import BaseDAO
class RoleDAO(BaseDAO[Role]):
"""DAO for FAB Role model. Provides basic CRUD via BaseDAO."""

View File

@@ -519,7 +519,6 @@ class ImportV1DashboardSchema(Schema):
tags = fields.List(fields.String(), allow_none=True)
theme_uuid = fields.UUID(allow_none=True)
theme_id = fields.Integer(allow_none=True)
roles = fields.List(fields.String(), allow_none=True)
class EmbeddedDashboardConfigSchema(Schema):

View File

@@ -37,7 +37,7 @@ from flask_compress import Compress
from flask_session import Session
from werkzeug.middleware.proxy_fix import ProxyFix
from superset.constants import CHANGE_ME_GUEST_TOKEN_JWT_SECRET, CHANGE_ME_SECRET_KEY
from superset.constants import CHANGE_ME_SECRET_KEY
from superset.databases.utils import make_url_safe
from superset.extensions import (
_event_logger,
@@ -634,17 +634,12 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
self.init_all_dependencies_and_extensions()
@staticmethod
def _log_config_warning(message: str) -> None:
top_banner = 80 * "-" + "\n" + 36 * " " + "WARNING\n" + 80 * "-"
bottom_banner = 80 * "-" + "\n" + 80 * "-"
logger.warning(top_banner)
logger.warning(message)
logger.warning(bottom_banner)
def check_secret_key(self) -> None:
if self.config["SECRET_KEY"] == CHANGE_ME_SECRET_KEY:
warning = (
def log_default_secret_key_warning() -> None:
top_banner = 80 * "-" + "\n" + 36 * " " + "WARNING\n" + 80 * "-"
bottom_banner = 80 * "-" + "\n" + 80 * "-"
logger.warning(top_banner)
logger.warning(
"A Default SECRET_KEY was detected, please use superset_config.py "
"to override it.\n"
"Use a strong complex alphanumeric string and use a tool to help"
@@ -653,44 +648,21 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
"For more info, see: https://superset.apache.org/docs/"
"configuration/configuring-superset#specifying-a-secret_key"
)
logger.warning(bottom_banner)
if self.config["SECRET_KEY"] == CHANGE_ME_SECRET_KEY:
if (
self.superset_app.debug
or self.superset_app.config["TESTING"]
or is_test()
):
logger.warning("Debug mode identified with default secret key")
self._log_config_warning(warning)
log_default_secret_key_warning()
return
self._log_config_warning(warning)
log_default_secret_key_warning()
logger.error("Refusing to start due to insecure SECRET_KEY")
sys.exit(1)
def check_guest_token_secret(self) -> None:
"""Refuse to start with default guest JWT secret when embedding is enabled."""
if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"):
return
if (
self.config.get("GUEST_TOKEN_JWT_SECRET")
!= CHANGE_ME_GUEST_TOKEN_JWT_SECRET
):
return
self._log_config_warning(
"EMBEDDED_SUPERSET is enabled but GUEST_TOKEN_JWT_SECRET has not "
"been changed from its default value.\n"
"The default value is publicly known and must be replaced before "
"running in production.\n"
"Set a strong random value in superset_config.py:\n"
" GUEST_TOKEN_JWT_SECRET = "
"'<output of: openssl rand -base64 42>'"
)
if self.superset_app.debug or self.superset_app.config["TESTING"] or is_test():
return
logger.error(
"Refusing to start: insecure GUEST_TOKEN_JWT_SECRET "
"with EMBEDDED_SUPERSET enabled"
)
sys.exit(1)
def configure_session(self) -> None:
if self.config["SESSION_SERVER_SIDE"]:
Session(self.superset_app)
@@ -775,7 +747,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
# Configuration of feature_flags must be done first to allow init features
# conditionally
self.configure_feature_flags()
self.check_guest_token_secret()
self.configure_db_encrypt()
self.setup_db()
@@ -796,13 +767,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
with self.superset_app.app_context():
self.init_app_in_ctx()
# Registered outside ``init_app_in_ctx`` because the SQLAlchemy
# event hook attaches to the ``Session`` *class* (a process-wide
# global), not to a Session instance — it has no dependency on
# the Flask app context. ``setup_db()`` ran earlier in
# ``init_app``, so the ``Session`` import has already been
# initialised by the time we get here.
self.setup_soft_delete_listener()
self.post_init()
def set_db_default_isolation(self) -> None:
@@ -985,23 +949,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
migrate.init_app(self.superset_app, db=db, directory=APP_DIR + "/migrations")
def setup_soft_delete_listener(self) -> None:
"""Register the global soft-delete filter on the SQLAlchemy Session.
Must be called after ``setup_db()`` so the Session class is
available. Uses the ``do_orm_execute`` + ``with_loader_criteria``
pattern recommended by SQLAlchemy maintainer Mike Bayer for
soft deletion in SQLAlchemy 1.4+:
https://github.com/sqlalchemy/sqlalchemy/issues/7973#issuecomment-1112561295
"""
from sqlalchemy import event
from sqlalchemy.orm import Session
from superset.models.helpers import _add_soft_delete_filter
if not event.contains(Session, "do_orm_execute", _add_soft_delete_filter):
event.listen(Session, "do_orm_execute", _add_soft_delete_filter)
def configure_wtf(self) -> None:
if self.config["WTF_CSRF_ENABLED"]:
csrf.init_app(self.superset_app)

View File

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

View File

@@ -1,302 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Pydantic schemas for action-log MCP tools."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Annotated, Any, Literal
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_serializer,
model_validator,
PositiveInt,
)
from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from superset.mcp_service.system.schemas import PaginationInfo
from superset.mcp_service.utils import sanitize_for_llm_context
from superset.mcp_service.utils.schema_utils import (
parse_json_or_list,
parse_json_or_model_list,
)
from superset.utils import json as json_utils
DEFAULT_LOG_COLUMNS: list[str] = ["id", "action", "user_id", "dttm"]
ALL_LOG_COLUMNS: list[str] = [
"id",
"action",
"user_id",
"dttm",
"dashboard_id",
"slice_id",
"json",
]
LOG_SORTABLE_COLUMNS: list[str] = ["id", "dttm"]
class ActionLogFilter(ColumnOperator):
"""Filter object for action-log listing.
col: Column to filter on.
opr: Operator to use.
value: Value to filter by.
"""
col: Literal["action", "user_id", "dashboard_id", "slice_id", "dttm"] = Field(
...,
description="Column to filter on.",
)
opr: ColumnOperatorEnum = Field(..., description="Operator to use.")
value: (
str | int | float | bool | datetime | list[str | int | float | bool | datetime]
) = Field(..., description="Value to filter by")
@model_validator(mode="after")
def normalize_dttm_value(self) -> "ActionLogFilter":
"""Normalize string dttm values to datetime to avoid VARCHAR bind mismatch.
Pydantic's left-to-right union matching keeps ISO strings as str when
str appears before datetime in the union. This validator parses them so
the DAO always receives a typed datetime for TIMESTAMP column comparisons.
Both scalar and list values are normalized so dttm IN (...) is also safe.
Replaces a trailing 'Z' with '+00:00' before parsing because
datetime.fromisoformat does not accept the 'Z' suffix on Python < 3.11.
"""
def _parse(val: str) -> datetime | str:
try:
s = val[:-1] + "+00:00" if val.endswith("Z") else val
parsed = datetime.fromisoformat(s)
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
except ValueError:
return val
if self.col == "dttm":
if isinstance(self.value, str):
self.value = _parse(self.value)
elif isinstance(self.value, list):
self.value = [
_parse(v) if isinstance(v, str) else v for v in self.value
]
return self
class ActionLogInfo(BaseModel):
id: int | None = Field(None, description="Log entry ID")
action: str | None = Field(None, description="Action name")
user_id: int | None = Field(
None, description="ID of the user who performed the action"
)
dttm: str | datetime | None = Field(None, description="Timestamp of the action")
dashboard_id: int | None = Field(None, description="Associated dashboard ID")
slice_id: int | None = Field(None, description="Associated chart/slice ID")
json: str | None = Field(
None,
description="JSON payload (user-controlled, wrapped in UNTRUSTED-CONTENT)",
)
model_config = ConfigDict(
from_attributes=True,
ser_json_timedelta="iso8601",
populate_by_name=True,
)
def model_post_init(self, __context: Any) -> None:
if isinstance(self.dttm, datetime) and self.dttm.tzinfo is None:
object.__setattr__(self, "dttm", self.dttm.replace(tzinfo=timezone.utc))
@model_serializer(mode="wrap")
def _filter_fields_by_context(self, serializer: Any, info: Any) -> dict[str, Any]:
data = serializer(self)
if info.context and isinstance(info.context, dict):
select_columns = info.context.get("select_columns")
if select_columns:
requested_fields = set(select_columns)
return {k: v for k, v in data.items() if k in requested_fields}
return data
class ActionLogList(BaseModel):
action_logs: list[ActionLogInfo]
count: int
total_count: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
columns_requested: list[str] = Field(default_factory=list)
columns_loaded: list[str] = Field(default_factory=list)
columns_available: list[str] = Field(default_factory=list)
sortable_columns: list[str] = Field(default_factory=list)
filters_applied: list[ActionLogFilter] = Field(default_factory=list)
pagination: PaginationInfo | None = None
timestamp: datetime | None = None
model_config = ConfigDict(ser_json_timedelta="iso8601")
class ListActionLogsRequest(BaseModel):
"""Request schema for list_action_logs."""
filters: Annotated[
list[ActionLogFilter],
Field(
default_factory=list,
description=(
"List of filter objects (col, opr, value). "
"Filter columns: action, user_id, dashboard_id, slice_id, dttm. "
"Cannot be used with 'search'."
),
),
]
select_columns: Annotated[
list[str],
Field(
default_factory=list,
description="Columns to return. Defaults to common columns.",
),
]
search: Annotated[
str | None,
Field(
default=None,
description=(
"Text search string matched against action. "
"Cannot be used together with 'filters'."
),
),
]
order_column: Annotated[
str | None,
Field(default=None, description="Column to sort by (default: dttm)"),
]
order_direction: Annotated[
Literal["asc", "desc"],
Field(default="desc", description="Sort direction ('asc' or 'desc')"),
]
page: Annotated[
PositiveInt,
Field(default=1, description="Page number (1-based)"),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Items per page (max {MAX_PAGE_SIZE})",
),
]
@field_validator("filters", mode="before")
@classmethod
def parse_filters(cls, v: Any) -> list[ActionLogFilter]:
return parse_json_or_model_list(v, ActionLogFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
def parse_columns(cls, v: Any) -> list[str]:
return parse_json_or_list(v, "select_columns")
@model_validator(mode="after")
def validate_search_and_filters(self) -> "ListActionLogsRequest":
if self.search and self.filters:
raise ValueError(
"Cannot use both 'search' and 'filters' simultaneously. "
"Use 'search' for text matching on action, or 'filters' for "
"column-based filtering, but not both."
)
return self
class ActionLogError(BaseModel):
error: str = Field(..., description="Error message")
error_type: str = Field(..., description="Error type")
timestamp: str | datetime | None = Field(None, description="Error timestamp")
model_config = ConfigDict(ser_json_timedelta="iso8601")
@classmethod
def create(cls, error: str, error_type: str) -> "ActionLogError":
return cls(
error=error,
error_type=error_type,
timestamp=datetime.now(timezone.utc),
)
class GetActionLogInfoRequest(BaseModel):
"""Request schema for get_action_log_info (ID-only lookup)."""
identifier: Annotated[
int,
Field(description="Log entry ID (integer)"),
]
def _sanitize_log_json(raw: Any) -> str | None:
"""Serialize the log JSON blob to a canonical string and wrap it in
UNTRUSTED-CONTENT delimiters.
The entire JSON blob — keys and values alike — is user-controlled and must
be treated as untrusted. Wrapping the canonical JSON string (rather than
processing individual dict leaves) closes the dict-key injection gap: no
key can inject instructions because every byte of the blob is enclosed
within the trust boundary.
Falls back to wrapping the raw string when the payload is not valid JSON.
"""
if raw is None:
return None
if isinstance(raw, str):
try:
canonical = json_utils.dumps(json_utils.loads(raw))
except (ValueError, TypeError):
canonical = raw
else:
try:
canonical = json_utils.dumps(raw)
except (ValueError, TypeError):
canonical = str(raw)
return sanitize_for_llm_context(
canonical,
field_path=("json",),
excluded_field_names=frozenset(),
)
def serialize_action_log_object(log: Any) -> ActionLogInfo | None:
if not log:
return None
dttm = getattr(log, "dttm", None)
if isinstance(dttm, datetime) and dttm.tzinfo is None:
dttm = dttm.replace(tzinfo=timezone.utc)
return ActionLogInfo(
id=getattr(log, "id", None),
action=getattr(log, "action", None),
user_id=getattr(log, "user_id", None),
dttm=dttm,
dashboard_id=getattr(log, "dashboard_id", None),
slice_id=getattr(log, "slice_id", None),
json=_sanitize_log_json(getattr(log, "json", None)),
)

View File

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

View File

@@ -1,97 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Get action log info MCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.daos.log import LogDAO
from superset.extensions import event_logger
from superset.mcp_service.action_log.schemas import (
ActionLogError,
ActionLogInfo,
GetActionLogInfoRequest,
serialize_action_log_object,
)
from superset.mcp_service.mcp_core import ModelGetInfoCore
logger = logging.getLogger(__name__)
@tool(
tags=["discovery"],
class_permission_name="Log",
annotations=ToolAnnotations(
title="Get action log info",
readOnlyHint=True,
destructiveHint=False,
),
)
async def get_action_log_info(
request: GetActionLogInfoRequest,
ctx: Context,
) -> ActionLogInfo | ActionLogError:
"""Get a single action log entry by its integer ID.
Returns the action, user_id, timestamp (dttm), dashboard_id, slice_id,
and JSON payload for the specified log record.
Requires the Log permission (controlled by Superset's RBAC). Users without
that permission will receive a permission error.
Use list_action_logs to discover log IDs.
"""
await ctx.info("Retrieving action log: identifier=%s" % (request.identifier,))
try:
with event_logger.log_context(action="mcp.get_action_log_info.lookup"):
get_tool = ModelGetInfoCore(
dao_class=LogDAO,
output_schema=ActionLogInfo,
error_schema=ActionLogError,
serializer=serialize_action_log_object,
supports_slug=False,
logger=logger,
)
result = get_tool.run_tool(request.identifier)
if isinstance(result, ActionLogInfo):
await ctx.info(
"Action log retrieved: id=%s, action=%s" % (result.id, result.action)
)
else:
await ctx.warning(
"Action log retrieval failed: error_type=%s, error=%s"
% (result.error_type, result.error)
)
return result
except Exception as e:
await ctx.error(
"Action log retrieval failed: identifier=%s, error=%s, error_type=%s"
% (request.identifier, str(e), type(e).__name__)
)
return ActionLogError(
error=f"Failed to get action log info: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)

View File

@@ -1,152 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""List action logs MCP tool."""
import logging
from datetime import datetime, timedelta, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.daos.log import LogDAO
from superset.extensions import event_logger
from superset.mcp_service.action_log.schemas import (
ActionLogError,
ActionLogFilter,
ActionLogInfo,
ActionLogList,
ALL_LOG_COLUMNS,
DEFAULT_LOG_COLUMNS,
ListActionLogsRequest,
LOG_SORTABLE_COLUMNS,
serialize_action_log_object,
)
from superset.mcp_service.mcp_core import ModelListCore
logger = logging.getLogger(__name__)
_DEFAULT_LIST_ACTION_LOGS_REQUEST = ListActionLogsRequest()
@tool(
tags=["core"],
class_permission_name="Log",
annotations=ToolAnnotations(
title="List action logs",
readOnlyHint=True,
destructiveHint=False,
),
)
async def list_action_logs(
request: ListActionLogsRequest | None = None,
ctx: Context | None = None,
) -> ActionLogList | ActionLogError:
"""List Superset action logs with filtering and pagination.
Returns audit log entries recording user interactions with dashboards and
charts. Defaults to the last 7 days to avoid pulling large result sets.
Requires the Log permission (controlled by Superset's RBAC). Users without
that permission will receive a permission error.
Sortable columns for order_column: id, dttm
Filter columns: action, user_id, dashboard_id, slice_id, dttm
When no dttm filter is provided the tool automatically applies
dttm >= (now - 7 days). Add an explicit dttm filter to override.
"""
if ctx is None:
raise RuntimeError("FastMCP context is required for list_action_logs")
request = request or _DEFAULT_LIST_ACTION_LOGS_REQUEST.model_copy(deep=True)
await ctx.info(
"Listing action logs: page=%s, page_size=%s" % (request.page, request.page_size)
)
await ctx.debug(
"Action log parameters: filters=%s, order_column=%s, order_direction=%s"
% (request.filters, request.order_column, request.order_direction)
)
try:
# Inject default 7-day dttm filter unless caller already provides one
filters: list[ColumnOperator] = list(request.filters)
has_dttm_filter = any(getattr(f, "col", None) == "dttm" for f in filters)
if not has_dttm_filter:
cutoff = datetime.now(timezone.utc) - timedelta(days=7)
default_filter = ActionLogFilter(
col="dttm",
opr=ColumnOperatorEnum.gte,
value=cutoff,
)
filters = [default_filter] + filters
await ctx.debug("Applied default 7-day dttm filter: cutoff=%s" % (cutoff,))
def _serialize(obj: object, cols: list[str] | None) -> ActionLogInfo | None:
return serialize_action_log_object(obj)
list_tool = ModelListCore(
dao_class=LogDAO,
output_schema=ActionLogInfo,
item_serializer=_serialize,
filter_type=ActionLogFilter,
default_columns=DEFAULT_LOG_COLUMNS,
search_columns=["action"],
list_field_name="action_logs",
output_list_schema=ActionLogList,
all_columns=ALL_LOG_COLUMNS,
sortable_columns=LOG_SORTABLE_COLUMNS,
logger=logger,
)
with event_logger.log_context(action="mcp.list_action_logs.query"):
result = list_tool.run_tool(
filters=filters,
search=request.search,
select_columns=request.select_columns,
order_column=request.order_column or "dttm",
order_direction=request.order_direction,
page=max(request.page - 1, 0),
page_size=request.page_size,
)
await ctx.info(
"Action logs listed: count=%s, total_count=%s"
% (
len(result.action_logs) if hasattr(result, "action_logs") else 0,
getattr(result, "total_count", None),
)
)
columns_to_filter = result.columns_requested
await ctx.debug(
"Applying field filtering via serialization context: columns=%s"
% (columns_to_filter,)
)
with event_logger.log_context(action="mcp.list_action_logs.serialization"):
return result.model_dump(
mode="json",
context={"select_columns": columns_to_filter},
)
except Exception as e:
await ctx.error(
"Action log listing failed: page=%s, error=%s, error_type=%s"
% (request.page, str(e), type(e).__name__)
)
raise

View File

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

View File

@@ -1,367 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Pydantic schemas for annotation layer and annotation responses."""
from __future__ import annotations
from datetime import datetime
from typing import Annotated, Any, Literal
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
PositiveInt,
)
from superset.daos.base import ColumnOperator, ColumnOperatorEnum
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from superset.mcp_service.system.schemas import PaginationInfo
from superset.mcp_service.utils import sanitize_for_llm_context
from superset.mcp_service.utils.schema_utils import (
parse_json_or_list,
parse_json_or_model_list,
)
from superset.utils import json as json_utils
DEFAULT_LAYER_COLUMNS = ["id", "name", "descr"]
DEFAULT_ANNOTATION_COLUMNS = ["id", "short_descr", "start_dttm", "end_dttm", "layer_id"]
class AnnotationLayerFilter(ColumnOperator):
"""Filter object for annotation layer listing."""
col: Literal["name"] = Field(
...,
description="Column to filter on. Supported: 'name'.",
)
opr: ColumnOperatorEnum = Field(..., description="Filter operator.")
value: str | int | float | bool | list[str | int | float | bool] = Field(
..., description="Value to filter by."
)
class AnnotationFilter(ColumnOperator):
"""Filter object for annotation listing."""
col: Literal["short_descr"] = Field(
...,
description="Column to filter on. Supported: 'short_descr'.",
)
opr: ColumnOperatorEnum = Field(..., description="Filter operator.")
value: str | int | float | bool | list[str | int | float | bool] = Field(
..., description="Value to filter by."
)
class AnnotationLayerInfo(BaseModel):
id: int | None = Field(None, description="Annotation layer ID")
name: str | None = Field(None, description="Annotation layer name")
descr: str | None = Field(None, description="Annotation layer description")
changed_on: str | datetime | None = Field(
None, description="Last modification timestamp"
)
created_on: str | datetime | None = Field(None, description="Creation timestamp")
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
class AnnotationLayerList(BaseModel):
annotation_layers: list[AnnotationLayerInfo]
count: int
total_count: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
columns_requested: list[str] = Field(default_factory=list)
columns_loaded: list[str] = Field(default_factory=list)
columns_available: list[str] = Field(default_factory=list)
sortable_columns: list[str] = Field(default_factory=list)
filters_applied: list[AnnotationLayerFilter] = Field(default_factory=list)
pagination: PaginationInfo | None = None
timestamp: datetime | None = None
model_config = ConfigDict(ser_json_timedelta="iso8601")
class ListAnnotationLayersRequest(BaseModel):
"""Request schema for list_annotation_layers."""
filters: Annotated[
list[AnnotationLayerFilter],
Field(
default_factory=list,
description="List of filter objects. Cannot be combined with 'search'.",
),
]
select_columns: Annotated[
list[str],
Field(
default_factory=list,
description="Columns to include in the response.",
),
]
search: Annotated[
str | None,
Field(
default=None,
description="Text search across annotation layer name and description.",
),
]
order_column: Annotated[
str | None, Field(default=None, description="Column to order results by.")
]
order_direction: Annotated[
Literal["asc", "desc"],
Field(default="desc", description="Sort direction."),
]
page: Annotated[
PositiveInt,
Field(default=1, description="Page number (1-based)."),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Items per page (max {MAX_PAGE_SIZE}).",
),
]
@field_validator("filters", mode="before")
@classmethod
def parse_filters(cls, v: Any) -> list[AnnotationLayerFilter]:
return parse_json_or_model_list(v, AnnotationLayerFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
def parse_columns(cls, v: Any) -> list[str]:
return parse_json_or_list(v, "select_columns")
@model_validator(mode="after")
def validate_search_and_filters(self) -> "ListAnnotationLayersRequest":
if self.search and self.filters:
raise ValueError("Cannot use both 'search' and 'filters' simultaneously.")
return self
class GetAnnotationLayerInfoRequest(BaseModel):
"""Request schema for get_annotation_layer_info."""
id: Annotated[int, Field(description="Annotation layer ID.")]
class AnnotationInfo(BaseModel):
id: int | None = Field(None, description="Annotation ID")
short_descr: str | None = Field(None, description="Short description")
long_descr: str | None = Field(None, description="Long description")
start_dttm: str | datetime | None = Field(None, description="Start datetime")
end_dttm: str | datetime | None = Field(None, description="End datetime")
json_metadata: str | None = Field(None, description="JSON metadata")
layer_id: int | None = Field(None, description="Parent annotation layer ID")
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
class AnnotationList(BaseModel):
annotations: list[AnnotationInfo]
count: int
total_count: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
# layer_id defaults to 0; the tool sets it after ModelListCore constructs this
# object. ModelListCore does not know about this domain-specific field.
layer_id: int = 0
columns_requested: list[str] = Field(default_factory=list)
columns_loaded: list[str] = Field(default_factory=list)
columns_available: list[str] = Field(default_factory=list)
sortable_columns: list[str] = Field(default_factory=list)
filters_applied: list[ColumnOperator] = Field(default_factory=list)
pagination: PaginationInfo | None = None
timestamp: datetime | None = None
model_config = ConfigDict(ser_json_timedelta="iso8601")
class ListLayerAnnotationsRequest(BaseModel):
"""Request schema for list_layer_annotations."""
layer_id: Annotated[
int, Field(description="Annotation layer ID to list annotations for.")
]
filters: Annotated[
list[AnnotationFilter],
Field(
default_factory=list,
description="List of filter objects. Cannot be combined with 'search'.",
),
]
select_columns: Annotated[
list[str],
Field(default_factory=list, description="Columns to include in the response."),
]
search: Annotated[
str | None,
Field(
default=None,
description="Text search across annotation short and long description.",
),
]
order_column: Annotated[
str | None, Field(default=None, description="Column to order results by.")
]
order_direction: Annotated[
Literal["asc", "desc"],
Field(default="desc", description="Sort direction."),
]
page: Annotated[
PositiveInt,
Field(default=1, description="Page number (1-based)."),
]
page_size: Annotated[
int,
Field(
default=DEFAULT_PAGE_SIZE,
gt=0,
le=MAX_PAGE_SIZE,
description=f"Items per page (max {MAX_PAGE_SIZE}).",
),
]
@field_validator("filters", mode="before")
@classmethod
def parse_filters(cls, v: Any) -> list[AnnotationFilter]:
return parse_json_or_model_list(v, AnnotationFilter, "filters")
@field_validator("select_columns", mode="before")
@classmethod
def parse_columns(cls, v: Any) -> list[str]:
return parse_json_or_list(v, "select_columns")
@model_validator(mode="after")
def validate_search_and_filters(self) -> "ListLayerAnnotationsRequest":
if self.search and self.filters:
raise ValueError("Cannot use both 'search' and 'filters' simultaneously.")
return self
class GetLayerAnnotationInfoRequest(BaseModel):
"""Request schema for get_layer_annotation_info."""
layer_id: Annotated[int, Field(description="Annotation layer ID.")]
annotation_id: Annotated[int, Field(description="Annotation ID.")]
class AnnotationLayerError(BaseModel):
error: str = Field(..., description="Error message")
error_type: str = Field(..., description="Type of error")
timestamp: str | datetime | None = Field(None, description="Error timestamp")
model_config = ConfigDict(ser_json_timedelta="iso8601")
@classmethod
def create(cls, error: str, error_type: str) -> "AnnotationLayerError":
from datetime import timezone
return cls(
error=error,
error_type=error_type,
timestamp=datetime.now(timezone.utc),
)
def _sanitize_annotation_layer_for_llm_context(
info: AnnotationLayerInfo,
) -> AnnotationLayerInfo:
payload = info.model_dump(mode="python")
for field_name in ("name", "descr"):
payload[field_name] = sanitize_for_llm_context(
payload.get(field_name), field_path=(field_name,)
)
return AnnotationLayerInfo.model_validate(payload)
def _sanitize_annotation_json_metadata(raw: Any) -> str | None:
"""Canonicalize and sanitize the json_metadata blob before LLM exposure.
Serializing to a canonical JSON string first prevents dict-key injection:
keys are rendered as quoted string literals inside the wrapped value rather
than being able to escape the delimiter context.
"""
if raw is None:
return None
if isinstance(raw, str):
try:
canonical: str = json_utils.dumps(json_utils.loads(raw))
except (ValueError, TypeError):
canonical = raw
else:
try:
canonical = json_utils.dumps(raw)
except (ValueError, TypeError):
canonical = str(raw)
return sanitize_for_llm_context(
canonical,
field_path=("json_metadata",),
excluded_field_names=frozenset(),
)
def _sanitize_annotation_for_llm_context(info: AnnotationInfo) -> AnnotationInfo:
payload = info.model_dump(mode="python")
for field_name in ("short_descr", "long_descr"):
payload[field_name] = sanitize_for_llm_context(
payload.get(field_name), field_path=(field_name,)
)
payload["json_metadata"] = _sanitize_annotation_json_metadata(
payload.get("json_metadata")
)
return AnnotationInfo.model_validate(payload)
def serialize_annotation_layer(obj: Any) -> AnnotationLayerInfo | None:
if not obj:
return None
return _sanitize_annotation_layer_for_llm_context(
AnnotationLayerInfo(
id=getattr(obj, "id", None),
name=getattr(obj, "name", None),
descr=getattr(obj, "descr", None),
changed_on=getattr(obj, "changed_on", None),
created_on=getattr(obj, "created_on", None),
)
)
def serialize_annotation(obj: Any) -> AnnotationInfo | None:
if not obj:
return None
return _sanitize_annotation_for_llm_context(
AnnotationInfo(
id=getattr(obj, "id", None),
short_descr=getattr(obj, "short_descr", None),
long_descr=getattr(obj, "long_descr", None),
start_dttm=getattr(obj, "start_dttm", None),
end_dttm=getattr(obj, "end_dttm", None),
json_metadata=getattr(obj, "json_metadata", None),
layer_id=getattr(obj, "layer_id", None),
)
)

View File

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

View File

@@ -1,97 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Get annotation layer info FastMCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationLayerError,
AnnotationLayerInfo,
GetAnnotationLayerInfoRequest,
serialize_annotation_layer,
)
from superset.mcp_service.mcp_core import ModelGetInfoCore
logger = logging.getLogger(__name__)
@tool(
tags=["discovery"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="Get annotation layer info",
readOnlyHint=True,
destructiveHint=False,
),
)
async def get_annotation_layer_info(
request: GetAnnotationLayerInfoRequest,
ctx: Context,
) -> AnnotationLayerInfo | AnnotationLayerError:
"""Get detailed information about an annotation layer by ID.
Returns the layer's name, description, and timestamps.
Example:
```json
{"id": 1}
```
"""
await ctx.info("Retrieving annotation layer: id=%s" % (request.id,))
try:
from superset.daos.annotation_layer import AnnotationLayerDAO
with event_logger.log_context(action="mcp.get_annotation_layer_info.lookup"):
get_tool = ModelGetInfoCore(
dao_class=AnnotationLayerDAO,
output_schema=AnnotationLayerInfo,
error_schema=AnnotationLayerError,
serializer=serialize_annotation_layer,
supports_slug=False,
logger=logger,
)
result = get_tool.run_tool(request.id)
if isinstance(result, AnnotationLayerInfo):
await ctx.info(
"Annotation layer retrieved: id=%s, name=%s" % (result.id, result.name)
)
else:
await ctx.warning(
"Annotation layer not found: id=%s, error_type=%s"
% (request.id, result.error_type)
)
return result
except Exception as e:
await ctx.error(
"Annotation layer lookup failed: id=%s, error=%s, error_type=%s"
% (request.id, str(e), type(e).__name__)
)
return AnnotationLayerError(
error=f"Failed to get annotation layer info: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)

View File

@@ -1,130 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Get a single annotation within a layer FastMCP tool."""
import logging
from datetime import datetime, timezone
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationInfo,
AnnotationLayerError,
GetLayerAnnotationInfoRequest,
serialize_annotation,
)
logger = logging.getLogger(__name__)
@tool(
tags=["discovery"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="Get annotation info",
readOnlyHint=True,
destructiveHint=False,
),
)
async def get_layer_annotation_info(
request: GetLayerAnnotationInfoRequest,
ctx: Context,
) -> AnnotationInfo | AnnotationLayerError:
"""Get detailed information about a specific annotation within a layer.
Both layer_id and annotation_id are required. Returns an error if the
annotation does not belong to the specified layer.
Example:
```json
{"layer_id": 1, "annotation_id": 42}
```
"""
await ctx.info(
"Retrieving annotation: layer_id=%s, annotation_id=%s"
% (request.layer_id, request.annotation_id)
)
try:
from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
# Verify the layer exists
with event_logger.log_context(
action="mcp.get_layer_annotation_info.layer_lookup"
):
layer = AnnotationLayerDAO.find_by_id(request.layer_id)
if layer is None:
await ctx.warning("Annotation layer not found: id=%s" % (request.layer_id,))
return AnnotationLayerError.create(
error=f"Annotation layer with id '{request.layer_id}' not found",
error_type="not_found",
)
# Fetch the annotation
with event_logger.log_context(
action="mcp.get_layer_annotation_info.annotation_lookup"
):
annotation = AnnotationDAO.find_by_id(request.annotation_id)
if annotation is None:
await ctx.warning(
"Annotation not found: annotation_id=%s" % (request.annotation_id,)
)
return AnnotationLayerError.create(
error=f"Annotation with id '{request.annotation_id}' not found",
error_type="not_found",
)
# Verify the annotation belongs to the requested layer
if getattr(annotation, "layer_id", None) != request.layer_id:
await ctx.warning(
"Annotation %s does not belong to layer %s"
% (request.annotation_id, request.layer_id)
)
return AnnotationLayerError.create(
error=(
f"Annotation '{request.annotation_id}' does not belong to "
f"layer '{request.layer_id}'"
),
error_type="not_found",
)
result = serialize_annotation(annotation)
await ctx.info(
"Annotation retrieved: id=%s, short_descr=%s"
% (result.id if result else None, result.short_descr if result else None)
)
return result or AnnotationLayerError.create(
error="Failed to serialize annotation",
error_type="SerializationError",
)
except Exception as e:
await ctx.error(
"Annotation lookup failed: layer_id=%s, annotation_id=%s, "
"error=%s, error_type=%s"
% (request.layer_id, request.annotation_id, str(e), type(e).__name__)
)
return AnnotationLayerError(
error=f"Failed to get annotation info: {str(e)}",
error_type="InternalError",
timestamp=datetime.now(timezone.utc),
)

View File

@@ -1,123 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""List annotation layers FastMCP tool."""
import logging
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.extensions import event_logger
from superset.mcp_service.annotation_layer.schemas import (
AnnotationLayerError,
AnnotationLayerFilter,
AnnotationLayerInfo,
AnnotationLayerList,
DEFAULT_LAYER_COLUMNS,
ListAnnotationLayersRequest,
serialize_annotation_layer,
)
from superset.mcp_service.mcp_core import ModelListCore
logger = logging.getLogger(__name__)
_DEFAULT_REQUEST = ListAnnotationLayersRequest()
_ALL_LAYER_COLUMNS = ["id", "name", "descr", "changed_on", "created_on"]
_SORTABLE_LAYER_COLUMNS = ["id", "name", "changed_on", "created_on"]
@tool(
tags=["core"],
class_permission_name="Annotation",
annotations=ToolAnnotations(
title="List annotation layers",
readOnlyHint=True,
destructiveHint=False,
),
)
async def list_annotation_layers(
request: ListAnnotationLayersRequest | None = None,
ctx: Context | None = None,
) -> AnnotationLayerList | AnnotationLayerError:
"""List annotation layers with filtering, search, and pagination.
Returns annotation layer metadata including name and description.
Sortable columns for order_column: id, name, changed_on, created_on
"""
if ctx is None:
raise RuntimeError("FastMCP context is required for list_annotation_layers")
request = request or _DEFAULT_REQUEST.model_copy(deep=True)
await ctx.info(
"Listing annotation layers: page=%s, page_size=%s, search=%s"
% (request.page, request.page_size, request.search)
)
try:
from superset.daos.annotation_layer import AnnotationLayerDAO
def _serialize(
obj: object, cols: list[str] | None
) -> AnnotationLayerInfo | None:
return serialize_annotation_layer(obj)
list_tool = ModelListCore(
dao_class=AnnotationLayerDAO,
output_schema=AnnotationLayerInfo,
item_serializer=_serialize,
filter_type=AnnotationLayerFilter,
default_columns=DEFAULT_LAYER_COLUMNS,
search_columns=["name", "descr"],
list_field_name="annotation_layers",
output_list_schema=AnnotationLayerList,
all_columns=_ALL_LAYER_COLUMNS,
sortable_columns=_SORTABLE_LAYER_COLUMNS,
logger=logger,
)
with event_logger.log_context(action="mcp.list_annotation_layers.query"):
result = list_tool.run_tool(
filters=request.filters,
search=request.search,
select_columns=request.select_columns,
order_column=request.order_column,
order_direction=request.order_direction,
page=max(request.page - 1, 0),
page_size=request.page_size,
)
await ctx.info(
"Annotation layers listed: count=%s, total_count=%s"
% (
len(result.annotation_layers)
if hasattr(result, "annotation_layers")
else 0,
getattr(result, "total_count", None),
)
)
return result
except Exception as e:
await ctx.error(
"Annotation layer listing failed: error=%s, error_type=%s"
% (str(e), type(e).__name__)
)
raise

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