Compare commits

..

77 Commits

Author SHA1 Message Date
Pat Buxton
3f4248b4d7 Update sqlachemy-utils to 0.42.0
* Support Python 3.11 through 3.13
2026-02-11 19:15:03 -08:00
Vanessa Giannoni
77148277b9 feat(charts): improve negative stacked bar label positioning and accessibility (#37405) 2026-02-11 17:46:10 -08:00
Evan Rusackas
981b370fe9 chore(storybook): consolidate storybook and enhance plugin stories (#37771)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 16:06:23 -08:00
Enzo Martellucci
b012b63e5b fix(native-filters): align refresh icon with default value field (#37802) 2026-02-11 21:26:26 +01:00
Kamil Gabryjelski
b0be47a4ac fix: Unreachable drop zones within tabs in dashbboard editor (#37904) 2026-02-11 21:08:19 +01:00
Ville Brofeldt
00d02cb2ea perf(gtf): improve task base filter (#37900) 2026-02-11 10:40:07 -08:00
Kamil Gabryjelski
26a2e12779 perf: fix N+1 query in Slice.datasource property (#37899) 2026-02-11 18:57:28 +01:00
Luis Sánchez
5f0001affc feat(timeseries): remove stream style for bar charts (#37532) 2026-02-11 09:25:03 -08:00
Ville Brofeldt
255a0ada81 fix(gtf): add missing user_id to task commands (#37867) 2026-02-11 09:04:27 -08:00
Evan Rusackas
9089f30045 chore(lint): upgrade array creation, effect, and TypeScript rules (#37885)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 08:45:21 -08:00
Kamil Gabryjelski
98ca599eef perf: fix N+1 query in chart list API when thumbnail_url is requested (#37895) 2026-02-11 17:19:48 +01:00
Evan Rusackas
d640fe42c9 chore: remove Applitools visual testing integration (#37873)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 08:07:19 -08:00
Evan Rusackas
534fa48f1f chore(lint): enforce stricter eslint/oxlint rules (#37883)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 08:07:02 -08:00
Evan Rusackas
c28729f944 chore(lint): add jest/expect-expect rule for test assertions (#37887)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 08:06:34 -08:00
Luis Sánchez
88a14f2ba0 fix(FiltersBadge): world map wont show filter icon after refresh page (#37260) 2026-02-11 16:33:32 +03:00
Ville Brofeldt
74e1607010 fix(extensions): broken test (#37871) 2026-02-11 08:33:45 -03:00
Mehmet Salih Yavuz
69c679be20 fix(explore): Don't show unsaved changes modal on new charts (#37714) 2026-02-11 13:05:42 +03:00
Evan Rusackas
9a79dbf445 fix(docs): make page size selector work in database table (#37863)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 22:15:55 -08:00
dependabot[bot]
7e5ca83220 chore(deps-dev): bump @types/node from 25.2.2 to 25.2.3 in /superset-frontend (#37851)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 14:24:55 -08:00
dependabot[bot]
7d4a7f113c chore(deps-dev): bump webpack from 5.105.0 to 5.105.1 in /docs (#37849)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 14:24:13 -08:00
dependabot[bot]
4eb8fc814a chore(deps-dev): bump @types/node from 25.2.2 to 25.2.3 in /superset-websocket (#37846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 14:23:11 -08:00
Tadas Barzdžius
39ac96817a fix(helm): Add default initContainer resources (#37637) 2026-02-10 11:51:37 -08:00
Levis Mbote
1388a62823 fix(filters): fix filter / customization name not updating in sidebar in real time (#37358) 2026-02-10 20:41:47 +01:00
Michael S. Molina
6a6b9b5386 chore: Bump core packages (0.0.1rc11, 0.0.1rc4) (#37860) 2026-02-10 16:37:07 -03:00
Michael S. Molina
b98b34a60f refactor: Make extensions contribution schema consistent (#37856) 2026-02-10 15:55:39 -03:00
Kamil Gabryjelski
7ec5f1d7ec fix(native-filters): Filters with select first value not restored correctly from url (#37855) 2026-02-10 18:54:42 +01:00
Đỗ Trọng Hải
76aa91f5ea fix(deps): pin react-error-boundary to 6.0.0 for React 17 peer dep constraint (#37706)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-10 23:22:14 +07:00
Michael S. Molina
c41942a38a chore(deps): Upgrade sqlglot from 27.15.2 to 28.10.0 (#37841) 2026-02-10 13:13:11 -03:00
Alexandru Soare
ae8d671fea fix(sql): fix sql suggestions (#37699) 2026-02-10 17:30:17 +02:00
Enzo Martellucci
c59d0a73d4 fix: Prevent table rows from overlapping pagination in table view (#37174)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2026-02-10 16:01:39 +01:00
Ville Brofeldt
0f1278fa61 fix(gtf): set dedup_key on atomic sql (#37820) 2026-02-10 06:56:14 -08:00
dependabot[bot]
948b1d613b chore(deps-dev): bump typescript-eslint from 8.54.0 to 8.55.0 in /docs (#37825)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 21:52:30 +07:00
dependabot[bot]
3af795af36 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.54.0 to 8.55.0 in /superset-websocket (#37822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 21:36:12 +07:00
dependabot[bot]
1cba53a043 chore(deps-dev): bump @typescript-eslint/parser from 8.54.0 to 8.55.0 in /superset-websocket (#37823)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:09:19 -08:00
dependabot[bot]
8c6bc3eaea chore(deps): bump antd from 6.2.3 to 6.3.0 in /docs (#37824)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:08:58 -08:00
dependabot[bot]
4d8ff84587 chore(deps-dev): bump @playwright/test from 1.58.1 to 1.58.2 in /superset-frontend (#37826)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:07:55 -08:00
dependabot[bot]
f370da5a87 chore(deps-dev): bump @typescript-eslint/parser from 8.54.0 to 8.55.0 in /docs (#37827)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:07:36 -08:00
dependabot[bot]
2df60f9caf chore(deps): bump immer from 11.1.3 to 11.1.4 in /superset-frontend (#37830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:07:08 -08:00
dependabot[bot]
d078f18ff8 chore(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /superset-websocket (#37796)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:06:17 -08:00
dependabot[bot]
6ca028dee9 chore(deps): bump axios from 1.12.2 to 1.13.5 in /docs (#37814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 01:05:54 -08:00
Tu Shaokun
76351ff12c fix(i18n): ensure language pack loads before React renders (#36893) 2026-02-10 00:29:04 -08:00
Joe Li
f6f96ecc49 test(chart-list): migrate Chart List tests from Cypress to RTL (#37813)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:25:49 -08:00
Ville Brofeldt
59dd2fa385 feat: add global task framework (#36368) 2026-02-09 10:45:56 -08:00
Michael S. Molina
6984e93171 fix: SQL Lab improvements and bug fixes (#37760) 2026-02-09 14:29:08 -03:00
Kamil Gabryjelski
f25d95be41 fix: Vertical lines in the middle of Treemap categories (#37808) 2026-02-09 17:44:59 +01:00
Đỗ Trọng Hải
5125a67002 build(dev-deps): remove npm from @apache-superset/core (#37774)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-09 13:00:40 -03:00
dependabot[bot]
059b57d784 chore(deps-dev): bump @types/node from 25.2.1 to 25.2.2 in /superset-frontend (#37801)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 22:47:12 +07:00
Đỗ Trọng Hải
a1d65c7529 feat(deps): significant npm audit fix to trim off inadvertently runtime dep from upstream libraries (#37220)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 22:13:57 +07:00
Amin Ghadersohi
15b3c96f8e fix(security): Add table blocklist and fix MCP SQL validation bypass (#37411) 2026-02-09 14:12:06 +01:00
Alexandru Soare
2b411b32ba fix(scatter): Fix ad-hoc metric for pointsize (#37669) 2026-02-09 11:13:06 +02:00
Nikhil
cebdb9e0b7 fix(ListView): add tooltip for layout toggle buttons (#37581)
Co-authored-by: root <root@DESKTOP-LUKSKD7.localdomain>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-02-09 02:37:53 -05:00
dependabot[bot]
ce872ddaf0 chore(deps-dev): bump @swc/core from 1.14.0 to 1.15.11 in /superset-frontend (#37511)
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@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 02:27:45 -05:00
dependabot[bot]
29aa69b779 chore(deps): update @luma.gl/engine requirement from ~9.2.5 to ~9.2.6 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#37762)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 01:28:59 -05:00
Evan Rusackas
ebee9bb3f9 refactor(types): consolidate shared table types and fix Funnel enum typo (#37768)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 01:14:34 -05:00
Vinícius Borges Alencar
82d6076804 refactor(charts): filter saved metrics by key and label (#37136) 2026-02-09 07:29:32 +03:00
Đỗ Trọng Hải
3b75af9ac3 docs(dev_portal/test): remove refs of testing tools not used in project (#37786)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-08 16:46:31 +07:00
Đỗ Trọng Hải
563d9f1a3f chore(lint): migrate Jest lint rules from eslint to oxlint (#37787)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-08 16:44:42 +07:00
Đỗ Trọng Hải
c4d2d42b3b build(dev-deps): move Webpack-dedicated js-yaml-loader to dev deps section (#37788)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-08 01:37:12 -08:00
dependabot[bot]
7580bd1401 chore(deps-dev): bump timezone-mock from 1.3.6 to 1.4.0 in /superset-frontend (#37333)
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@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 23:07:29 -08:00
SBIN2010
c4e7c3b03b refactor: consolidating ColorSchemeEnum settings into one place (#37591) 2026-02-07 23:04:20 -08:00
dependabot[bot]
3521f191b2 chore(deps): bump webpack from 5.96.1 to 5.105.0 in /superset-frontend/cypress-base (#37775)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 10:18:47 -08:00
Rini Misini
f4708a5648 fix(db): prevent long database error messages from overflowing UI (#37709)
Co-authored-by: RiniMisini12 <misinirini@gmail.com>
2026-02-07 21:13:09 +07:00
dependabot[bot]
b9ab03994a chore(deps-dev): bump jsdom from 27.4.0 to 28.0.0 in /superset-frontend (#37688)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 15:32:38 +07:00
dependabot[bot]
df253f6aa4 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.28.5 to 7.29.0 in /superset-frontend (#37631)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 00:12:35 -08:00
dependabot[bot]
5cea4fb7fe chore(deps): update @luma.gl/shadertools requirement from ~9.2.5 to ~9.2.6 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#37763)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 23:55:22 -08:00
dependabot[bot]
76a27d5360 chore(deps): bump d3-format from 1.4.5 to 3.1.2 in /superset-frontend/packages/superset-ui-core (#37442)
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@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 23:42:39 -08:00
dependabot[bot]
174e3c26d3 chore(deps): update @luma.gl/webgl requirement from ~9.2.5 to ~9.2.6 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#37764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 23:32:37 -08:00
Alexandru Soare
9ea5ded988 fix(dashboard): Prevent fatal error when database connection is unavailable (#37576) 2026-02-06 20:52:17 -08:00
Đỗ Trọng Hải
9086ae8e6c feat(ci): only bump patch version for Storybook-related deps until React 18 (#37749)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-02-06 13:29:32 -08:00
Evan Rusackas
fc5506e466 chore(frontend): comprehensive TypeScript quality improvements (#37625)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 13:16:57 -08:00
Abhishek Mishra
e9ae212c1c fix(alerts): show screenshot width field for PDF reports (#37037) 2026-02-06 11:19:18 -08:00
Evan Rusackas
46bca32677 docs(seo): add structured data, OpenGraph tags, and sitemap improvements (#37404)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 11:09:19 -08:00
JUST.in DO IT
a04571fa20 fix(world-map): reset hover highlight on mouse out (#37716)
Co-authored-by: Arunodoy18 <arunodoy630@gmail.com>
2026-02-06 10:27:57 -08:00
Evan Rusackas
fc26dbfebf chore(deps): upgrade deck.gl and luma.gl packages to ~9.2.6 (#37718)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:20:50 -08:00
Michael S. Molina
0415118544 chore: Bump @apache-superset/core (0.0.1-rc10) (#37759) 2026-02-06 14:18:22 -03:00
Michael S. Molina
935bbe6061 docs: Updates extensions docs (#37704) 2026-02-06 13:18:25 -03:00
Daniel Vaz Gaspar
ec6eaf4898 fix(deps): bump elasticsearch-dbapi to 0.2.12 for urllib3 2.x compatibility (#37758)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 16:03:04 +00:00
1090 changed files with 44415 additions and 55674 deletions

View File

@@ -9,9 +9,13 @@ updates:
- package-ecosystem: "npm"
ignore:
# not until React >= 18.0.0
# TODO: remove below entries until React >= 18.0.0
- dependency-name: "storybook"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@storybook*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "eslint-plugin-storybook"
- dependency-name: "react-error-boundary"
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
- dependency-name: "remark-gfm"
- dependency-name: "react-markdown"
@@ -23,6 +27,14 @@ updates:
# See https://github.com/apache/superset/pull/37384#issuecomment-3793991389
# TODO: remove the plugin once Lodash usage has been migrated to a more readily tree-shakeable alternative
- dependency-name: "@swc/plugin-transform-imports"
groups:
storybook:
applies-to: version-updates
patterns:
- "@storybook*"
- "storybook"
update-types:
- "patch"
directory: "/superset-frontend/"
schedule:
interval: "daily"
@@ -89,16 +101,6 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-histogram/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
schedule:
@@ -199,16 +201,6 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
schedule:
@@ -229,16 +221,6 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-event-flow/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
schedule:
@@ -249,16 +231,6 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
schedule:
@@ -270,7 +242,7 @@ updates:
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/preset-chart-xy/"
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
schedule:
interval: "daily"
labels:
@@ -280,7 +252,7 @@ updates:
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-heatmap/"
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
schedule:
interval: "daily"
labels:
@@ -299,16 +271,6 @@ updates:
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sunburst/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
schedule:
@@ -345,16 +307,7 @@ updates:
# not until React >= 18.0.0
- dependency-name: "react-markdown"
- dependency-name: "remark-gfm"
schedule:
interval: "daily"
labels:
- npm
- dependabot
open-pull-requests-limit: 5
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-demo/"
- dependency-name: "react-error-boundary"
schedule:
interval: "daily"
labels:

View File

@@ -304,26 +304,3 @@ monitor_memory() {
sleep 2
done
}
cypress-run-applitools() {
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
local flasklog="${HOME}/flask.log"
local port=8081
local cypress="./node_modules/.bin/cypress run"
local browser=${CYPRESS_BROWSER:-chrome}
export CYPRESS_BASE_URL="http://localhost:${port}"
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
local flaskProcessId=$!
$cypress --spec "cypress/applitools/**/*" --browser "$browser" --headless
say "::group::Flask log for default run"
cat "$flasklog"
say "::endgroup::"
# make sure the program exits
kill $flaskProcessId
}

View File

@@ -39,13 +39,9 @@ jobs:
# pkg:npm/store2@2.14.2
# adding an exception for an ambigious license on store2, which has been resolved in
# the latest version. It's MIT: https://github.com/nbubna/store/blob/master/LICENSE-MIT
# pkg:npm/applitools/*
# adding exception for all applitools modules (eyes-cypress and its dependencies),
# which has an explicit OSS license approved by ASF
# license: https://applitools.com/legal/open-source-terms-of-use/
# pkg:npm/node-forge@1.3.1
# selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
python-dependency-liccheck:
# NOTE: Configuration for liccheck lives in our pyproject.yml.

View File

@@ -1,70 +0,0 @@
name: Prefer TypeScript
on:
push:
branches:
- "master"
- "[0-9].[0-9]*"
paths:
- "superset-frontend/src/**"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
paths:
- "superset-frontend/src/**"
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
prefer_typescript:
if: github.ref == 'ref/heads/master' && github.event_name == 'pull_request'
name: Prefer TypeScript
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Get changed files
id: changed
uses: ./.github/actions/file-changes-action
with:
githubToken: ${{ github.token }}
- name: Determine if a .js or .jsx file was added
id: check
run: |
js_files_added() {
jq -r '
map(
select(
endswith(".js") or endswith(".jsx")
)
) | join("\n")
' ${HOME}/files_added.json
}
echo "js_files_added=$(js_files_added)" >> $GITHUB_OUTPUT
- if: steps.check.outputs.js_files_added
name: Add Comment to PR
uses: ./.github/actions/comment-on-pr
continue-on-error: true
env:
GITHUB_TOKEN: ${{ github.token }}
with:
msg: |
### WARNING: Prefer TypeScript
Looks like your PR contains new `.js` or `.jsx` files:
```
${{steps.check.outputs.js_files_added}}
```
As decided in [SIP-36](https://github.com/apache/superset/issues/9101), all new frontend code should be written in TypeScript. Please convert above files to TypeScript then re-request review.

View File

@@ -1,91 +0,0 @@
name: Applitools Cypress
on:
schedule:
- cron: "0 1 * * *"
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 "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
cypress-applitools:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
browser: ["chrome"]
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
APPLITOOLS_APP_NAME: Superset
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
APPLITOOLS_BATCH_ID: ${{ github.sha }}
APPLITOOLS_BATCH_NAME: Superset Cypress
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: redis:7-alpine
ports:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
ref: master
- name: Setup Python
uses: ./.github/actions/setup-backend/
- name: Import test data
uses: ./.github/actions/cached-dependencies
with:
run: testdata
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install npm dependencies
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Build javascript packages
uses: ./.github/actions/cached-dependencies
with:
run: build-instrumented-assets
- name: Setup Postgres
if: steps.check.outcome == 'failure'
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Install cypress
uses: ./.github/actions/cached-dependencies
with:
run: cypress-install
- name: Run Cypress
uses: ./.github/actions/cached-dependencies
env:
CYPRESS_BROWSER: ${{ matrix.browser }}
with:
run: cypress-run-applitools

View File

@@ -1,52 +0,0 @@
name: Applitools Storybook
on:
schedule:
- cron: "0 0 * * *"
env:
APPLITOOLS_APP_NAME: Superset
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
APPLITOOLS_BATCH_ID: ${{ github.sha }}
APPLITOOLS_BATCH_NAME: Superset Storybook
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 "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
cron:
needs: config
if: needs.config.outputs.has-secrets
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
ref: master
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install eyes-storybook dependencies
uses: ./.github/actions/cached-dependencies
with:
run: eyes-storybook-dependencies
- name: Install NPM dependencies
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Run Applitools Eyes-Storybook
working-directory: ./superset-frontend
run: npx eyes-storybook -u https://superset-storybook.netlify.app/

View File

@@ -163,11 +163,6 @@ jobs:
docker run --rm $TAG bash -c \
"npm run plugins:build"
- name: Build Plugins Storybook
run: |
docker run --rm $TAG bash -c \
"npm run plugins:build-storybook"
test-storybook:
needs: frontend-build
if: needs.frontend-build.outputs.should-run == 'true'

View File

@@ -52,7 +52,6 @@ jobs:
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@v5
with:

View File

@@ -24,6 +24,28 @@ assists people when migrating to a new version.
## Next
### Signal Cache Backend
A new `SIGNAL_CACHE_CONFIG` configuration provides a unified Redis-based backend for real-time coordination features in Superset. This backend enables:
- **Pub/sub messaging** for real-time event notifications between workers
- **Atomic distributed locking** using Redis SET NX EX (more performant than database-backed locks)
- **Event-based coordination** for background task management
The signal cache is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
Example configuration in `superset_config.py`:
```python
SIGNAL_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_KEY_PREFIX": "signal_",
"CACHE_REDIS_URL": "redis://localhost:6379/1",
"CACHE_DEFAULT_TIMEOUT": 300,
}
```
See `superset/config.py` for complete configuration options.
### WebSocket config for GAQ with Docker
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:

View File

@@ -105,12 +105,7 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -788,7 +788,7 @@ pytest ./link_to_test.py
### Frontend Testing
We use [Jest](https://jestjs.io/) and [Enzyme](https://airbnb.io/enzyme/) to test TypeScript/JavaScript. Tests can be run with:
We use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) to test TypeScript. Tests can be run with:
```bash
cd superset-frontend

View File

@@ -100,7 +100,7 @@ npm link superset-plugin-chart-hello-world
```
7. **Import and register in Superset**:
Edit `superset-frontend/src/visualizations/presets/MainPreset.js` to include your plugin.
Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin.
## Testing

View File

@@ -38,12 +38,14 @@ Extensions can add new views or panels to the host application, such as custom S
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "my_extension.main",
"name": "My Panel Name"
}
]
"sqllab": {
"panels": [
{
"id": "my_extension.main",
"name": "My Panel Name"
}
]
}
}
}
}
@@ -76,25 +78,27 @@ Extensions can contribute new menu items or context menus to the host applicatio
"frontend": {
"contributions": {
"menus": {
"sqllab.editor": {
"primary": [
{
"view": "builtin.editor",
"command": "my_extension.copy_query"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "my_extension.prettify"
}
],
"context": [
{
"view": "builtin.editor",
"command": "my_extension.clear"
}
]
"sqllab": {
"editor": {
"primary": [
{
"view": "builtin.editor",
"command": "my_extension.copy_query"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "my_extension.prettify"
}
],
"context": [
{
"view": "builtin.editor",
"command": "my_extension.clear"
}
]
}
}
}
}

View File

@@ -92,12 +92,14 @@ The `extension.json` file contains all metadata necessary for the host applicati
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "dataset_references.main",
"name": "Dataset references"
}
]
"sqllab": {
"panels": [
{
"id": "dataset_references.main",
"name": "Dataset references"
}
]
}
}
},
"moduleFederation": {
@@ -134,9 +136,9 @@ export const onDidChangeActivePanel: Event<Panel>;
export const onDidChangeTabTitle: Event<string>;
export const onDidQueryRun: Event<Editor>;
export const onDidQueryRun: Event<QueryContext>;
export const onDidQueryStop: Event<Editor>;
export const onDidQueryStop: Event<QueryContext>;
```
The following code demonstrates more examples of the existing frontend APIs:
@@ -150,16 +152,16 @@ export function activate(context) {
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
// Register a custom command
const commandDisposable = commands.registerCommand('my_extension.copy_query', {
title: 'Copy Query',
execute: () => {
const commandDisposable = commands.registerCommand(
'my_extension.copy_query',
() => {
// Command logic here
},
});
);
// Listen for query run events in SQL Lab
const eventDisposable = sqlLab.onDidQueryRun(editor => {
// Handle query execution event
const eventDisposable = sqlLab.onDidQueryRun(queryContext => {
console.log('Query started on database:', queryContext.tab.databaseId);
});
// Access a CSRF token for secure API requests

View File

@@ -24,7 +24,7 @@ under the License.
# SQL Lab Extension Points
SQL Lab provides 5 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and can be customized to add new functionality.
SQL Lab provides 4 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and supports different types of customizations. These areas will evolve over time as new features are added to SQL Lab.
## Layout Overview
@@ -41,42 +41,44 @@ SQL Lab provides 5 extension points where extensions can contribute custom UI co
│ │ │ │
│ │ │ │
│ │ │ │
──────────┴─────────────────────────────────────────┴─────────────
│ Status Bar │
└──────────────────────────────────────────────────────────────────┘
──────────┴─────────────────────────────────────────┴─────────────
```
| Extension Point | ID | Description |
| ----------------- | --------------------- | ---------------------------------------------------------- |
| **Left Sidebar** | `sqllab.leftSidebar` | Navigation and browsing (database explorer, saved queries) |
| **Editor** | `sqllab.editor` | SQL query editor workspace |
| **Right Sidebar** | `sqllab.rightSidebar` | Contextual tools (AI assistants, query analysis) |
| **Panels** | `sqllab.panels` | Results and related views (visualizations, data profiling) |
| **Status Bar** | `sqllab.statusBar` | Connection status and query metrics |
| Extension Point | ID | Views | Menus | Description |
| ----------------- | --------------------- | ----- | ----- | ---------------------------------------------- |
| **Left Sidebar** | `sqllab.leftSidebar` | — | ✓ | Menu actions for the database explorer |
| **Editor** | `sqllab.editor` | ✓\* | ✓ | Custom editors + toolbar actions |
| **Right Sidebar** | `sqllab.rightSidebar` | ✓ | — | Custom panels (AI assistants, query analysis) |
| **Panels** | `sqllab.panels` | ✓ | ✓ | Custom tabs + toolbar actions (data profiling) |
## Area Customizations
\*Editor views are contributed via [Editor Contributions](./editors), not standard view contributions.
Each extension point area supports three types of action customizations:
## Customization Types
### Views
Extensions can add custom views (React components) to **Right Sidebar** and **Panels**. Views appear as new panels or tabs in their respective areas.
### Menus
Extensions can add toolbar actions to **Left Sidebar**, **Editor**, and **Panels**. Menu contributions support:
```
┌───────────────────────────────────────────────────────────────┐
Area Title [Button] [Button] [•••] │
│ [Button] [Button] [•••]
├───────────────────────────────────────────────────────────────┤
│ │
│ │
│ Area Content │
│ │
│ (right-click for context menu) │
│ │
│ │
└───────────────────────────────────────────────────────────────┘
```
| Action Type | Location | Use Case |
| --------------------- | ----------------- | ----------------------------------------------------- |
| **Primary Actions** | Top-right buttons | Frequently used actions (e.g., run, refresh, add new) |
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
| **Context Actions** | Right-click menu | Context-sensitive actions on content |
| Action Type | Location | Use Case |
| --------------------- | ---------------- | ----------------------------------------------------- |
| **Primary Actions** | Toolbar buttons | Frequently used actions (e.g., run, refresh, add new) |
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
### Custom Editors
Extensions can replace the default SQL editor with custom implementations (Monaco, CodeMirror, etc.). See [Editor Contributions](./editors) for details.
## Examples
@@ -91,12 +93,14 @@ This example adds a "Data Profiler" panel to SQL Lab:
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "data_profiler.main",
"name": "Data Profiler"
}
]
"sqllab": {
"panels": [
{
"id": "data_profiler.main",
"name": "Data Profiler"
}
]
}
}
}
}
@@ -140,25 +144,27 @@ This example adds primary, secondary, and context actions to the editor:
}
],
"menus": {
"sqllab.editor": {
"primary": [
{
"view": "builtin.editor",
"command": "query_tools.format"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "query_tools.explain"
}
],
"context": [
{
"view": "builtin.editor",
"command": "query_tools.copy_as_cte"
}
]
"sqllab": {
"editor": {
"primary": [
{
"view": "builtin.editor",
"command": "query_tools.format"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "query_tools.explain"
}
],
"context": [
{
"view": "builtin.editor",
"command": "query_tools.copy_as_cte"
}
]
}
}
}
}
@@ -171,32 +177,38 @@ import { commands, sqlLab } from '@apache-superset/core';
export function activate(context) {
// Register the commands declared in extension.json
const formatCommand = commands.registerCommand('query_tools.format', {
execute: () => {
const formatCommand = commands.registerCommand(
'query_tools.format',
async () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
if (tab) {
const editor = await tab.getEditor();
// Format the SQL query
}
},
});
);
const explainCommand = commands.registerCommand('query_tools.explain', {
execute: () => {
const explainCommand = commands.registerCommand(
'query_tools.explain',
async () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
if (tab) {
const editor = await tab.getEditor();
// Show query explanation
}
},
});
);
const copyAsCteCommand = commands.registerCommand('query_tools.copy_as_cte', {
execute: () => {
const copyAsCteCommand = commands.registerCommand(
'query_tools.copy_as_cte',
async () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
if (tab) {
const editor = await tab.getEditor();
// Copy selected text as CTE
}
},
});
);
context.subscriptions.push(formatCommand, explainCommand, copyAsCteCommand);
}

View File

@@ -51,4 +51,5 @@ Extensions can provide:
- **[Deployment](./deployment)** - Packaging and deploying extensions
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
- **[Security](./security)** - Security considerations and best practices
- **[Tasks](./tasks)** - Framework for creating and managing long running tasks
- **[Community Extensions](./registry)** - Browse extensions shared by the community

View File

@@ -94,12 +94,14 @@ The generated `extension.json` contains basic metadata. Update it to register yo
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "hello_world.main",
"name": "Hello World"
}
]
"sqllab": {
"panels": [
{
"id": "hello_world.main",
"name": "Hello World"
}
]
}
}
},
"moduleFederation": {

View File

@@ -1,6 +1,6 @@
---
title: Community Extensions
sidebar_position: 10
sidebar_position: 11
---
<!--

View File

@@ -0,0 +1,440 @@
---
title: Tasks
sidebar_position: 10
---
<!--
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.
-->
# Global Task Framework
The Global Task Framework (GTF) provides a unified way to manage background tasks. It handles task execution, progress tracking, cancellation, and deduplication for both synchronous and asynchronous execution. The framework uses distributed locking internally to ensure race-free operations—you don't need to worry about concurrent task creation or cancellation conflicts.
## Enabling GTF
GTF is disabled by default and must be enabled via the `GLOBAL_TASK_FRAMEWORK` feature flag in your `superset_config.py`:
```python
FEATURE_FLAGS = {
"GLOBAL_TASK_FRAMEWORK": True,
}
```
When GTF is disabled:
- The Task List UI menu item is hidden
- The `/api/v1/task/*` endpoints return 404
- Calling or scheduling a `@task`-decorated function raises `GlobalTaskFrameworkDisabledError`
:::note Future Migration
When GTF is considered stable, it will replace legacy Celery tasks for built-in features like thumbnails and alerts & reports. Enabling this flag prepares your deployment for that migration.
:::
## Quick Start
### Define a Task
```python
from superset_core.api.tasks import task, get_context
@task
def process_data(dataset_id: int) -> None:
ctx = get_context()
@ctx.on_cleanup
def cleanup():
logger.info("Processing complete")
data = fetch_dataset(dataset_id)
process_and_cache(data)
```
### Execute a Task
```python
# Async execution - schedules on Celery worker
task = process_data.schedule(dataset_id=123)
print(task.status) # "pending"
# Sync execution - runs inline in current process
task = process_data(dataset_id=123)
# ... blocks until complete
print(task.status) # "success"
```
### Async vs Sync Execution
| Method | When to Use |
|--------|-------------|
| `.schedule()` | Long-running operations, background processing, when you need to return immediately |
| Direct call | Short operations, when deduplication matters, when you need the result before responding |
Both execution modes provide the same task features: deduplication, progress tracking, cancellation, and visibility in the Task List UI. The difference is whether execution happens in a Celery worker (async) or inline (sync).
## Task Lifecycle
```
PENDING ──→ IN_PROGRESS ────→ SUCCESS
│ │
│ ├──────────→ FAILURE
│ ↓ ↑
│ ABORTING ────────────┘
│ │
│ ├──────────→ TIMED_OUT (timeout)
│ │
└─────────────┴──────────→ ABORTED (user cancel)
```
| Status | Description |
|--------|-------------|
| `PENDING` | Queued, awaiting execution |
| `IN_PROGRESS` | Executing |
| `ABORTING` | Abort/timeout triggered, abort handlers running |
| `SUCCESS` | Completed successfully |
| `FAILURE` | Failed with error or abort/cleanup handler exception |
| `ABORTED` | Cancelled by user/admin |
| `TIMED_OUT` | Exceeded configured timeout |
## Context API
Access task context via `get_context()` from within any `@task` function. The context provides methods for updating task metadata and registering handlers.
### Updating Task Metadata
Use `update_task()` to report progress and store custom payload data:
```python
@task
def my_task(items: list[int]) -> None:
ctx = get_context()
for i, item in enumerate(items):
result = process(item)
ctx.update_task(
progress=(i + 1, len(items)),
payload={"last_result": result}
)
```
:::tip
Call `update_task()` once per iteration for best performance. Frequent DB writes are throttled to limit metastore load, so batching progress and payload updates together in a single call ensures both are persisted at the same time.
:::
#### Progress Formats
The `progress` parameter accepts three formats:
| Format | Example | Display |
|--------|---------|---------|
| `tuple[int, int]` | `progress=(3, 100)` | 3 of 100 (3%) with ETA |
| `float` (0.0-1.0) | `progress=0.5` | 50% with ETA |
| `int` | `progress=42` | 42 processed |
:::tip
Use the tuple format `(current, total)` whenever possible. It provides the richest information to users: showing both the count and percentage, while still computing ETA automatically.
:::
#### Payload
The `payload` parameter stores custom metadata that can help users understand what the task is doing. Each call to `update_task()` replaces the previous payload completely.
In the Task List UI, when a payload is defined, an info icon appears in the **Details** column. Users can hover over it to see the JSON content.
### Handlers
Register handlers to run cleanup logic or respond to abort requests:
| Handler | When it runs | Use case |
|---------|--------------|----------|
| `on_cleanup` | Always (success, failure, abort) | Release resources, close connections |
| `on_abort` | When task is aborted | Set stop flag, cancel external operations |
```python
@task
def my_task() -> None:
ctx = get_context()
@ctx.on_cleanup
def cleanup():
logger.info("Task ended, cleaning up")
@ctx.on_abort
def handle_abort():
logger.info("Abort requested")
# ... task logic
```
Multiple handlers of the same type execute in LIFO order (last registered runs first). Abort handlers run first when abort is detected, then cleanup handlers run when the task ends.
#### Best-Effort Execution
**All registered handlers will always be attempted, even if one fails.** This ensures that a failure in one handler doesn't prevent other handlers from running their cleanup logic.
For example, if you have three cleanup handlers and the second one throws an exception:
1. Handler 3 runs ✓
2. Handler 2 throws an exception ✗ (logged, but execution continues)
3. Handler 1 runs ✓
If any handler fails, the task is marked as `FAILURE` with combined error details showing all handler failures.
:::tip
Write handlers to be independent and self-contained. Don't assume previous handlers succeeded, and don't rely on shared state between handlers.
:::
## Making Tasks Abortable
When users click **Cancel** in the Task List, the system decides whether to **abort** (stop) the task or **unsubscribe** (remove the user from a shared task). Abort occurs when:
- It's a private or system task
- It's a shared task and the user is the last subscriber
- An admin checks **Force abort** to stop the task for all subscribers
Pending tasks can always be aborted: they simply won't start. In-progress tasks require an abort handler to be abortable:
```python
@task
def abortable_task(items: list[str]) -> None:
ctx = get_context()
should_stop = False
@ctx.on_abort
def handle_abort():
nonlocal should_stop
should_stop = True
logger.info("Abort signal received")
@ctx.on_cleanup
def cleanup():
logger.info("Task ended, cleaning up")
for item in items:
if should_stop:
return # Exit gracefully
process(item)
```
**Key points:**
- Registering `on_abort` marks the task as abortable and starts the abort listener
- The abort handler fires automatically when abort is triggered
- Use a flag pattern to gracefully stop processing at safe points
- Without an abort handler, in-progress tasks cannot be aborted: the Cancel button in the Task List UI will be disabled
The framework automatically skips execution if a task was aborted while pending: no manual check needed at task start.
:::tip
Always implement an abort handler for long-running tasks. This allows users to cancel unneeded tasks and free up worker capacity for other operations.
:::
## Timeouts
Set a timeout to automatically abort tasks that run too long:
```python
from superset_core.api.tasks import task, get_context, TaskOptions
# Set default timeout in decorator
@task(timeout=300) # 5 minutes
def process_data(dataset_id: int) -> None:
ctx = get_context()
should_stop = False
@ctx.on_abort
def handle_abort():
nonlocal should_stop
should_stop = True
for chunk in fetch_large_dataset(dataset_id):
if should_stop:
return
process(chunk)
# Override timeout at call time
task = process_data.schedule(
dataset_id=123,
options=TaskOptions(timeout=600) # Override to 10 minutes
)
```
### How Timeouts Work
The timeout timer starts when the task begins executing (status changes to `IN_PROGRESS`). When the timeout expires:
1. **With an abort handler registered:** The task transitions to `ABORTING`, abort handlers run, then cleanup handlers run. The final status depends on handler execution:
- If handlers complete successfully → `TIMED_OUT` status
- If handlers throw an exception → `FAILURE` status
2. **Without an abort handler:** The framework cannot forcibly terminate the task. A warning is logged, and the task continues running. The Task List UI shows a warning indicator (⚠️) in the Details column to alert users that the timeout cannot be enforced.
### Timeout Precedence
| Source | Priority | Example |
|--------|----------|---------|
| `TaskOptions.timeout` | Highest | `options=TaskOptions(timeout=600)` |
| `@task(timeout=...)` | Default | `@task(timeout=300)` |
| Not set | No timeout | Task runs indefinitely |
Call-time options always override decorator defaults, allowing tasks to have sensible defaults while permitting callers to extend or shorten the timeout for specific use cases.
:::warning
Timeouts require an abort handler to be effective. Without one, the timeout triggers only a warning and the task continues running. Always implement an abort handler when using timeouts.
:::
## Deduplication
Use `task_key` to prevent duplicate task execution:
```python
from superset_core.api.tasks import TaskOptions
# Without key - creates new task each time (random UUID)
task1 = my_task.schedule(x=1)
task2 = my_task.schedule(x=1) # Different task
# With key - joins existing task if active
task1 = my_task.schedule(x=1, options=TaskOptions(task_key="report_123"))
task2 = my_task.schedule(x=1, options=TaskOptions(task_key="report_123")) # Returns same task
```
When a task with matching key already exists, the user is added as a subscriber and the existing task is returned. This behavior is consistent across all scopes—private tasks naturally have only one subscriber since their deduplication key includes the user ID.
Deduplication only applies to active tasks (pending/in-progress). Once a task completes, a new task with the same key can be created.
### Sync Join-and-Wait
When a sync call joins an existing task, it blocks until the task completes:
```python
# Schedule async task
task = my_task.schedule(options=TaskOptions(task_key="report_123"))
# Later sync call with same key blocks until completion of the active task
task2 = my_task(options=TaskOptions(task_key="report_123"))
assert task.uuid == task2.uuid # True
print(task2.status) # "success" (terminal status)
```
## Task Scopes
```python
from superset_core.api.tasks import task, TaskScope
@task # Private by default
def private_task(): ...
@task(scope=TaskScope.SHARED) # Multiple users can subscribe
def shared_task(): ...
@task(scope=TaskScope.SYSTEM) # Admin-only visibility
def system_task(): ...
```
| Scope | Visibility | Cancel Behavior |
|-------|------------|-----------------|
| `PRIVATE` | Creator only | Cancels immediately |
| `SHARED` | All subscribers | Last subscriber cancels; others unsubscribe |
| `SYSTEM` | Admins only | Admin cancels |
## Task Cleanup
Completed tasks accumulate in the database over time. Configure a scheduled prune job to automatically remove old tasks:
```python
# In your superset_config.py, add to your Celery beat schedule:
CELERY_CONFIG.beat_schedule["prune_tasks"] = {
"task": "prune_tasks",
"schedule": crontab(minute=0, hour=0), # Run daily at midnight
"kwargs": {
"retention_period_days": 90, # Keep tasks for 90 days
"max_rows_per_run": 10000, # Limit deletions per run
},
}
```
The prune job only removes tasks in terminal states (`SUCCESS`, `FAILURE`, `ABORTED`, `TIMED_OUT`). Active tasks (`PENDING`, `IN_PROGRESS`, `ABORTING`) are never pruned.
See `superset/config.py` for a complete example configuration.
:::tip Signal Cache for Faster Notifications
By default, abort detection and sync join-and-wait use database polling. Configure `SIGNAL_CACHE_CONFIG` to enable Redis pub/sub for real-time notifications. See [Signal Cache Backend](/docs/configuration/cache#signal-cache-backend) for configuration details.
:::
## API Reference
### @task Decorator
```python
@task(
name: str | None = None,
scope: TaskScope = TaskScope.PRIVATE,
timeout: int | None = None
)
```
- `name`: Task identifier (defaults to function name)
- `scope`: `PRIVATE`, `SHARED`, or `SYSTEM`
- `timeout`: Default timeout in seconds (can be overridden via `TaskOptions`)
### TaskContext Methods
| Method | Description |
|--------|-------------|
| `update_task(progress, payload)` | Update progress and/or custom payload |
| `on_cleanup(handler)` | Register cleanup handler |
| `on_abort(handler)` | Register abort handler (makes task abortable) |
### TaskOptions
```python
TaskOptions(
task_key: str | None = None,
task_name: str | None = None,
timeout: int | None = None
)
```
- `task_key`: Deduplication key (also used as display name if `task_name` is not set)
- `task_name`: Human-readable display name for the Task List UI
- `timeout`: Timeout in seconds (overrides decorator default)
:::tip
Provide a descriptive `task_name` for better readability in the Task List UI. While `task_key` is used for deduplication and may be technical (e.g., `chart_export_123`), `task_name` can be user-friendly (e.g., `"Export Sales Chart 123"`).
:::
## Error Handling
Let exceptions propagate: the framework captures them automatically and sets task status to `FAILURE`:
```python
@task
def risky_task() -> None:
# No try/catch needed - framework handles it
result = operation_that_might_fail()
```
On failure, the framework records:
- `error_message`: Exception message
- `exception_type`: Exception class name
- `stack_trace`: Full traceback (visible when `SHOW_STACKTRACE=True`)
In the Task List UI, failed tasks show error details when hovering over the status. When stack traces are enabled, a separate bug icon appears in the **Details** column for viewing the full traceback.
Cleanup handlers still run after an exception, so resources can be properly released as necessary.
:::tip
Use descriptive exception messages. In environments where stack traces are hidden (`SHOW_STACKTRACE=False`), users see only the error message and exception type when hovering over failed tasks. Clear messages help users troubleshoot issues without administrator assistance.
:::

View File

@@ -47,5 +47,5 @@ This is a list of statements that describe how we do frontend development in Sup
- We do not debate code formatting style in PRs, instead relying on automated tooling to enforce it.
- If there's not a linting rule, we don't have a rule!
- See: [Linting How-Tos](../contributing/howtos#typescript--javascript)
- We use [React Storybook](https://storybook.js.org/) and [Applitools](https://applitools.com/) to help preview/test and stabilize our components
- We use [React Storybook](https://storybook.js.org/) to help preview/test and stabilize our components
- A public Storybook with components from the `master` branch is available [here](https://apache-superset.github.io/superset-ui/?path=/story/*)

View File

@@ -53,6 +53,7 @@ module.exports = {
'extensions/deployment',
'extensions/mcp',
'extensions/security',
'extensions/tasks',
'extensions/registry',
],
},

View File

@@ -60,7 +60,6 @@ Superset embraces a testing pyramid approach:
- **pytest**: Python testing framework with powerful fixtures and plugins
- **SQLAlchemy Test Utilities**: Database testing and transaction management
- **Flask Test Client**: API endpoint testing and request simulation
- **Factory Boy**: Test data generation and model factories
## Best Practices
@@ -157,7 +156,6 @@ npm run test:coverage
- **React Testing Library** - Component testing utilities
- **Playwright** - End-to-end testing (replacing Cypress)
- **Storybook** - Component development and testing
- **MSW** - API mocking for testing
---

View File

@@ -7,6 +7,12 @@ version: 1
# Caching
:::note
When a cache backend is configured, Superset expects it to remain available. Operations will
fail if the configured backend becomes unavailable rather than silently degrading. This
fail-fast behavior ensures operators are immediately aware of infrastructure issues.
:::
Superset uses [Flask-Caching](https://flask-caching.readthedocs.io/) for caching purposes.
Flask-Caching supports various caching backends, including Redis (recommended), Memcached,
SimpleCache (in-memory), or the local filesystem.
@@ -153,6 +159,84 @@ Then on configuration:
WEBDRIVER_AUTH_FUNC = auth_driver
```
## Signal Cache Backend
Superset supports an optional signal cache (`SIGNAL_CACHE_CONFIG`) for
high-performance distributed operations. This configuration enables:
- **Distributed locking**: Moves lock operations from the metadata database to Redis, improving
performance and reducing metastore load
- **Real-time event notifications**: Enables instant pub/sub messaging for task abort signals and
completion notifications instead of polling-based approaches
:::note
This requires Redis or Valkey specifically—it uses Redis-specific features (pub/sub, `SET NX EX`)
that are not available in general Flask-Caching backends.
:::
### Configuration
The signal cache uses Flask-Caching style configuration for consistency with other cache
backends. Configure `SIGNAL_CACHE_CONFIG` in `superset_config.py`:
```python
SIGNAL_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_REDIS_HOST": "localhost",
"CACHE_REDIS_PORT": 6379,
"CACHE_REDIS_DB": 0,
"CACHE_REDIS_PASSWORD": "", # Optional
}
```
For Redis Sentinel deployments:
```python
SIGNAL_CACHE_CONFIG = {
"CACHE_TYPE": "RedisSentinelCache",
"CACHE_REDIS_SENTINELS": [("sentinel1", 26379), ("sentinel2", 26379)],
"CACHE_REDIS_SENTINEL_MASTER": "mymaster",
"CACHE_REDIS_SENTINEL_PASSWORD": None, # Sentinel password (if different)
"CACHE_REDIS_PASSWORD": "", # Redis password
"CACHE_REDIS_DB": 0,
}
```
For SSL/TLS connections:
```python
SIGNAL_CACHE_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_REDIS_HOST": "redis.example.com",
"CACHE_REDIS_PORT": 6380,
"CACHE_REDIS_SSL": True,
"CACHE_REDIS_SSL_CERTFILE": "/path/to/client.crt",
"CACHE_REDIS_SSL_KEYFILE": "/path/to/client.key",
"CACHE_REDIS_SSL_CA_CERTS": "/path/to/ca.crt",
}
```
### Distributed Lock TTL
You can configure the default lock TTL (time-to-live) in seconds. Locks automatically expire after
this duration to prevent deadlocks from crashed processes:
```python
DISTRIBUTED_LOCK_DEFAULT_TTL = 30 # Default: 30 seconds
```
Individual lock acquisitions can override this value when needed.
### Database-Only Mode
When `SIGNAL_CACHE_CONFIG` is not configured, Superset uses database-backed operations:
- **Locking**: Uses the KeyValue table with periodic cleanup of expired entries
- **Event notifications**: Uses database polling instead of pub/sub
While database-backed operations work reliably, the Redis backend is recommended for production
deployments where low latency and reduced database load are important.
:::resources
- [Blog: The Data Engineer's Guide to Lightning-Fast Superset Dashboards](https://preset.io/blog/the-data-engineers-guide-to-lightning-fast-apache-superset-dashboards/)
- [Blog: Accelerating Dashboards with Materialized Views](https://preset.io/blog/accelerating-apache-superset-dashboards-with-materialized-views/)

View File

@@ -1,7 +1,63 @@
---
sidebar_position: 9
title: Frequently Asked Questions
description: Common questions about Apache Superset including performance, database support, visualizations, and configuration.
keywords: [superset faq, superset questions, superset help, data visualization faq]
---
import FAQSchema from '@site/src/components/FAQSchema';
<FAQSchema faqs={[
{
question: "How big of a dataset can Superset handle?",
answer: "Superset can work with even gigantic databases. Superset acts as a thin layer above your underlying databases or data engines, which do all the processing. Superset simply visualizes the results of the query. The key to achieving acceptable performance is whether your database can execute queries and return results at acceptable speed."
},
{
question: "What are the computing specifications required to run Superset?",
answer: "The specs depend on how many users you have and their activity, not on the size of your data. Community members have reported 8GB RAM, 2vCPUs as adequate for a moderately-sized instance. Monitor your resource usage and adjust as needed."
},
{
question: "Can I join or query multiple tables at one time?",
answer: "Not in the Explore or Visualization UI directly. A Superset SQLAlchemy datasource can only be a single table or a view. You can create a view that joins tables, or use SQL Lab where you can write SQL queries to join multiple tables."
},
{
question: "How do I create my own visualization?",
answer: "Read the instructions in the Creating Visualization Plugins documentation to learn how to build custom visualizations for Superset."
},
{
question: "Can I upload and visualize CSV data?",
answer: "Yes! Superset supports CSV upload functionality. Read the Exploring Data documentation to learn how to enable and use CSV upload."
},
{
question: "Why are my queries timing out?",
answer: "There are many possible causes. For SQL Lab, Superset allows queries to run up to 6 hours by default (configurable via SQLLAB_ASYNC_TIME_LIMIT_SEC). For dashboard timeouts, check your gateway/proxy timeout settings and adjust SUPERSET_WEBSERVER_TIMEOUT in superset_config.py."
},
{
question: "Why is the map not visible in the geospatial visualization?",
answer: "You need to register a free account at Mapbox.com, obtain an API key, and add it to your .env file at the key MAPBOX_API_KEY."
},
{
question: "What database engine can I use as a backend for Superset?",
answer: "Superset is tested using MySQL, PostgreSQL, and SQLite backends for storing its internal metadata. While Superset supports many databases as data sources, only these are recommended for the metadata store in production."
},
{
question: "Does Superset work with my database?",
answer: "Superset supports any database with a Python SQLAlchemy dialect and DBAPI driver. Check the Connecting to Databases documentation for the full list of supported databases."
},
{
question: "Does Superset offer a public API?",
answer: "Yes, Superset has a public REST API documented using Swagger. Enable FAB_API_SWAGGER_UI in superset_config.py to access interactive API documentation at /swagger/v1."
},
{
question: "Does Superset collect any telemetry data?",
answer: "Superset uses Scarf by default to collect basic telemetry data to help maintainers understand version usage. Users can opt out by setting the SCARF_ANALYTICS environment variable to false."
},
{
question: "Does Superset have a trash bin to recover deleted assets?",
answer: "No, there is no built-in way to recover deleted dashboards, charts, or datasets. It is recommended to take periodic backups of the metadata database and use export functionality for recovery."
}
]} />
# FAQ
## How big of a dataset can Superset handle?

View File

@@ -23,6 +23,7 @@ import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs';
import { themes } from 'prism-react-renderer';
import remarkImportPartial from 'remark-import-partial';
import remarkLocalizeBadges from './plugins/remark-localize-badges.mjs';
import remarkTechArticleSchema from './plugins/remark-tech-article-schema.mjs';
import * as fs from 'fs';
import * as path from 'path';
@@ -46,7 +47,7 @@ if (!versionsConfig.components.disabled) {
sidebarPath: require.resolve('./sidebarComponents.js'),
editUrl:
'https://github.com/apache/superset/edit/master/docs/components',
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
admonitions: {
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
extendDefaults: true,
@@ -74,7 +75,7 @@ if (!versionsConfig.developer_portal.disabled) {
sidebarPath: require.resolve('./sidebarTutorials.js'),
editUrl:
'https://github.com/apache/superset/edit/master/docs/developer_portal',
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
admonitions: {
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
extendDefaults: true,
@@ -180,6 +181,83 @@ const config: Config = {
favicon: '/img/favicon.ico',
organizationName: 'apache',
projectName: 'superset',
// SEO: Structured data (Organization, Software, WebSite with SearchAction)
headTags: [
// SoftwareApplication schema
{
tagName: 'script',
attributes: {
type: 'application/ld+json',
},
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Apache Superset',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Cross-platform',
description: 'Apache Superset is a modern, enterprise-ready business intelligence web application for data exploration and visualization.',
url: 'https://superset.apache.org',
license: 'https://www.apache.org/licenses/LICENSE-2.0',
author: {
'@type': 'Organization',
name: 'Apache Software Foundation',
url: 'https://www.apache.org/',
logo: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
},
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
featureList: [
'Interactive dashboards',
'SQL IDE',
'40+ visualization types',
'Semantic layer',
'Role-based access control',
'REST API',
],
}),
},
// WebSite schema with SearchAction (enables sitelinks search box in Google)
{
tagName: 'script',
attributes: {
type: 'application/ld+json',
},
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Apache Superset',
url: 'https://superset.apache.org',
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://superset.apache.org/search?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
}),
},
// Preconnect hints for faster external resource loading
{
tagName: 'link',
attributes: {
rel: 'preconnect',
href: 'https://WR5FASX5ED-dsn.algolia.net',
crossorigin: 'anonymous',
},
},
{
tagName: 'link',
attributes: {
rel: 'preconnect',
href: 'https://analytics.apache.org',
},
},
],
themes: [
'@saucelabs/theme-github-codeblock',
'@docusaurus/theme-mermaid',
@@ -212,6 +290,19 @@ const config: Config = {
},
},
],
// SEO: Generate robots.txt during build
[
require.resolve('./plugins/robots-txt-plugin.js'),
{
policies: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/v1/', '/_next/', '/static/js/*.map'],
},
],
},
],
[
'@docusaurus/plugin-client-redirects',
{
@@ -373,7 +464,7 @@ const config: Config = {
}
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
},
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
admonitions: {
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
extendDefaults: true,
@@ -396,11 +487,57 @@ const config: Config = {
theme: {
customCss: require.resolve('./src/styles/custom.css'),
},
// SEO: Sitemap configuration with priorities
sitemap: {
lastmod: 'date',
changefreq: 'weekly',
priority: 0.5,
ignorePatterns: ['/tags/**'],
filename: 'sitemap.xml',
createSitemapItems: async (params) => {
const { defaultCreateSitemapItems, ...rest } = params;
const items = await defaultCreateSitemapItems(rest);
return items.map((item) => {
// Boost priority for key pages
if (item.url.includes('/docs/intro')) {
return { ...item, priority: 1.0, changefreq: 'daily' };
}
if (item.url.includes('/docs/quickstart')) {
return { ...item, priority: 0.9, changefreq: 'weekly' };
}
if (item.url.includes('/docs/installation/')) {
return { ...item, priority: 0.8, changefreq: 'weekly' };
}
if (item.url.includes('/docs/databases')) {
return { ...item, priority: 0.8, changefreq: 'weekly' };
}
if (item.url.includes('/docs/faq')) {
return { ...item, priority: 0.7, changefreq: 'monthly' };
}
if (item.url === 'https://superset.apache.org/') {
return { ...item, priority: 1.0, changefreq: 'daily' };
}
return item;
});
},
},
} satisfies Options,
],
],
themeConfig: {
// SEO: OpenGraph and Twitter meta tags
metadata: [
{ name: 'keywords', content: 'data visualization, business intelligence, BI, dashboards, SQL, analytics, open source, Apache, charts, reporting' },
{ property: 'og:type', content: 'website' },
{ property: 'og:site_name', content: 'Apache Superset' },
{ property: 'og:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
{ property: 'og:image:width', content: '1200' },
{ property: 'og:image:height', content: '630' },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
{ name: 'twitter:site', content: '@ApacheSuperset' },
],
colorMode: {
defaultMode: 'dark',
disableSwitch: false,
@@ -499,7 +636,6 @@ const config: Config = {
copyright: `
<div class="footer__ci-services">
<span>CI powered by</span>
<a href="https://applitools.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/applitools.png" alt="Applitools" title="Applitools - Visual Testing" /></a>
<a href="https://www.netlify.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/netlify.png" alt="Netlify" title="Netlify - Deploy Previews" /></a>
</div>
<p>Copyright © ${new Date().getFullYear()},

View File

@@ -1,6 +1,6 @@
{
"copyright": {
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://applitools.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/applitools.png\" alt=\"Applitools\" title=\"Applitools - Visual Testing\" /></a>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a>&nbsp;|&nbsp;\n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a>&nbsp;|&nbsp;\n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a>&nbsp;|&nbsp;\n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a>&nbsp;|&nbsp;\n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
"description": "The footer copyright"
}
}

View File

@@ -64,7 +64,7 @@
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.11",
"antd": "^6.2.3",
"antd": "^6.3.0",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001769",
"docusaurus-plugin-openapi-docs": "^4.6.0",
@@ -94,7 +94,7 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"@typescript-eslint/parser": "^8.55.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
@@ -102,8 +102,8 @@
"globals": "^17.3.0",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.54.0",
"webpack": "^5.105.0"
"typescript-eslint": "^8.55.0",
"webpack": "^5.105.1"
},
"browserslist": {
"production": [

View File

@@ -0,0 +1,153 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Note: visit from unist-util-visit is available if needed for tree traversal
/**
* Remark plugin that automatically injects TechArticle schema import and component
* into documentation MDX files based on frontmatter.
*
* This enables rich snippets for technical documentation in search results.
*
* Frontmatter options:
* - title: (required) Article headline
* - description: (required) Article description
* - keywords: (optional) Array of keywords
* - seo_proficiency: (optional) 'Beginner' or 'Expert', defaults to 'Beginner'
* - seo_schema: (optional) Set to false to disable schema injection
*/
export default function remarkTechArticleSchema() {
return (tree, file) => {
const frontmatter = file.data.frontMatter || {};
// Skip if explicitly disabled or missing required fields
if (frontmatter.seo_schema === false) {
return;
}
// Only add schema if we have title and description
if (!frontmatter.title || !frontmatter.description) {
return;
}
const title = frontmatter.title;
const description = frontmatter.description;
const keywords = Array.isArray(frontmatter.keywords) ? frontmatter.keywords : [];
const proficiencyLevel = frontmatter.seo_proficiency || 'Beginner';
// Create the import statement
const importNode = {
type: 'mdxjsEsm',
value: `import TechArticleSchema from '@site/src/components/TechArticleSchema';`,
data: {
estree: {
type: 'Program',
sourceType: 'module',
body: [
{
type: 'ImportDeclaration',
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: 'TechArticleSchema' },
},
],
source: {
type: 'Literal',
value: '@site/src/components/TechArticleSchema',
},
},
],
},
},
};
// Create the component node for MDX
const componentNode = {
type: 'mdxJsxFlowElement',
name: 'TechArticleSchema',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'title',
value: title,
},
{
type: 'mdxJsxAttribute',
name: 'description',
value: description,
},
...(keywords.length > 0
? [
{
type: 'mdxJsxAttribute',
name: 'keywords',
value: {
type: 'mdxJsxAttributeValueExpression',
value: JSON.stringify(keywords),
data: {
estree: {
type: 'Program',
sourceType: 'module',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: keywords.map((k) => ({
type: 'Literal',
value: k,
})),
},
},
],
},
},
},
},
]
: []),
...(proficiencyLevel !== 'Beginner'
? [
{
type: 'mdxJsxAttribute',
name: 'proficiencyLevel',
value: proficiencyLevel,
},
]
: []),
],
children: [],
};
// Insert import at the beginning
tree.children.unshift(importNode);
// Find the first heading and insert component after it
let insertIndex = 1; // Default: after import
for (let i = 1; i < tree.children.length; i++) {
if (tree.children[i].type === 'heading') {
insertIndex = i + 1;
break;
}
}
tree.children.splice(insertIndex, 0, componentNode);
};
}

View File

@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
const path = require('path');
/* eslint-enable @typescript-eslint/no-require-imports */
/**
* Docusaurus plugin to generate robots.txt during build
* Configuration is passed via plugin options
*/
module.exports = function robotsTxtPlugin(context, options = {}) {
const { siteConfig } = context;
const {
policies = [{ userAgent: '*', allow: '/' }],
additionalSitemaps = [],
} = options;
return {
name: 'robots-txt-plugin',
async postBuild({ outDir }) {
const sitemapUrl = `${siteConfig.url}/sitemap.xml`;
// Build robots.txt content
const lines = [];
// Add policies
for (const policy of policies) {
lines.push(`User-agent: ${policy.userAgent}`);
if (policy.allow) {
const allows = Array.isArray(policy.allow) ? policy.allow : [policy.allow];
for (const allow of allows) {
lines.push(`Allow: ${allow}`);
}
}
if (policy.disallow) {
const disallows = Array.isArray(policy.disallow) ? policy.disallow : [policy.disallow];
for (const disallow of disallows) {
lines.push(`Disallow: ${disallow}`);
}
}
if (policy.crawlDelay) {
lines.push(`Crawl-delay: ${policy.crawlDelay}`);
}
lines.push(''); // Empty line between policies
}
// Add sitemaps
lines.push(`Sitemap: ${sitemapUrl}`);
for (const sitemap of additionalSitemaps) {
lines.push(`Sitemap: ${sitemap}`);
}
// Write robots.txt
const robotsPath = path.join(outDir, 'robots.txt');
fs.writeFileSync(robotsPath, lines.join('\n'));
console.log('Generated robots.txt');
},
};
};

View File

@@ -97,6 +97,7 @@ const sidebars = {
'extensions/deployment',
'extensions/mcp',
'extensions/security',
'extensions/tasks',
'extensions/registry',
],
},

View File

@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { JSX } from 'react';
import Head from '@docusaurus/Head';
interface FAQItem {
question: string;
answer: string;
}
interface FAQSchemaProps {
faqs: FAQItem[];
}
/**
* Component that injects FAQPage JSON-LD structured data
* Use this on FAQ pages to enable rich snippets in search results
*
* @example
* <FAQSchema faqs={[
* { question: "What is Superset?", answer: "Apache Superset is..." },
* { question: "How do I install it?", answer: "You can install via..." }
* ]} />
*/
export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null {
// FAQPage schema requires a non-empty mainEntity array per schema.org specs
if (!faqs || faqs.length === 0) {
return null;
}
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
};
return (
<Head>
<script type="application/ld+json">{JSON.stringify(schema)}</script>
</Head>
);
}

View File

@@ -0,0 +1,91 @@
/**
* 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 type { JSX } from 'react';
import Head from '@docusaurus/Head';
import { useLocation } from '@docusaurus/router';
interface TechArticleSchemaProps {
title: string;
description: string;
datePublished?: string;
dateModified?: string;
keywords?: string[];
proficiencyLevel?: 'Beginner' | 'Expert';
}
/**
* Component that injects TechArticle JSON-LD structured data for documentation pages.
* This helps search engines understand technical documentation content.
*
* @example
* <TechArticleSchema
* title="Installing Superset with Docker"
* description="Learn how to install Apache Superset using Docker Compose"
* keywords={['docker', 'installation', 'superset']}
* proficiencyLevel="Beginner"
* />
*/
export default function TechArticleSchema({
title,
description,
datePublished,
dateModified,
keywords = [],
proficiencyLevel = 'Beginner',
}: TechArticleSchemaProps): JSX.Element {
const location = useLocation();
const url = `https://superset.apache.org${location.pathname}`;
const schema = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description,
url,
proficiencyLevel,
author: {
'@type': 'Organization',
name: 'Apache Superset Contributors',
url: 'https://github.com/apache/superset/graphs/contributors',
},
publisher: {
'@type': 'Organization',
name: 'Apache Software Foundation',
url: 'https://www.apache.org/',
logo: {
'@type': 'ImageObject',
url: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
...(datePublished && { datePublished }),
...(dateModified && { dateModified }),
...(keywords.length > 0 && { keywords: keywords.join(', ') }),
};
return (
<Head>
<script type="application/ld+json">{JSON.stringify(schema)}</script>
</Head>
);
}

View File

@@ -579,7 +579,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
columns={columns}
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
pagination={{
pageSize: 20,
defaultPageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} databases`,
}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/static/img/superset-og-image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -195,19 +195,19 @@
dependencies:
"@ant-design/fast-color" "^3.0.0"
"@ant-design/cssinjs-utils@^2.0.2":
version "2.0.2"
resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz"
integrity sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==
"@ant-design/cssinjs-utils@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.1.tgz#c70d86206204e882073a0fe4969a5ddf154c6915"
integrity sha512-RKxkj5pGFB+FkPJ5NGhoX3DK3xsv0pMltha7Ei1AnY3tILeq38L7tuhaWDPQI/5nlPxOog44wvqpNyyGcUsNMg==
dependencies:
"@ant-design/cssinjs" "^2.0.1"
"@ant-design/cssinjs" "^2.1.0"
"@babel/runtime" "^7.23.2"
"@rc-component/util" "^1.4.0"
"@ant-design/cssinjs@^2.0.1", "@ant-design/cssinjs@^2.0.3":
version "2.0.3"
resolved "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.3.tgz"
integrity sha512-HAo8SZ3a6G8v6jT0suCz1270na6EA3obeJWM4uzRijBhdwdoMAXWK2f4WWkwB28yUufsfk3CAhN1coGPQq4kNQ==
"@ant-design/cssinjs@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-2.1.0.tgz#081394937f86aefe55e35198019d0483f405a484"
integrity sha512-eZFrPCnrYrF3XtL7qA4L75P0qA3TtZta8H3Yggy7UYFh8gZgu5bSMNF+v4UVCzGxzYmx8ZvPdgOce0BJ6PsW9g==
dependencies:
"@babel/runtime" "^7.11.1"
"@emotion/hash" "^0.8.0"
@@ -2842,20 +2842,20 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.11.0":
version "1.11.0"
resolved "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.11.0.tgz"
integrity sha512-VDiEsskThWi8l0/1Nquc9I4ytcMKQYAb9Jkm6wiX5O5fpcMRsm+b8OulBMbr/b4rFTl/2y2y4GdKqQ+2whD+XQ==
"@rc-component/cascader@~1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
dependencies:
"@rc-component/select" "~1.5.0"
"@rc-component/tree" "~1.1.0"
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/checkbox@~1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz"
integrity sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==
"@rc-component/checkbox@~2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@rc-component/checkbox/-/checkbox-2.0.0.tgz#90e0b30c276e507a5ab9942f626fe4572f5e4ff9"
integrity sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==
dependencies:
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
@@ -2870,10 +2870,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/color-picker@~3.0.3":
version "3.0.3"
resolved "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz"
integrity sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==
"@rc-component/color-picker@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rc-component/color-picker/-/color-picker-3.1.0.tgz#437586ea2fc27862e7429a754cf85e519e05f461"
integrity sha512-o7Vavj7yyfVxFmeynXf0fCHVlC0UTE9al74c6nYuLck+gjuVdQNWSVXR8Efq/mmWFy7891SCOsfaPq6Eqe1s/g==
dependencies:
"@ant-design/fast-color" "^3.0.0"
"@rc-component/util" "^1.3.0"
@@ -2886,24 +2886,24 @@
dependencies:
"@rc-component/util" "^1.3.0"
"@rc-component/dialog@~1.8.2":
version "1.8.3"
resolved "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.3.tgz"
integrity sha512-ek1ai9PYSyQ9/3RW6jE62lB8rGb9Qw/NTC8/03HbLbosNboknqTuyppbBvUwJFLWeW307F7xBQ6IJJV4OWWmXw==
"@rc-component/dialog@~1.8.4":
version "1.8.4"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
dependencies:
"@rc-component/motion" "^1.1.3"
"@rc-component/portal" "^2.1.0"
"@rc-component/util" "^1.7.0"
"@rc-component/util" "^1.9.0"
clsx "^2.1.1"
"@rc-component/drawer@~1.4.1":
version "1.4.1"
resolved "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.1.tgz"
integrity sha512-kNJQie/QjJO5wGeWrZQwSGeuo8staxXx1nYN+dpK2UY7i8teo5PQdZ6ukKSnnW9vmPXsLn3F5nKYRbf43e8+5g==
"@rc-component/drawer@~1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@rc-component/drawer/-/drawer-1.4.2.tgz#eb13a6556fb67be28407295c89401b01549224e0"
integrity sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.1.3"
"@rc-component/util" "^1.7.0"
"@rc-component/util" "^1.9.0"
clsx "^2.1.1"
"@rc-component/dropdown@~1.0.0", "@rc-component/dropdown@~1.0.2":
@@ -3082,21 +3082,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.5.0":
version "1.5.1"
resolved "https://registry.npmjs.org/@rc-component/select/-/select-1.5.1.tgz"
integrity sha512-ARXtwfCVnpDJj1bQjh1cimUlNQkZiN72hvtL2G4mKXIYfkokYdA2Vyu2deAfY7kuHSWpmZygVuohQt6TxOYjnA==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.3.0"
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/select@~1.5.2":
version "1.5.2"
resolved "https://registry.npmjs.org/@rc-component/select/-/select-1.5.2.tgz"
integrity sha512-7wqD5D4I2+fc5XoB4nzDDK656QPlDnFAUaxLljkU1wwSpi4+MZxndv9vgg7NQfveuuf0/ilUdOjuPg7NPl7Mmg==
"@rc-component/select@~1.6.0", "@rc-component/select@~1.6.5":
version "1.6.5"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.6.5.tgz#69276239c6ac0884a67597961b0224c4ad0bc4ca"
integrity sha512-Cx+/OYEorXlPQ6ZFDro3HbalPZLlJWagvGtl8DGYO4losXM6gw43qbsxWqU1c3XOQVIOUDBlr7dSksSNMj8kXg==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3180,23 +3169,23 @@
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.6.0.tgz"
integrity sha512-UvEGmZT+gcVvRwImAZg3/sXw9nUdn4FmCs1rSIMWjEXEIAo0dTGmIyWuLCvs+1rGe9AZ7CHMPiQUEbdadwV0fw==
"@rc-component/tree-select@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
dependencies:
"@rc-component/select" "~1.5.0"
"@rc-component/tree" "~1.1.0"
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/tree@~1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz"
integrity sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.3.tgz#a70ae847a768763f4f461375620c1feccfcc933a"
integrity sha512-mG8hF2ogQcKaEpfyxzPvMWqqkptofd7Sf+YiXOpPzuXLTLwNKfLDJtysc1/oybopbnzxNqWh2Vgwi+GYwNIb7w==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.2.1"
"@rc-component/util" "^1.8.1"
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
@@ -3219,18 +3208,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@rc-component/util/-/util-1.7.0.tgz"
integrity sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
"@rc-component/util@^1.8.1":
version "1.8.2"
resolved "https://registry.npmjs.org/@rc-component/util/-/util-1.8.2.tgz"
integrity sha512-mx0F1VSqatbucxAcM2+/d4MAGJv/CpXxM9sAebUJkrgQLHsffY97o6gFx8cy+vqVTT/Hr8/6A8j3IYp6n5754g==
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.9.0.tgz#ec5fe657a98554f26ef761345ca4b745be00af0e"
integrity sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -4735,100 +4716,100 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.54.0", "@typescript-eslint/eslint-plugin@^8.52.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz"
integrity sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==
"@typescript-eslint/eslint-plugin@8.55.0", "@typescript-eslint/eslint-plugin@^8.52.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz#086d2ef661507b561f7b17f62d3179d692a0765f"
integrity sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.54.0"
"@typescript-eslint/type-utils" "8.54.0"
"@typescript-eslint/utils" "8.54.0"
"@typescript-eslint/visitor-keys" "8.54.0"
"@typescript-eslint/scope-manager" "8.55.0"
"@typescript-eslint/type-utils" "8.55.0"
"@typescript-eslint/utils" "8.55.0"
"@typescript-eslint/visitor-keys" "8.55.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.4.0"
"@typescript-eslint/parser@8.54.0", "@typescript-eslint/parser@^8.52.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz"
integrity sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==
"@typescript-eslint/parser@8.55.0", "@typescript-eslint/parser@^8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.55.0.tgz#6eace4e9e95f178d3447ed1f17f3d6a5dfdb345c"
integrity sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==
dependencies:
"@typescript-eslint/scope-manager" "8.54.0"
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/typescript-estree" "8.54.0"
"@typescript-eslint/visitor-keys" "8.54.0"
"@typescript-eslint/scope-manager" "8.55.0"
"@typescript-eslint/types" "8.55.0"
"@typescript-eslint/typescript-estree" "8.55.0"
"@typescript-eslint/visitor-keys" "8.55.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz"
integrity sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==
"@typescript-eslint/project-service@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.55.0.tgz#b8a71c06a625bdad481c24d5614b68e252f3ae9b"
integrity sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.54.0"
"@typescript-eslint/types" "^8.54.0"
"@typescript-eslint/tsconfig-utils" "^8.55.0"
"@typescript-eslint/types" "^8.55.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz"
integrity sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==
"@typescript-eslint/scope-manager@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz#8a0752c31c788651840dc98f840b0c2ebe143b8c"
integrity sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==
dependencies:
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/visitor-keys" "8.54.0"
"@typescript-eslint/types" "8.55.0"
"@typescript-eslint/visitor-keys" "8.55.0"
"@typescript-eslint/tsconfig-utils@8.54.0", "@typescript-eslint/tsconfig-utils@^8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz"
integrity sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==
"@typescript-eslint/tsconfig-utils@8.55.0", "@typescript-eslint/tsconfig-utils@^8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz#62f1d005419985e09d37a040b2f1450e4e805afa"
integrity sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==
"@typescript-eslint/type-utils@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz"
integrity sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==
"@typescript-eslint/type-utils@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz#195d854b3e56308ce475fdea2165313bb1190200"
integrity sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==
dependencies:
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/typescript-estree" "8.54.0"
"@typescript-eslint/utils" "8.54.0"
"@typescript-eslint/types" "8.55.0"
"@typescript-eslint/typescript-estree" "8.55.0"
"@typescript-eslint/utils" "8.55.0"
debug "^4.4.3"
ts-api-utils "^2.4.0"
"@typescript-eslint/types@8.54.0", "@typescript-eslint/types@^8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz"
integrity sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==
"@typescript-eslint/types@8.55.0", "@typescript-eslint/types@^8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.55.0.tgz#8449c5a7adac61184cac92dbf6315733569708c2"
integrity sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==
"@typescript-eslint/typescript-estree@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz"
integrity sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==
"@typescript-eslint/typescript-estree@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz#c83ac92c11ce79bedd984937c7780a65e7f7b2e3"
integrity sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==
dependencies:
"@typescript-eslint/project-service" "8.54.0"
"@typescript-eslint/tsconfig-utils" "8.54.0"
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/visitor-keys" "8.54.0"
"@typescript-eslint/project-service" "8.55.0"
"@typescript-eslint/tsconfig-utils" "8.55.0"
"@typescript-eslint/types" "8.55.0"
"@typescript-eslint/visitor-keys" "8.55.0"
debug "^4.4.3"
minimatch "^9.0.5"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.4.0"
"@typescript-eslint/utils@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz"
integrity sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==
"@typescript-eslint/utils@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.55.0.tgz#c1744d94a3901deb01f58b09d3478d811f96d619"
integrity sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.54.0"
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/typescript-estree" "8.54.0"
"@typescript-eslint/scope-manager" "8.55.0"
"@typescript-eslint/types" "8.55.0"
"@typescript-eslint/typescript-estree" "8.55.0"
"@typescript-eslint/visitor-keys@8.54.0":
version "8.54.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz"
integrity sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==
"@typescript-eslint/visitor-keys@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz#3d9a40fd4e3705c63d8fae3af58988add3ed464d"
integrity sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==
dependencies:
"@typescript-eslint/types" "8.54.0"
"@typescript-eslint/types" "8.55.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -5182,24 +5163,24 @@ ansi-styles@^6.1.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^6.2.3:
version "6.2.3"
resolved "https://registry.npmjs.org/antd/-/antd-6.2.3.tgz"
integrity sha512-q92r7/hcQAR2iv6CCysdz7c2Pdl/3nhslc3azF9e6AEl4knO6v+nlaeor1oF2jBanZ/tiw2m3NprOVUgPDvyhg==
antd@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.0.tgz#0ea0596d2d8c19cb5860e3fa6ad20128f6c8ffda"
integrity sha512-bbHJcASrRHp02wTpr940KtUHlTT6tvmaD4OAjqgOJXNmTQ/+qBDdBVWY/yeDV41p/WbWjTLlaqRGVbL3UEVpNw==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.0.3"
"@ant-design/cssinjs-utils" "^2.0.2"
"@ant-design/cssinjs" "^2.1.0"
"@ant-design/cssinjs-utils" "^2.1.1"
"@ant-design/fast-color" "^3.0.1"
"@ant-design/icons" "^6.1.0"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.28.4"
"@rc-component/cascader" "~1.11.0"
"@rc-component/checkbox" "~1.0.1"
"@rc-component/cascader" "~1.14.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.0.3"
"@rc-component/dialog" "~1.8.2"
"@rc-component/drawer" "~1.4.1"
"@rc-component/color-picker" "~3.1.0"
"@rc-component/dialog" "~1.8.4"
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.6.2"
"@rc-component/image" "~1.6.0"
@@ -5217,7 +5198,7 @@ antd@^6.2.3:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.5.2"
"@rc-component/select" "~1.6.5"
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
@@ -5226,11 +5207,11 @@ antd@^6.2.3:
"@rc-component/textarea" "~1.1.2"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.3.0"
"@rc-component/tree" "~1.1.0"
"@rc-component/tree-select" "~1.6.0"
"@rc-component/tree" "~1.2.3"
"@rc-component/tree-select" "~1.8.0"
"@rc-component/trigger" "^3.9.0"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.8.1"
"@rc-component/util" "^1.9.0"
clsx "^2.1.1"
dayjs "^1.11.11"
scroll-into-view-if-needed "^3.1.0"
@@ -5418,12 +5399,12 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.12.2:
version "1.12.2"
resolved "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz"
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
version "1.13.5"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43"
integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
follow-redirects "^1.15.11"
form-data "^4.0.5"
proxy-from-env "^1.1.0"
babel-loader@^9.2.1:
@@ -7207,14 +7188,6 @@ encodeurl@~2.0.0:
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.17.4:
version "5.18.4"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz"
integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
enhanced-resolve@^5.19.0:
version "5.19.0"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
@@ -7973,7 +7946,7 @@ flatted@^3.2.9:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
follow-redirects@^1.0.0, follow-redirects@^1.15.6:
follow-redirects@^1.0.0, follow-redirects@^1.15.11:
version "1.15.11"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
@@ -7995,10 +7968,10 @@ form-data-encoder@^2.1.2:
resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
form-data@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
@@ -14099,7 +14072,7 @@ synckit@^0.11.12:
dependencies:
"@pkgr/core" "^0.2.9"
tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
@@ -14396,15 +14369,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.54.0:
version "8.54.0"
resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz"
integrity sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==
typescript-eslint@^8.55.0:
version "8.55.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.55.0.tgz#abae8295c5f0f82f816218113a46e89bc30c3de2"
integrity sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.54.0"
"@typescript-eslint/parser" "8.54.0"
"@typescript-eslint/typescript-estree" "8.54.0"
"@typescript-eslint/utils" "8.54.0"
"@typescript-eslint/eslint-plugin" "8.55.0"
"@typescript-eslint/parser" "8.55.0"
"@typescript-eslint/typescript-estree" "8.55.0"
"@typescript-eslint/utils" "8.55.0"
typescript@~5.9.3:
version "5.9.3"
@@ -14887,14 +14860,6 @@ warning@^4.0.3:
dependencies:
loose-envify "^1.0.0"
watchpack@^2.4.4:
version "2.4.4"
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz"
integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
watchpack@^2.5.1:
version "2.5.1"
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz"
@@ -15022,10 +14987,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.105.0:
version "5.105.0"
resolved "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz"
integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==
webpack@^5.105.1, webpack@^5.88.1, webpack@^5.95.0:
version "5.105.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.1.tgz#c05cb3621196c76fa3b3a9bea446d14616b83778"
integrity sha512-Gdj3X74CLJJ8zy4URmK42W7wTZUJrqL+z8nyGEr4dTN0kb3nVs+ZvjbTOqRYPD7qX4tUmwyHL9Q9K6T1seW6Yw==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -15053,37 +15018,6 @@ webpack@^5.105.0:
watchpack "^2.5.1"
webpack-sources "^3.3.3"
webpack@^5.88.1, webpack@^5.95.0:
version "5.104.1"
resolved "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz"
integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.4"
es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
graceful-fs "^4.2.11"
json-parse-even-better-errors "^2.3.1"
loader-runner "^4.3.1"
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.3.16"
watchpack "^2.4.4"
webpack-sources "^3.3.3"
webpackbar@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz"

View File

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

View File

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

View File

@@ -312,6 +312,12 @@ supersetNode:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Launch additional containers into supersetNode pod
extraContainers: []
@@ -410,6 +416,12 @@ supersetWorker:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Launch additional containers into supersetWorker pod
extraContainers: []
# -- Annotations to be added to supersetWorker deployment
@@ -492,6 +504,12 @@ supersetCeleryBeat:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Launch additional containers into supersetCeleryBeat pods
extraContainers: []
# -- Annotations to be added to supersetCeleryBeat deployment
@@ -585,6 +603,12 @@ supersetCeleryFlower:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- Launch additional containers into supersetCeleryFlower pods
extraContainers: []
# -- Annotations to be added to supersetCeleryFlower deployment
@@ -749,6 +773,12 @@ init:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
resources:
limits:
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
# -- A Superset init script
# @default -- a script to create admin user and initialize roles
initscript: |-

View File

@@ -99,8 +99,8 @@ dependencies = [
"simplejson>=3.15.0",
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.3, <0.39",
"sqlglot>=27.15.2, <28",
"sqlalchemy-utils>=0.42.0, <0.43",
"sqlglot>=28.10.0, <29",
# newer pandas needs 0.9+
"tabulate>=0.9.0, <1.0",
"typing-extensions>=4, <5",
@@ -141,7 +141,7 @@ druid = ["pydruid>=0.6.5,<0.7"]
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.9, <0.3.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = ["fastmcp==2.14.3"]

View File

@@ -399,12 +399,12 @@ sqlalchemy==1.4.54
# marshmallow-sqlalchemy
# shillelagh
# sqlalchemy-utils
sqlalchemy-utils==0.38.3
sqlalchemy-utils==0.42.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core
# flask-appbuilder
sqlglot==27.15.2
sqlglot==28.10.0
# via
# apache-superset (pyproject.toml)
# apache-superset-core

View File

@@ -990,13 +990,13 @@ sqlalchemy==1.4.54
# sqlalchemy-utils
sqlalchemy-bigquery==1.15.0
# via apache-superset
sqlalchemy-utils==0.38.3
sqlalchemy-utils==0.42.0
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
# flask-appbuilder
sqlglot==27.15.2
sqlglot==28.10.0
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.0.1rc3"
version = "0.0.1rc4"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [
@@ -45,8 +45,8 @@ dependencies = [
"flask-appbuilder>=5.0.2,<6",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy-utils>=0.38.0",
"sqlglot>=27.15.2, <28",
"sqlalchemy-utils>=0.42.0",
"sqlglot>=28.10.0, <29",
"typing-extensions>=4.0.0",
]

View File

@@ -46,6 +46,7 @@ from superset_core.api.models import (
Query,
SavedQuery,
Tag,
Task,
User,
)
@@ -248,6 +249,48 @@ class KeyValueDAO(BaseDAO[KeyValue]):
id_column_name = "id"
class TaskDAO(BaseDAO[Task]):
"""
Abstract Task DAO interface.
Host implementations will replace this class during initialization
with a concrete implementation providing actual functionality.
"""
# Class variables that will be set by host implementation
model_cls = None
base_filter = None
id_column_name = "id"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def find_by_task_key(
cls,
task_type: str,
task_key: str,
scope: str = "private",
user_id: int | None = None,
) -> Task | None:
"""
Find active task by type, key, scope, and user.
Uses dedup_key internally for efficient querying with a unique index.
Only returns tasks that are active (pending or in progress).
Uniqueness logic by scope:
- private: scope + task_type + task_key + user_id
- shared/system: scope + task_type + task_key (user-agnostic)
:param task_type: Task type to filter by
:param task_key: Task identifier for deduplication
:param scope: Task scope (private/shared/system)
:param user_id: User ID (required for private tasks)
:returns: Task instance or None if not found or not active
"""
...
__all__ = [
"BaseDAO",
"DatasetDAO",
@@ -259,4 +302,5 @@ __all__ = [
"SavedQueryDAO",
"TagDAO",
"KeyValueDAO",
"TaskDAO",
]

View File

@@ -40,6 +40,7 @@ from flask_appbuilder import Model
from sqlalchemy.orm import scoped_session
if TYPE_CHECKING:
from superset_core.api.tasks import TaskProperties
from superset_core.api.types import (
AsyncQueryHandle,
QueryOptions,
@@ -361,6 +362,132 @@ class KeyValue(CoreModel):
changed_by_fk: int | None
class Task(CoreModel):
"""
Abstract Task model interface.
Host implementations will replace this class during initialization
with concrete implementation providing actual functionality.
This model represents async tasks in the Global Task Framework (GTF).
Non-filterable fields (progress, error info, execution config) are stored
in a `properties` JSON blob for schema flexibility.
"""
__abstract__ = True
# Type hints for expected column attributes
id: int
uuid: UUID
task_key: str # For deduplication
task_type: str # e.g., 'sql_execution'
task_name: str | None # Human readable name
scope: str # private/shared/system
status: str
dedup_key: str # Computed deduplication key
# Timestamps (from AuditMixinNullable)
created_on: datetime | None
changed_on: datetime | None
started_at: datetime | None
ended_at: datetime | None
# User context
created_by_fk: int | None
user_id: int | None
# Task output data
payload: str # JSON serialized task output data
def get_payload(self) -> dict[str, Any]:
"""
Get payload as parsed JSON.
Payload contains task-specific output data set by task code.
Host implementations will replace this method during initialization
with concrete implementation providing actual functionality.
:returns: Dictionary containing payload data
"""
raise NotImplementedError("Method will be replaced during initialization")
def set_payload(self, data: dict[str, Any]) -> None:
"""
Update payload with new data (merges with existing).
Host implementations will replace this method during initialization
with concrete implementation providing actual functionality.
:param data: Dictionary of data to merge into payload
"""
raise NotImplementedError("Method will be replaced during initialization")
@property
def properties(self) -> Any:
"""
Get typed properties (runtime state and execution config).
Properties contain:
- is_abortable: bool | None - has abort handler registered
- progress_percent: float | None - progress 0.0-1.0
- progress_current: int | None - current iteration count
- progress_total: int | None - total iterations
- error_message: str | None - human-readable error message
- exception_type: str | None - exception class name
- stack_trace: str | None - full formatted traceback
- timeout: int | None - timeout in seconds
Host implementations will replace this property during initialization.
:returns: TaskProperties dataclass instance
"""
raise NotImplementedError("Property will be replaced during initialization")
def update_properties(self, updates: "TaskProperties") -> None:
"""
Update specific properties fields (merge semantics).
Only updates fields present in the updates dict.
Host implementations will replace this method during initialization.
:param updates: TaskProperties dict with fields to update
Example:
task.update_properties({"is_abortable": True})
"""
raise NotImplementedError("Method will be replaced during initialization")
class TaskSubscriber(CoreModel):
"""
Abstract TaskSubscriber model interface.
Host implementations will replace this class during initialization
with concrete implementation providing actual functionality.
This model tracks task subscriptions for multi-user shared tasks. When a user
schedules a shared task with the same parameters as an existing task,
they are subscribed to that task instead of creating a duplicate.
"""
__abstract__ = True
# Type hints for expected attributes (no actual field definitions)
id: int
task_id: int
user_id: int
subscribed_at: datetime
# Audit fields from AuditMixinNullable
created_on: datetime | None
changed_on: datetime | None
created_by_fk: int | None
changed_by_fk: int | None
def get_session() -> scoped_session:
"""
Retrieve the SQLAlchemy session to directly interface with the
@@ -384,6 +511,8 @@ __all__ = [
"SavedQuery",
"Tag",
"KeyValue",
"Task",
"TaskSubscriber",
"CoreModel",
"get_session",
]

View File

@@ -0,0 +1,361 @@
# 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 __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any, Callable, Generic, Literal, ParamSpec, TypedDict, TypeVar
from superset_core.api.models import Task
P = ParamSpec("P")
R = TypeVar("R")
class TaskStatus(str, Enum):
"""
Status of task execution.
"""
PENDING = "pending"
IN_PROGRESS = "in_progress"
SUCCESS = "success"
FAILURE = "failure"
ABORTING = "aborting" # Abort/timeout requested, handlers running
ABORTED = "aborted" # User/admin cancelled
TIMED_OUT = "timed_out" # Timeout expired
class TaskScope(str, Enum):
"""
Scope of task visibility and access control.
"""
PRIVATE = "private" # User-specific tasks (default)
SHARED = "shared" # Multi-user collaborative tasks
SYSTEM = "system" # Admin-only background tasks
class TaskProperties(TypedDict, total=False):
"""
TypedDict for task runtime state and execution config.
Stored as JSON in the database, accessed as a dict throughout the codebase.
All fields are optional (total=False) - only set keys are present in the dict.
Usage:
# Reading - always use .get() since keys may not be present
if task.properties.get("is_abortable"):
...
# Writing/updating - only include keys you want to set
task.update_properties({"is_abortable": True, "progress_percent": 0.5})
Notes:
- Sparse dict: only keys that are explicitly set are present
- Unknown keys from JSON are preserved (forward compatibility)
- Always use .get() for reads since keys may be absent
"""
# Execution config - set at task creation
execution_mode: Literal["async", "sync"]
timeout: int
# Runtime state - set by framework during execution
is_abortable: bool
progress_percent: float
progress_current: int
progress_total: int
# Error info - set when task fails
error_message: str
exception_type: str
stack_trace: str
@dataclass(frozen=True)
class TaskOptions:
"""
Execution metadata for tasks.
NOTE: This is intentionally minimal for the initial implementation.
Additional options (queue, priority, run_at, delay_s,
max_retries, retry_backoff_s, tags, etc.) can be added later when needed.
Future enhancements will include:
- Validation (e.g., run_at vs delay_s mutual exclusion)
- Queue routing and priority management
- Retry policies and backoff strategies
Example:
from superset_core.api.tasks import TaskOptions, TaskScope
# Private task (default)
task = my_task.schedule(arg1)
# Custom task with deduplication
task = my_task.schedule(
arg1,
options=TaskOptions(
task_key="custom_key",
task_name="Custom Task Name"
)
)
# Task with custom name
task = admin_task.schedule(
options=TaskOptions(task_name="Admin Operation")
)
# Task with timeout (overrides decorator default)
task = long_task.schedule(
options=TaskOptions(timeout=600) # 10 minute timeout
)
"""
task_key: str | None = None
task_name: str | None = None
timeout: int | None = None # Timeout in seconds
class TaskContext(ABC):
"""
Abstract task context for write-only task state updates.
Tasks use this context to update their state (progress, payload) and
check for cancellation. Tasks should not need to read their own state -
they are the source of state, not consumers of it.
Host implementations will replace this abstract class during initialization
with a concrete implementation providing actual functionality.
"""
@abstractmethod
def update_task(
self,
progress: float | int | tuple[int, int] | None = None,
payload: dict[str, Any] | None = None,
) -> None:
"""
Update task progress and/or payload atomically.
All parameters are optional. Payload is merged with existing data,
not replaced. All updates occur in a single database transaction.
Progress can be specified in three ways:
- float (0.0-1.0): Percentage only, e.g., 0.5 means 50%
- int: Count only (total unknown), e.g., 42 means "42 items processed"
- tuple[int, int]: Count and total, e.g., (3, 100) means "3 of 100"
The percentage is automatically computed from count/total.
:param progress: Progress value, or None to leave unchanged
:param payload: Payload data to merge (dict), or None to leave unchanged
Examples:
# Percentage only - displays as "In progress: 50 %"
ctx.update_task(progress=0.5)
# Count only (total unknown) - displays as "In progress: 42"
ctx.update_task(progress=42)
# Count and total - displays as "In progress: 3 of 100 (3 %)"
ctx.update_task(progress=(3, 100))
# Update payload only
ctx.update_task(payload={"step": "processing"})
# Update both atomically
ctx.update_task(
progress=(80, 100),
payload={"processed": 80, "total": 100}
)
"""
...
@abstractmethod
def on_cleanup(self, handler: Callable[[], None]) -> Callable[[], None]:
"""
Register a cleanup handler that runs when the task ends.
Cleanup handlers are called when the task completes (success),
fails with an error, or is cancelled. Multiple handlers can be
registered and will execute in LIFO order (last registered runs first).
Can be used as a decorator:
@ctx.on_cleanup
def cleanup():
logger.info("Task ended")
Or called directly:
ctx.on_cleanup(lambda: logger.info("Task ended"))
:param handler: Cleanup function to register
:returns: The handler (for decorator compatibility)
"""
...
@abstractmethod
def on_abort(self, handler: Callable[[], None]) -> Callable[[], None]:
"""
Register handler that runs when task is aborted.
When the first handler is registered, background polling starts
automatically. The handler will be called when an abort is detected.
The handler executes in a background thread and the task code
continues running unless the handler takes action to stop it.
:param handler: Callback function to execute when abort is detected
:returns: The handler (for decorator compatibility)
Example:
@ctx.on_abort
def handle_abort():
logger.info("Task was aborted!")
cleanup_partial_work()
"""
...
def task(
name: str | None = None,
scope: TaskScope = TaskScope.PRIVATE,
timeout: int | None = None,
) -> Callable[[Callable[P, R]], "TaskWrapper[P]"]:
"""
Decorator to register a task.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
:param name: Optional unique task name (e.g., "superset.generate_thumbnail").
If not provided, uses the function name as the task name.
:param scope: Task scope (TaskScope.PRIVATE, SHARED, or SYSTEM).
Defaults to TaskScope.PRIVATE.
:param timeout: Optional timeout in seconds. When the timeout is reached,
abort handlers are triggered if registered. Can be overridden
at call time via TaskOptions(timeout=...).
:returns: TaskWrapper with .schedule() method
Note:
Both direct calls and .schedule() return Task, regardless of the
original function's return type. The decorated function's return value
is discarded; only side effects and context updates matter.
Example:
from superset_core.api.tasks import task, get_context, TaskScope
# Private task (default scope)
@task
def generate_thumbnail(chart_id: int) -> None:
ctx = get_context()
# ... task implementation
# Named task with shared scope
@task(name="generate_report", scope=TaskScope.SHARED)
def generate_chart_thumbnail(chart_id: int) -> None:
ctx = get_context()
# Update progress and payload atomically
ctx.update_task(
progress=0.5,
payload={"chart_id": chart_id, "status": "processing"}
)
# ... task implementation
ctx.update_task(progress=1.0)
# System task (admin-only)
@task(scope=TaskScope.SYSTEM)
def cleanup_old_data() -> None:
ctx = get_context()
# ... cleanup implementation
# Task with timeout
@task(timeout=300) # 5-minute timeout
def long_running_task() -> None:
ctx = get_context()
@ctx.on_abort
def handle_abort():
# Called when timeout or manual abort
pass
# Schedule async execution
task = generate_chart_thumbnail.schedule(chart_id=123) # Returns Task
# Direct call for sync execution (blocks until task is complete)
task = generate_chart_thumbnail(chart_id=123) # Also returns Task
"""
raise NotImplementedError("Function will be replaced during initialization")
class TaskWrapper(Generic[P]):
"""
Type stub for task wrapper returned by @task decorator.
Both __call__ and .schedule() return Task.
"""
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Task:
"""Execute the task synchronously."""
raise NotImplementedError("Will be replaced during initialization")
def schedule(self, *args: P.args, **kwargs: P.kwargs) -> Task:
"""Schedule the task for async execution."""
raise NotImplementedError("Will be replaced during initialization")
def get_context() -> TaskContext:
"""
Get the current task context from ambient context.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
This function provides ambient access to the task context without
requiring it to be passed as a parameter. It can only be called
from within an async task execution.
:returns: The current TaskContext
:raises RuntimeError: If called outside a task execution context
Example:
@task("thumbnail_generation")
def generate_chart_thumbnail(chart_id: int):
ctx = get_context() # Access ambient context
# Update task state - no need to fetch task object
ctx.update_task(
progress=0.5,
payload={"chart_id": chart_id}
)
"""
raise NotImplementedError("Function will be replaced during initialization")
__all__ = [
"TaskStatus",
"TaskScope",
"TaskProperties",
"TaskContext",
"TaskOptions",
"task",
"get_context",
]

View File

@@ -56,19 +56,37 @@ class ModuleFederationConfig(BaseModel):
class ContributionConfig(BaseModel):
"""Configuration for frontend UI contributions."""
"""Configuration for frontend UI contributions.
Views and menus use a nested structure: type -> scope -> location -> contributions.
Example:
{
"views": {
"sqllab": {
"panels": [{"id": "my-ext.panel", "name": "My Panel"}],
"leftSidebar": [{"id": "my-ext.sidebar", "name": "Sidebar"}]
}
},
"menus": {
"sqllab": {
"editor": {"primary": [...], "secondary": [...]}
}
}
}
"""
commands: list[dict[str, Any]] = Field(
default_factory=list,
description="Command contributions",
)
views: dict[str, list[dict[str, Any]]] = Field(
views: dict[str, dict[str, list[dict[str, Any]]]] = Field(
default_factory=dict,
description="View contributions by location",
description="View contributions by scope and location",
)
menus: dict[str, Any] = Field(
menus: dict[str, dict[str, Any]] = Field(
default_factory=dict,
description="Menu contributions",
description="Menu contributions by scope and location",
)

View File

@@ -1,114 +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 __future__ import annotations
from typing import Any, Protocol, runtime_checkable, TypeVar
from pydantic import BaseModel
from superset_core.semantic_layers.semantic_view import SemanticView
ConfigT = TypeVar("ConfigT", bound=BaseModel, contravariant=True)
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
# TODO (betodealmeida): convert to ABC
@runtime_checkable
class SemanticLayer(Protocol[ConfigT, SemanticViewT]):
"""
A protocol for semantic layers.
"""
@classmethod
def from_configuration(
cls,
configuration: dict[str, Any],
) -> SemanticLayer[ConfigT, SemanticViewT]:
"""
Create a semantic layer from its configuration.
"""
@classmethod
def get_configuration_schema(
cls,
configuration: ConfigT | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the configuration needed to add the semantic layer.
A partial configuration `configuration` can be sent to improve the schema,
allowing for progressive validation and better UX. For example, a semantic
layer might require:
- auth information
- a database
If the user provides the auth information, a client can send the partial
configuration to this method, and the resulting JSON schema would include
the list of databases the user has access to, allowing a dropdown to be
populated.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are populated based on the provided connection info.
"""
@classmethod
def get_runtime_schema(
cls,
configuration: ConfigT,
runtime_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the runtime parameters needed to load semantic views.
This returns the schema needed to connect to a semantic view given the
configuration for the semantic layer. For example, a semantic layer might
be configured by:
- auth information
- an optional database
If the user does not provide a database when creating the semantic layer, the
runtime schema would require the database name to be provided before loading any
semantic views. This allows users to create semantic layers that connect to a
specific database (or project, account, etc.), or that allow users to select it
at query time.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are required if they were not provided in the initial
configuration.
"""
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
) -> set[SemanticViewT]:
"""
Get the semantic views available in the semantic layer.
The runtime configuration can provide information like a given project or
schema, used to restrict the semantic views returned.
"""
def get_semantic_view(
self,
name: str,
additional_configuration: dict[str, Any],
) -> SemanticViewT:
"""
Get a specific semantic view by its name and additional configuration.
"""

View File

@@ -1,105 +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 __future__ import annotations
import enum
from typing import Protocol, runtime_checkable
from superset_core.semantic_layers.types import (
Dimension,
Filter,
GroupLimit,
Metric,
OrderTuple,
SemanticResult,
)
# TODO (betodealmeida): move to the extension JSON
class SemanticViewFeature(enum.Enum):
"""
Custom features supported by semantic layers.
"""
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
GROUP_LIMIT = "GROUP_LIMIT"
GROUP_OTHERS = "GROUP_OTHERS"
# TODO (betodealmeida): convert to ABC
@runtime_checkable
class SemanticView(Protocol):
"""
A protocol for semantic views.
"""
features: frozenset[SemanticViewFeature]
def uid(self) -> str:
"""
Returns a unique identifier for the semantic view.
"""
def get_dimensions(self) -> set[Dimension]:
"""
Get the dimensions defined in the semantic view.
"""
def get_metrics(self) -> set[Metric]:
"""
Get the metrics defined in the semantic view.
"""
def get_values(
self,
dimension: Dimension,
filters: set[Filter] | None = None,
) -> SemanticResult:
"""
Return distinct values for a dimension.
"""
def get_dataframe(
self,
metrics: list[Metric],
dimensions: list[Dimension],
filters: set[Filter] | None = None,
order: list[OrderTuple] | None = None,
limit: int | None = None,
offset: int | None = None,
*,
group_limit: GroupLimit | None = None,
) -> SemanticResult:
"""
Execute a semantic query and return the results as a DataFrame.
"""
def get_row_count(
self,
metrics: list[Metric],
dimensions: list[Dimension],
filters: set[Filter] | None = None,
order: list[OrderTuple] | None = None,
limit: int | None = None,
offset: int | None = None,
*,
group_limit: GroupLimit | None = None,
) -> SemanticResult:
"""
Execute a query and return the number of rows the result would have.
"""

View File

@@ -1,328 +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 __future__ import annotations
import enum
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from functools import total_ordering
from typing import Type as TypeOf
from pandas import DataFrame
__all__ = [
"BINARY",
"BOOLEAN",
"DATE",
"DATETIME",
"DECIMAL",
"Day",
"Dimension",
"Hour",
"INTEGER",
"INTERVAL",
"Minute",
"Month",
"NUMBER",
"OBJECT",
"Quarter",
"Second",
"STRING",
"TIME",
"Week",
"Year",
]
class Type:
"""
Base class for types.
"""
class INTEGER(Type):
"""
Represents an integer type.
"""
class NUMBER(Type):
"""
Represents a number type.
"""
class DECIMAL(Type):
"""
Represents a decimal type.
"""
class STRING(Type):
"""
Represents a string type.
"""
class BOOLEAN(Type):
"""
Represents a boolean type.
"""
class DATE(Type):
"""
Represents a date type.
"""
class TIME(Type):
"""
Represents a time type.
"""
class DATETIME(DATE, TIME):
"""
Represents a datetime type.
"""
class INTERVAL(Type):
"""
Represents an interval type.
"""
class OBJECT(Type):
"""
Represents an object type.
"""
class BINARY(Type):
"""
Represents a binary type.
"""
@dataclass(frozen=True)
@total_ordering
class Grain:
"""
Base class for time and date grains with comparison support.
Attributes:
name: Human-readable name of the grain (e.g., "Second")
representation: ISO 8601 representation (e.g., "PT1S")
value: Time period as a timedelta
"""
name: str
representation: str
value: timedelta
def __eq__(self, other: object) -> bool:
if isinstance(other, Grain):
return self.value == other.value
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, Grain):
return self.value < other.value
return NotImplemented
def __hash__(self) -> int:
return hash((self.name, self.representation, self.value))
class Second(Grain):
name = "Second"
representation = "PT1S"
value = timedelta(seconds=1)
class Minute(Grain):
name = "Minute"
representation = "PT1M"
value = timedelta(minutes=1)
class Hour(Grain):
name = "Hour"
representation = "PT1H"
value = timedelta(hours=1)
class Day(Grain):
name = "Day"
representation = "P1D"
value = timedelta(days=1)
class Week(Grain):
name = "Week"
representation = "P1W"
value = timedelta(weeks=1)
class Month(Grain):
name = "Month"
representation = "P1M"
value = timedelta(days=30)
class Quarter(Grain):
name = "Quarter"
representation = "P3M"
value = timedelta(days=90)
class Year(Grain):
name = "Year"
representation = "P1Y"
value = timedelta(days=365)
@dataclass(frozen=True)
class Dimension:
id: str
name: str
type: TypeOf[Type]
definition: str | None = None
description: str | None = None
grain: TypeOf[Grain] | None = None
@dataclass(frozen=True)
class Metric:
id: str
name: str
type: TypeOf[Type]
definition: str
description: str | None = None
@dataclass(frozen=True)
class AdhocExpression:
id: str
definition: str
class Operator(str, enum.Enum):
EQUALS = "="
NOT_EQUALS = "!="
GREATER_THAN = ">"
LESS_THAN = "<"
GREATER_THAN_OR_EQUAL = ">="
LESS_THAN_OR_EQUAL = "<="
IN = "IN"
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
class PredicateType(enum.Enum):
WHERE = "WHERE"
HAVING = "HAVING"
@dataclass(frozen=True, order=True)
class Filter:
type: PredicateType
column: Dimension | Metric | None
operator: Operator
value: FilterValues | frozenset[FilterValues]
class OrderDirection(enum.Enum):
ASC = "ASC"
DESC = "DESC"
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
@dataclass(frozen=True)
class GroupLimit:
"""
Limit query to top/bottom N combinations of specified dimensions.
The `filters` parameter allows specifying separate filter constraints for the
group limit subquery. This is useful when you want to determine the top N groups
using different criteria (e.g., a different time range) than the main query.
For example, you might want to find the top 10 products by sales over the last
30 days, but then show daily sales for those products over the last 7 days.
"""
dimensions: list[Dimension]
top: int
metric: Metric | None
direction: OrderDirection = OrderDirection.DESC
group_others: bool = False
filters: set[Filter] | None = None
@dataclass(frozen=True)
class SemanticRequest:
"""
Represents a request made to obtain semantic results.
This could be a SQL query, an HTTP request, etc.
"""
type: str
definition: str
@dataclass(frozen=True)
class SemanticResult:
"""
Represents the results of a semantic query.
This includes any requests (SQL queries, HTTP requests) that were performed in order
to obtain the results, in order to help troubleshooting.
"""
requests: list[SemanticRequest]
# TODO (betodealmeida): convert to PyArrow Table
results: DataFrame
@dataclass(frozen=True)
class SemanticQuery:
"""
Represents a semantic query.
"""
metrics: list[Metric]
dimensions: list[Dimension]
filters: set[Filter] | None = None
order: list[OrderTuple] | None = None
limit: int | None = None
offset: int | None = None
group_limit: GroupLimit | None = None

View File

@@ -23,8 +23,6 @@
"@babel/preset-typescript": "^7.26.0",
"@types/react": "^19.0.10",
"copy-webpack-plugin": "^13.0.0",
"install": "^0.13.0",
"npm": "^11.1.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2",
"webpack": "^5.98.0",

View File

@@ -135,7 +135,9 @@ module.exports = {
'icons',
'i18n-strings',
'react-prefer-function-component',
'react-you-might-not-need-an-effect',
'prettier',
'react-you-might-not-need-an-effect',
],
rules: {
// === Essential Superset customizations ===
@@ -235,12 +237,22 @@ module.exports = {
'jsx-a11y/mouse-events-have-key-events': 0,
'jsx-a11y/no-static-element-interactions': 0,
// React effect best practices
'react-you-might-not-need-an-effect/no-empty-effect': 'error',
'react-you-might-not-need-an-effect/no-pass-live-state-to-parent': 'error',
'react-you-might-not-need-an-effect/no-initialize-state': 'error',
// Lodash
'lodash/import-scope': [2, 'member'],
// File progress
'file-progress/activate': 1,
// React effect rules
'react-you-might-not-need-an-effect/no-adjust-state-on-prop-change':
'error',
'react-you-might-not-need-an-effect/no-pass-data-to-parent': 'error',
// Restricted imports
'no-restricted-imports': [
'error',
@@ -273,6 +285,52 @@ module.exports = {
],
},
overrides: [
// Ban JavaScript files in src/ - all new code must be TypeScript
{
files: ['src/**/*.js', 'src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in src/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in plugins/ - all plugin source code must be TypeScript
{
files: ['plugins/**/src/**/*.js', 'plugins/**/src/**/*.jsx'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in plugins/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
// Ban JavaScript files in packages/ - with exceptions for config files and generators
{
files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'],
excludedFiles: [
'packages/generator-superset/**/*', // Yeoman generator templates run via Node
'packages/**/__mocks__/**/*', // Test mocks
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'Program',
message:
'JavaScript files are not allowed in packages/. Please use TypeScript (.ts/.tsx) instead.',
},
],
},
},
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
@@ -303,7 +361,7 @@ module.exports = {
],
'@typescript-eslint/no-empty-function': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-use-before-define': 1,
'@typescript-eslint/no-use-before-define': 'error',
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
@@ -399,27 +457,13 @@ module.exports = {
'**/spec/**/*',
],
excludedFiles: 'cypress-base/cypress/**/*',
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
env: {
'jest/globals': true,
},
settings: {
jest: {
version: 'detect',
},
},
extends: [
'plugin:jest/recommended',
'plugin:jest-dom/recommended',
'plugin:testing-library/react',
],
plugins: ['jest-dom', 'no-only-tests', 'testing-library'],
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'jest/consistent-test-it': 'error',
'no-only-tests/no-only-tests': 'error',
'prefer-promise-reject-errors': 0,
'max-classes-per-file': 0,

View File

@@ -23,8 +23,13 @@ const customConfig = require('../webpack.config.js');
// Filter out plugins that shouldn't be included in Storybook's static build
// ReactRefreshWebpackPlugin adds Fast Refresh code that requires a dev server runtime,
// which isn't available when serving the static storybook build
// ForkTsCheckerWebpackPlugin causes TypeScript project reference errors in Storybook context
const pluginsToExclude = [
'ReactRefreshWebpackPlugin',
'ForkTsCheckerWebpackPlugin',
];
const filteredPlugins = customConfig.plugins.filter(
plugin => plugin.constructor.name !== 'ReactRefreshWebpackPlugin',
plugin => !pluginsToExclude.includes(plugin.constructor.name),
);
// Deep clone and modify rules to disable React Fast Refresh and dev mode in SWC loader
@@ -73,9 +78,9 @@ const disableDevModeInRules = rules =>
module.exports = {
stories: [
'../src/@(components|common|filters|explore|views|dashboard|features)/**/*.stories.@(tsx|jsx)',
'../packages/superset-ui-demo/storybook/stories/**/*.*.@(tsx|jsx)',
'../packages/superset-ui-core/src/components/**/*.stories.@(tsx|jsx)',
'../src/**/*.stories.tsx',
'../packages/superset-ui-core/src/**/*.stories.tsx',
'../plugins/*/src/**/*.stories.tsx',
],
addons: [
@@ -102,6 +107,8 @@ module.exports = {
...customConfig.resolve?.alias,
// Fix for Storybook 8.6.x with React 17 - resolve ESM module paths
'react-dom/test-utils': require.resolve('react-dom/test-utils'),
// Shared storybook utilities
'@storybook-shared': join(__dirname, 'shared'),
},
},
plugins: [...config.plugins, ...filteredPlugins],

View File

@@ -28,6 +28,27 @@ import { App, Layout, Space, Content } from 'antd';
import 'src/theme.ts';
import './storybook.css';
// Set up bootstrap data for components that check HTML_SANITIZATION config
// (e.g., HandlebarsViewer). This allows <style> tags in Handlebars templates.
if (typeof document !== 'undefined') {
let appEl = document.getElementById('app');
if (!appEl) {
appEl = document.createElement('div');
appEl.id = 'app';
document.body.appendChild(appEl);
}
appEl.setAttribute(
'data-bootstrap',
JSON.stringify({
common: {
conf: {
HTML_SANITIZATION: false,
},
},
}),
);
}
export const GlobalStylesOverrides = () => (
<Global
styles={css`

View File

@@ -61,10 +61,7 @@ export default function ResizableChartDemo({
);
}
export const withResizableChartDemo: Decorator<{
width: number;
height: number;
}> = (storyFn, context) => {
export const withResizableChartDemo: Decorator = (Story, context) => {
const {
parameters: { initialSize, panelPadding },
} = context;
@@ -73,7 +70,14 @@ export const withResizableChartDemo: Decorator<{
initialSize={initialSize as Size | undefined}
panelPadding={panelPadding}
>
{innerSize => storyFn({ ...context, ...context.args, ...innerSize })}
{innerSize => (
<Story
args={{
...context.args,
...innerSize,
}}
/>
)}
</ResizableChartDemo>
);
};

View File

@@ -23,9 +23,25 @@ import {
ResizableBoxProps,
ResizeCallbackData,
} from 'react-resizable';
import { styled } from '@apache-superset/core/ui';
import 'react-resizable/css/styles.css';
const StyledResizableBox = styled(ResizableBox)`
&.panel {
overflow: hidden;
background: ${({ theme }) => theme.colorBgContainer};
border: 1px solid ${({ theme }) => theme.colorBorder};
border-radius: ${({ theme }) => theme.borderRadius}px;
}
.panel-body {
overflow: hidden;
width: 100%;
height: 100%;
}
`;
export type Size = ResizeCallbackData['size'];
export default function ResizablePanel({
@@ -41,7 +57,7 @@ export default function ResizablePanel({
}) {
const { width, height } = initialSize;
return (
<ResizableBox
<StyledResizableBox
className="panel"
width={width}
height={height}
@@ -60,6 +76,6 @@ export default function ResizablePanel({
{heading ? <div className="panel-heading">{heading}</div> : null}
<div className="panel-body">{children}</div>
</>
</ResizableBox>
</StyledResizableBox>
);
}

View File

@@ -32,7 +32,7 @@ export default function createQueryStory({
[key: string]: {
chartType: string;
formData: {
[key: string]: any;
[key: string]: unknown;
};
};
};
@@ -43,7 +43,7 @@ export default function createQueryStory({
mode: string | number,
width: number,
height: number,
formData: any,
formData: string,
) => {
const { chartType } = choices[mode];

View File

@@ -17,21 +17,18 @@
* under the License.
*/
import { SuperChart, VizType } from '@superset-ui/core';
import dummyDatasource from '../../../../../shared/dummyDatasource';
import data from '../data';
export const basic = () => (
<SuperChart
chartType="box-plot"
width={800}
height={600}
datasource={dummyDatasource}
queriesData={[{ data }]}
formData={{
colorScheme: 'd3Category10',
vizType: VizType.BoxPlot,
whiskerOptions: 'Min/max (no outliers)',
}}
/>
);
export { default as ErrorMessage } from './ErrorMessage';
export { default as Expandable } from './Expandable';
export { default as ResizablePanel, type Size } from './ResizablePanel';
export {
default as ResizableChartDemo,
SupersetBody,
withResizableChartDemo,
} from './ResizableChartDemo';
export {
default as VerifyCORS,
renderError,
type Props as VerifyCORSProps,
} from './VerifyCORS';
export { default as createQueryStory } from './createQueryStory';
export { default as dummyDatasource } from './dummyDatasource';

View File

@@ -1,64 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": true
},
"transform": {
"react": {
"runtime": "automatic",
"importSource": "@emotion/react",
"throwIfNamespace": true
},
"optimizer": {
"globals": {
"vars": {
"process.env.NODE_ENV": "production"
}
}
}
},
"target": "es2015",
"loose": true,
"externalHelpers": false,
"preserveAllComments": false,
"experimental": {
"plugins": [
[
"@swc/plugin-emotion",
{
"sourceMap": true,
"autoLabel": "dev-only",
"labelFormat": "[local]"
}
],
[
"@swc/plugin-transform-imports",
{
"lodash": {
"transform": "lodash/{{member}}",
"preventFullImport": true,
"skipDefaultConversion": false
},
"lodash-es": {
"transform": "lodash-es/{{member}}",
"preventFullImport": true,
"skipDefaultConversion": false
}
}
]
]
}
},
"module": {
"type": "es6",
"strict": false,
"strictMode": false,
"lazy": false,
"noInterop": false
},
"minify": false
}

View File

@@ -52,8 +52,6 @@ module.exports = {
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-runtime', { corejs: 3 }],
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
['babel-plugin-typescript-to-proptypes', { loose: true }],
[
'@emotion/babel-plugin',
{

View File

@@ -1,29 +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 = {
apiKey: process.env.APPLITOOLS_API_KEY,
batchId: process.env.APPLITOOLS_BATCH_ID,
batchName: process.env.APPLITOOLS_BATCH_NAME,
browser: [{ width: 1920, height: 1080, name: 'chrome' }],
failCypressOnDiff: false,
isDisabled: false,
showLogs: false,
testConcurrency: 10,
ignoreCaret: true,
};

View File

@@ -18,73 +18,67 @@
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'cypress';
import eyesPlugin from '@applitools/eyes-cypress';
const { verifyDownloadTasks } = require('cy-verify-downloads');
export default eyesPlugin(
defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 8000,
numTestsKeptInMemory: 3,
// Disabled after realizing this MESSES UP rison encoding in intricate ways
experimentalFetchPolyfill: false,
experimentalMemoryManagement: true,
requestTimeout: 10000,
video: false,
viewportWidth: 1280,
viewportHeight: 1024,
projectId: 'ud5x2f',
retries: {
runMode: 2,
openMode: 0,
export default defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 8000,
numTestsKeptInMemory: 3,
// Disabled after realizing this MESSES UP rison encoding in intricate ways
experimentalFetchPolyfill: false,
experimentalMemoryManagement: true,
requestTimeout: 10000,
video: false,
viewportWidth: 1280,
viewportHeight: 1024,
projectId: 'ud5x2f',
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
// Check https://github.com/cypress-io/cypress/issues/27804 for context
// TODO: This workaround should be removed when upgrading Cypress
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// eslint-disable-next-line no-param-reassign
launchOptions.args = launchOptions.args.map(arg => {
if (arg === '--headless') {
return '--headless=new';
}
return arg;
});
launchOptions.args.push(
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-sandbox',
'--disable-software-rasterizer',
'--memory-pressure-off',
'--js-flags=--max-old-space-size=4096',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
);
}
return launchOptions;
});
// eslint-disable-next-line global-require
require('@cypress/code-coverage/task')(on, config);
on('task', verifyDownloadTasks);
// eslint-disable-next-line global-require,import/extensions
return config;
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
// Check https://github.com/cypress-io/cypress/issues/27804 for context
// TODO: This workaround should be removed when upgrading Cypress
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// eslint-disable-next-line no-param-reassign
launchOptions.args = launchOptions.args.map(arg => {
if (arg === '--headless') {
return '--headless=new';
}
return arg;
});
launchOptions.args.push(
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-sandbox',
'--disable-software-rasterizer',
'--memory-pressure-off',
'--js-flags=--max-old-space-size=4096',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
);
}
return launchOptions;
});
// eslint-disable-next-line global-require
require('@cypress/code-coverage/task')(on, config);
on('task', verifyDownloadTasks);
// eslint-disable-next-line global-require,import/extensions
return config;
},
baseUrl: 'http://localhost:8088',
excludeSpecPattern: ['**/_skip.*'],
experimentalRunAllSpecs: true,
specPattern: [
'cypress/e2e/**/*.{js,jsx,ts,tsx}',
'cypress/applitools/**/*.{js,jsx,ts,tsx}',
],
},
}),
);
baseUrl: 'http://localhost:8088',
excludeSpecPattern: ['**/_skip.*'],
experimentalRunAllSpecs: true,
specPattern: ['cypress/e2e/**/*.{js,jsx,ts,tsx}'],
},
});

View File

@@ -1,45 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CHART_LIST } from 'cypress/utils/urls';
describe('charts list view', () => {
beforeEach(() => {
cy.visit(CHART_LIST);
});
afterEach(() => {
cy.eyesClose();
});
it('should load the Charts list', () => {
cy.get('[aria-label="unordered-list"]').click();
cy.eyesOpen({
testName: 'Charts list-view',
});
cy.eyesCheckWindow('Charts list-view loaded');
});
it('should load the Charts card list', () => {
cy.get('[aria-label="appstore"]').click();
cy.eyesOpen({
testName: 'Charts card-view',
});
cy.eyesCheckWindow('Charts card-view loaded');
});
});

View File

@@ -1,53 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
import { waitForChartLoad } from 'cypress/utils';
import { WORLD_HEALTH_CHARTS } from '../e2e/dashboard/utils';
describe('Dashboard load', () => {
beforeEach(() => {
cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
});
afterEach(() => {
cy.eyesClose();
});
it('should load the Dashboard', () => {
cy.eyesOpen({
testName: 'Dashboard page',
});
cy.eyesCheckWindow('Dashboard loaded');
});
it('should load the Dashboard in edit mode', () => {
cy.get('.header-with-actions')
.find('[aria-label="Edit dashboard"]')
.click();
// wait for a chart to appear
cy.get('[data-test="grid-container"]').find('.box_plot', {
timeout: 10000,
});
cy.eyesOpen({
testName: 'Dashboard edit mode',
});
cy.eyesCheckWindow('Dashboard edit mode loaded');
});
});

View File

@@ -1,45 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { DASHBOARD_LIST } from 'cypress/utils/urls';
describe('dashboard list view', () => {
beforeEach(() => {
cy.visit(DASHBOARD_LIST);
});
afterEach(() => {
cy.eyesClose();
});
it('should load the Dashboards list', () => {
cy.get('[aria-label="unordered-list"]').click();
cy.eyesOpen({
testName: 'Dashboards list-view',
});
cy.eyesCheckWindow('Dashboards list-view loaded');
});
it('should load the Dashboards card list', () => {
cy.get('[aria-label="appstore"]').click();
cy.eyesOpen({
testName: 'Dashboards card-view',
});
cy.eyesCheckWindow('Dashboards card-view loaded');
});
});

View File

@@ -1,46 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
FORM_DATA_DEFAULTS,
NUM_METRIC,
} from '../e2e/explore/visualizations/shared.helper';
describe('explore view', () => {
beforeEach(() => {
cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
});
afterEach(() => {
cy.eyesClose();
});
it('should load Explore', () => {
const LINE_CHART_DEFAULTS = {
...FORM_DATA_DEFAULTS,
viz_type: 'echarts_timeseries_line',
};
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
cy.visitChartByParams(formData);
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
cy.eyesOpen({
testName: 'Explore page',
});
cy.eyesCheckWindow('Explore loaded');
});
});

View File

@@ -1,57 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CHART_LIST } from 'cypress/utils/urls';
import { setGridMode, clearAllInputs } from 'cypress/utils';
import { setFilter } from '../explore/utils';
describe('Charts filters', () => {
before(() => {
cy.visit(CHART_LIST);
setGridMode('card');
});
beforeEach(() => {
clearAllInputs();
});
it('should allow filtering by "Owner"', () => {
setFilter('Owner', 'alpha user');
setFilter('Owner', 'admin user');
});
it('should allow filtering by "Modified by" correctly', () => {
setFilter('Modified by', 'alpha user');
setFilter('Modified by', 'admin user');
});
it('should allow filtering by "Type" correctly', () => {
setFilter('Type', 'Area Chart');
setFilter('Type', 'Bubble Chart');
});
it('should allow filtering by "Dataset" correctly', () => {
setFilter('Dataset', 'birth_names');
setFilter('Dataset', 'video_game_sales');
});
it('should allow filtering by "Dashboards" correctly', () => {
setFilter('Dashboard', 'USA Births Names');
setFilter('Dashboard', 'Video Game Sales');
});
});

View File

@@ -23,12 +23,9 @@ import {
interceptBulkDelete,
interceptUpdate,
interceptDelete,
visitSampleChartFromList,
saveChartToDashboard,
interceptFiltering,
interceptFavoriteStatus,
} from '../explore/utils';
import { interceptGet as interceptDashboardGet } from '../dashboard/utils';
function orderAlphabetical() {
setFilter('Sort', 'Alphabetical');
@@ -57,60 +54,6 @@ function visitChartList() {
}
describe('Charts list', () => {
describe('Cross-referenced dashboards', () => {
beforeEach(() => {
cy.createSampleDashboards([0, 1, 2, 3]);
cy.createSampleCharts([0]);
visitChartList();
});
// Skipped: depends on "Supported Charts Dashboard" which requires specific example loading
it.skip('should show the cross-referenced dashboards in the table cell', () => {
interceptDashboardGet();
cy.getBySel('table-row')
.first()
.find('[data-test="table-row-cell"]')
.find('[data-test="crosslinks"]')
.should('be.empty');
cy.getBySel('table-row')
.eq(10)
.find('[data-test="table-row-cell"]')
.find('[data-test="crosslinks"]')
.contains('Supported Charts Dashboard')
.invoke('removeAttr', 'target')
.click();
cy.wait('@get');
});
it('should show the newly added dashboards in a tooltip', () => {
interceptDashboardGet();
visitSampleChartFromList('1 - Sample chart');
saveChartToDashboard('1 - Sample chart', '1 - Sample dashboard');
saveChartToDashboard('1 - Sample chart', '2 - Sample dashboard');
saveChartToDashboard('1 - Sample chart', '3 - Sample dashboard');
saveChartToDashboard('1 - Sample chart', '4 - Sample dashboard');
visitChartList();
cy.getBySel('count-crosslinks').should('be.visible');
});
});
describe('card mode', () => {
before(() => {
visitChartList();
setGridMode('card');
});
it('should preserve other filters when sorting', () => {
// Check that we have some cards (count varies based on loaded examples)
cy.getBySel('styled-card').should('have.length.at.least', 1);
setFilter('Type', 'Big Number');
setFilter('Sort', 'Least recently modified');
// After filtering to Big Number type, we should have fewer cards
cy.getBySel('styled-card').should('have.length.at.least', 1);
});
});
describe('common actions', () => {
beforeEach(() => {
visitChartList();

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import '@cypress/code-coverage/support';
import '@applitools/eyes-cypress/commands';
import { expect } from 'chai';
import rison from 'rison';

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@
]
},
"dependencies": {
"@applitools/eyes-cypress": "^3.44.9",
"@cypress/code-coverage": "^3.10.4",
"@superset-ui/core": "^2.1.0",
"brace": "^0.11.1",

View File

@@ -6,11 +6,11 @@
"strict": true,
"target": "es2019",
"lib": ["es2019", "DOM"],
"types": ["cypress", "@applitools/eyes-cypress", "node"],
"types": ["cypress", "node"],
"allowJs": true,
"noEmit": true
},
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/types/index.d.ts"],
"files": ["cypress/support/index.d.ts"],
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -22,27 +22,40 @@
* @author Apache
*/
import type { Rule } from 'eslint';
import type { Node } from 'estree';
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
const plugin: { rules: Record<string, Rule.RuleModule> } = {
rules: {
'no-template-vars': {
create(context) {
function handler(node) {
if (node.arguments.length) {
const firstArgs = node.arguments[0];
meta: {
type: 'problem',
docs: {
description: 'Disallow variables in translation template strings',
},
schema: [],
},
create(context: Rule.RuleContext): Rule.RuleListener {
function handler(node: Node): void {
const callNode = node as Node & {
arguments: Array<Node & { type: string; expressions?: Node[] }>;
};
// Check all arguments (e.g., tn has singular and plural templates)
for (const arg of callNode.arguments ?? []) {
if (
firstArgs.type === 'TemplateLiteral' &&
firstArgs.expressions.length
arg.type === 'TemplateLiteral' &&
(arg as Node & { expressions?: Node[] }).expressions?.length
) {
context.report({
node,
message:
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
});
break; // Only report once per call
}
}
}
@@ -53,19 +66,29 @@ module.exports = {
},
},
'sentence-case-buttons': {
create(context) {
function isTitleCase(str) {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce sentence case for button text in translations',
},
schema: [],
},
create(context: Rule.RuleContext): Rule.RuleListener {
function isTitleCase(str: string): boolean {
// Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words)
return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str);
}
function isButtonContext(node) {
const { parent } = node;
function isButtonContext(node: Node & { parent?: Node }): boolean {
const { parent } = node as Node & {
parent?: Node & Record<string, unknown>;
};
if (!parent) return false;
// Check for button-specific props
if (parent.type === 'Property') {
const key = parent.key.name;
const key = (parent as unknown as { key: { name: string } }).key
.name;
return [
'primaryButtonName',
'secondaryButtonName',
@@ -75,10 +98,16 @@ module.exports = {
}
// Check for Button components
if (parent.type === 'JSXExpressionContainer') {
const jsx = parent.parent;
if (jsx?.type === 'JSXElement') {
const elementName = jsx.openingElement.name.name;
// Cast to string because ESTree Node type doesn't include JSX types
if ((parent.type as string) === 'JSXExpressionContainer') {
const jsx = (parent as Node & { parent?: Node }).parent as
| (Node & {
type: string;
openingElement?: { name: { name: string } };
})
| undefined;
if ((jsx?.type as string) === 'JSXElement') {
const elementName = jsx?.openingElement?.name.name;
return elementName === 'Button';
}
}
@@ -86,21 +115,24 @@ module.exports = {
return false;
}
function handler(node) {
if (node.arguments.length) {
const firstArg = node.arguments[0];
if (
firstArg.type === 'Literal' &&
typeof firstArg.value === 'string'
) {
const text = firstArg.value;
function handler(node: Node): void {
const callNode = node as Node & {
arguments: Array<Node & { type: string; value?: unknown }>;
};
// Check all string literal arguments (e.g., tn has singular and plural)
for (const arg of callNode.arguments ?? []) {
if (arg.type === 'Literal' && typeof arg.value === 'string') {
const text = arg.value;
if (isButtonContext(node) && isTitleCase(text)) {
if (
isButtonContext(node as Node & { parent?: Node }) &&
isTitleCase(text)
) {
const sentenceCase = text
.toLowerCase()
.replace(/^\w/, c => c.toUpperCase());
.replace(/^\w/, (c: string) => c.toUpperCase());
context.report({
node: firstArg,
node: arg,
message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`,
});
}
@@ -116,3 +148,5 @@ module.exports = {
},
},
};
module.exports = plugin;

View File

@@ -22,17 +22,19 @@
* @author Apache
*/
/* eslint-disable no-template-curly-in-string */
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin = require('.');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const rule = plugin.rules['no-template-vars'];
const rule: Rule.RuleModule = plugin.rules['no-template-vars'];
const errors = [
const errors: Array<{ type: string }> = [
{
type: 'CallExpression',
},

View File

@@ -2,7 +2,7 @@
"name": "eslint-plugin-i18n-strings",
"version": "1.0.0",
"description": "Warns about translation variables",
"main": "index.js",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@@ -22,12 +22,29 @@
* @author Apache
*/
import type { Rule } from 'eslint';
import type { Node } from 'estree';
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
interface JSXAttribute {
name?: { name: string };
value?: { type: string; value?: string; expression?: { value: string } };
}
interface JSXOpeningElement {
name: { name: string };
attributes: JSXAttribute[];
}
interface JSXElementNode {
type: string;
openingElement: JSXOpeningElement;
}
const plugin: { rules: Record<string, Rule.RuleModule> } = {
rules: {
'no-fa-icons-usage': {
meta: {
@@ -39,20 +56,27 @@ module.exports = {
},
schema: [],
},
create(context) {
create(context: Rule.RuleContext): Rule.RuleListener {
return {
// Check for JSX elements with class names containing "fa"
JSXElement(node) {
JSXElement(node: Node): void {
const jsxNode = node as unknown as JSXElementNode;
if (
node.openingElement &&
node.openingElement.name.name === 'i' &&
node.openingElement.attributes &&
node.openingElement.attributes.some(
attr =>
attr.name &&
attr.name.name === 'className' &&
/fa fa-/.test(attr.value.value),
)
jsxNode.openingElement &&
jsxNode.openingElement.name.name === 'i' &&
jsxNode.openingElement.attributes &&
jsxNode.openingElement.attributes.some((attr: JSXAttribute) => {
if (attr.name?.name !== 'className') return false;
// Handle className="fa fa-home"
if (attr.value?.type === 'Literal') {
return /fa fa-/.test(attr.value.value ?? '');
}
// Handle className={'fa fa-home'}
if (attr.value?.type === 'JSXExpressionContainer') {
return /fa fa-/.test(attr.value.expression?.value ?? '');
}
return false;
})
) {
context.report({
node,
@@ -66,3 +90,5 @@ module.exports = {
},
},
};
module.exports = plugin;

View File

@@ -22,16 +22,20 @@
* @author Apache
*/
import type { Rule } from 'eslint';
const { RuleTester } = require('eslint');
const plugin = require('.');
const plugin: { rules: Record<string, Rule.RuleModule> } = require('.');
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const rule = plugin.rules['no-fa-icons-usage'];
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } },
});
const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage'];
const errors = [
const errors: Array<{ message: string }> = [
{
message:
'FontAwesome icons should not be used. Use the src/components/Icons component instead.',

View File

@@ -2,7 +2,7 @@
"name": "eslint-plugin-icons",
"version": "1.0.0",
"description": "Warns about direct usage of Ant Design icons",
"main": "index.js",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@@ -18,7 +18,7 @@
*/
// https://www.w3.org/wiki/CSS/Properties/color/keywords
module.exports = [
const COLOR_KEYWORDS: string[] = [
'black',
'silver',
'gray',
@@ -170,3 +170,5 @@ module.exports = [
'whitesmoke',
'yellowgreen',
];
export default COLOR_KEYWORDS;

View File

@@ -1,119 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @fileoverview Rule to warn about literal colors
* @author Apache
*/
const COLOR_KEYWORDS = require('./colors');
function hasHexColor(quasi) {
if (typeof quasi === 'string') {
const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi;
return !!quasi.match(regex);
}
return false;
}
function hasRgbColor(quasi) {
if (typeof quasi === 'string') {
const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i;
return !!quasi.match(regex);
}
return false;
}
function hasLiteralColor(quasi, strict = false) {
if (typeof quasi === 'string') {
// matches literal colors at the start or end of a CSS prop
return COLOR_KEYWORDS.some(color => {
const regexColon = new RegExp(`: ${color}`);
const regexSemicolon = new RegExp(` ${color};`);
return (
!!quasi.match(regexColon) ||
!!quasi.match(regexSemicolon) ||
(strict && quasi === color)
);
});
}
return false;
}
const WARNING_MESSAGE =
'Theme color variables are preferred over rgb(a)/hex/literal colors';
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
rules: {
'no-literal-colors': {
create(context) {
const warned = [];
return {
TemplateElement(node) {
const rawValue = node?.value?.raw;
const isChildParentTagged =
node?.parent?.parent?.type === 'TaggedTemplateExpression';
const isChildParentArrow =
node?.parent?.parent?.type === 'ArrowFunctionExpression';
const isParentTemplateLiteral =
node?.parent?.type === 'TemplateLiteral';
const loc = node?.parent?.parent?.loc;
const locId = loc && JSON.stringify(loc);
const hasWarned = warned.includes(locId);
if (
!hasWarned &&
(isChildParentTagged ||
(isChildParentArrow && isParentTemplateLiteral)) &&
rawValue &&
(hasLiteralColor(rawValue) ||
hasHexColor(rawValue) ||
hasRgbColor(rawValue))
) {
context.report(node, loc, WARNING_MESSAGE);
warned.push(locId);
}
},
Literal(node) {
const value = node?.value;
const isParentProperty = node?.parent?.type === 'Property';
const locId = JSON.stringify(node.loc);
const hasWarned = warned.includes(locId);
if (
!hasWarned &&
isParentProperty &&
value &&
(hasLiteralColor(value, true) ||
hasHexColor(value) ||
hasRgbColor(value))
) {
context.report(node, node.loc, WARNING_MESSAGE);
warned.push(locId);
}
},
};
},
},
},
};

View File

@@ -0,0 +1,162 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @fileoverview Rule to warn about literal colors
* @author Apache
*/
import type { Rule } from 'eslint';
import type { Node, SourceLocation } from 'estree';
import COLOR_KEYWORDS from './colors';
function hasHexColor(quasi: string): boolean {
const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi;
return !!quasi.match(regex);
}
function hasRgbColor(quasi: string): boolean {
const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i;
return !!quasi.match(regex);
}
function hasLiteralColor(quasi: string, strict: boolean = false): boolean {
// matches literal colors at the start or end of a CSS prop
return COLOR_KEYWORDS.some((color: string) => {
const regexColon = new RegExp(`: ${color}`);
const regexSemicolon = new RegExp(` ${color};`);
return (
!!quasi.match(regexColon) ||
!!quasi.match(regexSemicolon) ||
(strict && quasi === color)
);
});
}
const WARNING_MESSAGE: string =
'Theme color variables are preferred over rgb(a)/hex/literal colors';
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
interface TemplateElementNode {
type: string;
value?: { raw: string };
loc?: SourceLocation | null;
parent?: {
type: string;
parent?: { type: string; loc?: SourceLocation | null };
};
}
interface LiteralNode {
type: string;
value?: unknown;
loc?: SourceLocation | null;
parent?: { type: string };
}
const plugin: { rules: Record<string, Rule.RuleModule> } = {
rules: {
'no-literal-colors': {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow literal color values; use theme colors instead',
},
schema: [],
},
create(context: Rule.RuleContext): Rule.RuleListener {
const warned: string[] = [];
return {
TemplateElement(node: Node): void {
const templateNode = node as TemplateElementNode;
const rawValue = templateNode?.value?.raw;
const isChildParentTagged =
templateNode?.parent?.parent?.type === 'TaggedTemplateExpression';
const isChildParentArrow =
templateNode?.parent?.parent?.type === 'ArrowFunctionExpression';
const isParentTemplateLiteral =
templateNode?.parent?.type === 'TemplateLiteral';
const loc = templateNode?.parent?.parent?.loc;
const locId = loc && JSON.stringify(loc);
const hasWarned = locId ? warned.includes(locId) : false;
if (
!hasWarned &&
(isChildParentTagged ||
(isChildParentArrow && isParentTemplateLiteral)) &&
rawValue &&
(hasLiteralColor(rawValue) ||
hasHexColor(rawValue) ||
hasRgbColor(rawValue))
) {
context.report({
node,
...(loc && { loc: loc as SourceLocation }),
message: WARNING_MESSAGE,
});
if (locId) {
warned.push(locId);
}
}
},
Literal(node: Node): void {
const literalNode = node as LiteralNode;
const value = literalNode?.value;
// Only process string literals (not numbers, booleans, null, or RegExp)
if (typeof value !== 'string') {
return;
}
const parent = literalNode?.parent as Node & {
type: string;
value?: Node;
};
// Only check property values, not keys (e.g., { color: 'red' } not { red: 1 })
const isPropertyValue =
parent?.type === 'Property' && parent.value === node;
const locId = node.loc ? JSON.stringify(node.loc) : null;
const hasWarned = locId ? warned.includes(locId) : false;
if (
!hasWarned &&
isPropertyValue &&
(hasLiteralColor(value, true) ||
hasHexColor(value) ||
hasRgbColor(value))
) {
context.report({
node,
...(node.loc && { loc: node.loc as SourceLocation }),
message: WARNING_MESSAGE,
});
if (locId) {
warned.push(locId);
}
}
},
};
},
},
},
};
module.exports = plugin;

View File

@@ -2,7 +2,7 @@
"name": "eslint-plugin-theme-colors",
"version": "1.0.0",
"description": "Warns about rgb(a)/hex/literal colors",
"main": "index.js",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@@ -36,7 +36,13 @@ module.exports = {
'^@apache-superset/core/(.*)$': '<rootDir>/packages/superset-core/src/$1',
},
testEnvironment: '<rootDir>/spec/helpers/jsDomWithFetchAPI.ts',
modulePathIgnorePatterns: ['<rootDir>/packages/generator-superset'],
modulePathIgnorePatterns: [
'<rootDir>/packages/generator-superset',
'<rootDir>/packages/.*/esm',
'<rootDir>/packages/.*/lib',
'<rootDir>/plugins/.*/esm',
'<rootDir>/plugins/.*/lib',
],
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
snapshotSerializers: ['@emotion/jest/serializer'],
testEnvironmentOptions: {
@@ -47,7 +53,6 @@ module.exports = {
'src/**/*.{js,jsx,ts,tsx}',
'{packages,plugins}/**/src/**/*.{js,jsx,ts,tsx}',
'!**/*.stories.*',
'!packages/superset-ui-demo/**/*',
],
coverageDirectory: '<rootDir>/coverage/',
coveragePathIgnorePatterns: [
@@ -59,7 +64,7 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
],
preset: 'ts-jest',
transform: {

View File

@@ -1,10 +1,11 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "react", "jsx-a11y", "typescript", "unicorn"],
"plugins": ["import", "react", "jest", "jsx-a11y", "typescript", "unicorn"],
"env": {
"browser": true,
"node": true,
"es2020": true
"es2020": true,
"jest": true
},
"globals": {
"__webpack_public_path__": "writable",
@@ -27,7 +28,8 @@
// === Core ESLint rules ===
// Error prevention
"no-console": "warn",
"no-alert": "warn",
"no-alert": "error",
"constructor-super": "error",
"no-debugger": "error",
"no-unused-vars": "off",
"no-undef": "error",
@@ -147,7 +149,8 @@
],
"react/no-array-index-key": "off",
"react/no-children-prop": "error",
"react/no-danger": "warn",
"react/no-danger": "error",
"react/forbid-foreign-prop-types": "error",
"react/no-danger-with-children": "error",
"react/no-deprecated": "error",
"react/no-did-update-set-state": "error",
@@ -230,7 +233,7 @@
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-use-before-define": "warn",
"@typescript-eslint/no-use-before-define": "error",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
@@ -249,6 +252,8 @@
],
// === Unicorn rules (bonus coverage) ===
"unicorn/no-new-array": "error",
"unicorn/no-invalid-remove-event-listener": "error",
"unicorn/filename-case": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/no-null": "off",
@@ -256,18 +261,26 @@
"unicorn/no-array-for-each": "off",
"unicorn/prefer-module": "off",
"unicorn/prefer-node-protocol": "off",
"unicorn/no-useless-undefined": "off"
"unicorn/no-useless-undefined": "off",
// === Jest rules ===
"jest/consistent-test-it": ["error", { "fn": "test" }],
"jest/no-focused-tests": "error",
"jest/no-disabled-tests": "error",
"jest/expect-expect": [
"error",
{
"assertFunctionNames": [
"expect",
"expect*",
"runTimezoneTest",
"compareURI",
"test*WithInitialValues"
]
}
]
},
"ignorePatterns": [
"*.test.{js,ts,jsx,tsx}",
"*.spec.{js,ts,jsx,tsx}",
"**/__tests__/**",
"**/__mocks__/**",
"**/test/**",
"**/tests/**",
"**/spec/**",
"plugins/**/test/**/*",
"packages/**/test/**/*",
"packages/generator-superset/**/*",
"cypress-base/**",
"node_modules/**",

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@
"build-translation": "scripts/po2json.sh",
"bundle-stats": "cross-env BUNDLE_ANALYZER=true npm run build && npx open-cli ../superset/static/stats/statistics.html",
"clear-npm": "mkdir -p /tmp/empty && rsync -a --delete /tmp/empty/ node_modules/ && rmdir node_modules /tmp/empty",
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\", \"!packages/superset-core/**/*\"]' packages",
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-core/**/*\"]' packages",
"cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage",
"dev": "webpack --mode=development --color --watch",
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
@@ -57,18 +57,17 @@
"lint-fix:all": "npx oxlint --config oxlint.json --fix",
"lint:full": "npm run lint && npm run check:custom-rules",
"check:custom-rules": "node scripts/check-custom-rules.js",
"check:storybook-coverage": "node scripts/check-storybook-coverage.js",
"ensure-oxc": "echo 'OXC linter is ready' && npx oxlint --version",
"lint-stats": "node ./scripts/oxlint-metrics-uploader.js",
"plugins:build": "node ./scripts/build.js",
"plugins:build-assets": "node ./scripts/copyAssets.js",
"plugins:build-storybook": "cd packages/superset-ui-demo && npm run build-storybook",
"plugins:create-conventional-version": "npm run prune && lerna version --conventional-commits --create-release github --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
"plugins:create-minor-version": "npm run prune && lerna version minor --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
"plugins:create-patch-version": "npm run prune && lerna version patch --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
"plugins:publish-all": "npm run prune && npm run plugins:build && lerna publish from-package --force-publish --yes",
"plugins:release-conventional": "npm run prune && npm run plugins:build && lerna publish --conventional-commits --create-release github --yes",
"plugins:release-from-tag": "npm run prune && npm run plugins:build && lerna publish from-package --yes",
"plugins:storybook": "cd packages/superset-ui-demo && npm run storybook",
"playwright:test": "playwright test",
"playwright:ui": "playwright test --ui",
"playwright:headed": "playwright test --headed",
@@ -97,12 +96,25 @@
],
"dependencies": {
"@apache-superset/core": "file:packages/superset-core",
"@deck.gl/aggregation-layers": "~9.2.5",
"@deck.gl/core": "~9.2.5",
"@deck.gl/extensions": "~9.2.5",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mesh-layers": "~9.2.5",
"@deck.gl/react": "~9.2.5",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
@@ -131,6 +143,7 @@
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@types/d3-selection": "^3.0.11",
"@types/d3-time-format": "^4.0.3",
"@types/react-google-recaptcha": "^2.1.9",
"@visx/axis": "^3.8.0",
@@ -162,11 +175,10 @@
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^171.4.0",
"immer": "^11.1.3",
"immer": "^11.1.4",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.17.23",
@@ -174,11 +186,11 @@
"markdown-to-jsx": "^9.7.3",
"match-sorter": "^6.3.4",
"memoize-one": "^5.2.1",
"pretty-ms": "^9.3.0",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.6",
"ol": "^7.5.2",
"prop-types": "^15.8.1",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
@@ -222,7 +234,6 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.63.10",
"@babel/cli": "^7.28.6",
"@babel/compat-data": "^7.28.4",
"@babel/core": "^7.29.0",
@@ -231,7 +242,7 @@
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
@@ -244,7 +255,7 @@
"@emotion/jest": "^11.14.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.58.1",
"@playwright/test": "^1.58.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-actions": "^8.6.15",
"@storybook/addon-controls": "^8.6.15",
@@ -258,9 +269,9 @@
"@storybook/test": "^8.6.15",
"@storybook/test-runner": "^0.17.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.14.0",
"@swc/plugin-emotion": "^12.0.0",
"@swc/plugin-transform-imports": "^10.0.0",
"@swc/core": "^1.15.11",
"@swc/plugin-emotion": "^14.5.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^12.1.5",
@@ -272,7 +283,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.2.1",
"@types/node": "^25.2.3",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-loadable": "^5.5.11",
@@ -284,7 +295,6 @@
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.6",
"@types/rison": "0.1.0",
"@types/sinon": "^17.0.3",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^7.18.0",
@@ -294,7 +304,6 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"baseline-browser-mapping": "^2.9.19",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
@@ -310,11 +319,9 @@
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^27.8.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
@@ -333,7 +340,8 @@
"jest-environment-jsdom": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^27.4.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^28.0.0",
"lerna": "^8.2.3",
"lightningcss": "^1.31.1",
"mini-css-extract-plugin": "^2.10.0",
@@ -346,7 +354,6 @@
"react-refresh": "^0.18.0",
"react-resizable": "^3.1.3",
"redux-mock-store": "^1.5.4",
"sinon": "^18.0.0",
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.5.0",
@@ -356,7 +363,6 @@
"terser-webpack-plugin": "^5.3.16",
"thread-loader": "^4.0.4",
"ts-jest": "^29.4.6",
"ts-loader": "^9.5.4",
"tscw-config": "^1.1.2",
"tsx": "^4.21.0",
"typescript": "5.4.5",
@@ -390,19 +396,20 @@
"underscore": "^1.13.7",
"jspdf": "^4.0.0",
"nwsapi": "^2.2.13",
"@deck.gl/aggregation-layers": "~9.2.2",
"@deck.gl/core": "~9.2.2",
"@deck.gl/extensions": "~9.2.2",
"@deck.gl/geo-layers": "~9.2.2",
"@deck.gl/layers": "~9.2.2",
"@deck.gl/mesh-layers": "~9.2.2",
"@deck.gl/react": "~9.2.2",
"@deck.gl/widgets": "~9.2.2",
"@luma.gl/constants": "~9.2.2",
"@luma.gl/core": "~9.2.2",
"@luma.gl/engine": "~9.2.2",
"@luma.gl/shadertools": "~9.2.2",
"@luma.gl/webgl": "~9.2.2"
"@deck.gl/aggregation-layers": "~9.2.5",
"@deck.gl/core": "~9.2.5",
"@deck.gl/extensions": "~9.2.5",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mesh-layers": "~9.2.5",
"@deck.gl/react": "~9.2.5",
"@deck.gl/widgets": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5"
},
"readme": "ERROR: No README data found!",
"scarfSettings": {

View File

@@ -17,6 +17,7 @@
* under the License.
*/
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
import helpers, { result } from 'yeoman-test';
import appModule from '../generators/app';

View File

@@ -18,15 +18,17 @@
*/
import { dirname, join } from 'path';
import helpers, { result } from 'yeoman-test';
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
import helpers from 'yeoman-test';
// @ts-ignore -- fs-extra/esm has no type declarations
import { copySync } from 'fs-extra/esm';
import { fileURLToPath } from 'url';
import pluginChartModule from '../generators/plugin-chart';
test('generator-superset:plugin-chart:creates files', async () => {
await helpers
const result = await helpers
.run(pluginChartModule)
.onTargetDirectory(dir => {
.onTargetDirectory((dir: string) => {
// `dir` is the path to the new temporary directory
const generatorDirname = dirname(fileURLToPath(import.meta.url));
copySync(

View File

@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.0.1-rc9",
"version": "0.0.1-rc11",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -16,8 +16,6 @@
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"install": "^0.13.0",
"npm": "^11.8.0",
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.23",

View File

@@ -93,20 +93,55 @@ export interface EditorContribution {
*/
export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css';
/**
* Valid locations within SQL Lab.
*/
export type SqlLabLocation =
| 'leftSidebar'
| 'rightSidebar'
| 'panels'
| 'editor'
| 'statusBar'
| 'results'
| 'queryHistory';
/**
* Nested structure for view contributions by scope and location.
* @example
* {
* sqllab: {
* panels: [{ id: "my-ext.panel", name: "My Panel" }],
* leftSidebar: [{ id: "my-ext.sidebar", name: "My Sidebar" }]
* }
* }
*/
export interface ViewContributions {
sqllab?: Partial<Record<SqlLabLocation, ViewContribution[]>>;
}
/**
* Nested structure for menu contributions by scope and location.
* @example
* {
* sqllab: {
* editor: { primary: [...], secondary: [...] }
* }
* }
*/
export interface MenuContributions {
sqllab?: Partial<Record<SqlLabLocation, MenuContribution>>;
}
/**
* Aggregates all contributions (commands, menus, views, and editors) provided by an extension or module.
*/
export interface Contributions {
/** List of command contributions. */
commands: CommandContribution[];
/** Mapping of menu contributions by menu key. */
menus: {
[key: string]: MenuContribution;
};
/** Mapping of view contributions by view key. */
views: {
[key: string]: ViewContribution[];
};
/** Nested mapping of menu contributions by scope and location. */
menus: MenuContributions;
/** Nested mapping of view contributions by scope and location. */
views: ViewContributions;
/** List of editor contributions. */
editors?: EditorContribution[];
}

View File

@@ -21,7 +21,7 @@ beforeEach(() => {
jest.resetAllMocks();
});
it('should pipe to `console` methods', () => {
test('should pipe to `console` methods', () => {
const { logging } = require('@apache-superset/core');
jest.spyOn(logging, 'debug').mockImplementation();
@@ -49,7 +49,7 @@ it('should pipe to `console` methods', () => {
expect(() => logging.trace()).toThrow('Trace:');
});
it('should use noop functions when console unavailable', () => {
test('should use noop functions when console unavailable', () => {
Object.assign(window, { console: undefined });
const { logging } = require('@apache-superset/core');

View File

@@ -202,7 +202,7 @@ test('serializeThemeConfig defaults to "default" for unknown algorithms', () =>
const unknownAlgorithm = () => ({});
const config: AntdThemeConfig = {
token: { colorPrimary: '#ff0000' },
// @ts-ignore
// @ts-expect-error
algorithm: unknownAlgorithm,
};
@@ -237,7 +237,7 @@ test('serializeThemeConfig defaults each unknown algorithm in array to "default"
const unknownAlgorithm = () => ({});
const config: AntdThemeConfig = {
token: { colorPrimary: '#ff0000' },
// @ts-ignore
// @ts-expect-error
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
};
@@ -257,10 +257,10 @@ test('serializeThemeConfig handles mixed known and unknown algorithms in array',
token: { colorPrimary: '#ff0000' },
algorithm: [
antdThemeImport.darkAlgorithm,
// @ts-ignore
// @ts-expect-error
unknownAlgorithm1,
antdThemeImport.compactAlgorithm,
// @ts-ignore
// @ts-expect-error
unknownAlgorithm2,
],
};

View File

@@ -22,16 +22,21 @@ import { ReactElement, ReactNode, ReactText, ComponentType } from 'react';
import type {
AdhocColumn,
Column,
CurrencyFormatter,
Currency,
DatasourceType,
DataRecordValue,
JsonObject,
JsonValue,
Metric,
NumberFormatter,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
QueryResponse,
TimeFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { sharedControls, sharedControlComponents } from './shared-controls';
export type { Metric } from '@superset-ui/core';
@@ -608,3 +613,78 @@ export type ControlFormItemSpec<T extends ControlType = ControlType> = {
defaultValue?: Currency;
}
: {});
export enum ColorSchemeEnum {
Green = 'Green',
Red = 'Red',
}
/** ----------------------------------------------
* Shared Table Chart Types
* Used by plugin-chart-table and plugin-chart-ag-grid-table
* --------------------------------------------- */
export type CustomFormatter = (value: DataRecordValue) => string;
export type BasicColorFormatterType = {
backgroundColor: string;
arrowColor: string;
mainArrow: string;
};
export type SortByItem = {
id: string;
key: string;
desc?: boolean;
};
export type SearchOption = {
value: string;
label: string;
};
export interface ServerPaginationData {
pageSize?: number;
currentPage?: number;
sortBy?: SortByItem[];
searchText?: string;
searchColumn?: string;
}
export type TableColumnConfig = {
d3NumberFormat?: string;
d3SmallNumberFormat?: string;
d3TimeFormat?: string;
columnWidth?: number;
horizontalAlign?: 'left' | 'right' | 'center';
showCellBars?: boolean;
alignPositiveNegative?: boolean;
colorPositiveNegative?: boolean;
truncateLongCells?: boolean;
currencyFormat?: Currency;
visible?: boolean;
customColumnName?: string;
displayTypeIcon?: boolean;
};
export interface DataColumnMeta {
// `key` is what is called `label` in the input props
key: string;
// `label` is verbose column name used for rendering
label: string;
// `originalLabel` preserves the original label when time comparison transforms the labels
originalLabel?: string;
dataType: GenericDataType;
formatter?:
| TimeFormatter
| NumberFormatter
| CustomFormatter
| CurrencyFormatter;
isMetric?: boolean;
isPercentMetric?: boolean;
isNumeric?: boolean;
config?: TableColumnConfig;
isChildColumn?: boolean;
description?: string;
currencyCodeColumn?: string;
}

View File

@@ -45,7 +45,7 @@ describe('metricColumnFilter', () => {
}) as SqlaFormData;
describe('shouldSkipMetricColumn', () => {
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
test('should skip unprefixed percent metric columns if prefixed version exists', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData([], ['metric1']);
@@ -58,7 +58,7 @@ describe('metricColumnFilter', () => {
expect(result).toBe(true);
});
it('should not skip if column is also a regular metric', () => {
test('should not skip if column is also a regular metric', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData(['metric1'], ['metric1']);
@@ -71,7 +71,7 @@ describe('metricColumnFilter', () => {
expect(result).toBe(false);
});
it('should not skip if column starts with %', () => {
test('should not skip if column starts with %', () => {
const colnames = ['%metric1'];
const formData = createFormData(['metric1'], []);
@@ -84,7 +84,7 @@ describe('metricColumnFilter', () => {
expect(result).toBe(false);
});
it('should not skip if no prefixed version exists', () => {
test('should not skip if no prefixed version exists', () => {
const colnames = ['metric1'];
const formData = createFormData([], ['metric1']);
@@ -99,35 +99,35 @@ describe('metricColumnFilter', () => {
});
describe('isRegularMetric', () => {
it('should return true for regular metrics', () => {
test('should return true for regular metrics', () => {
const formData = createFormData(['metric1', 'metric2'], []);
expect(isRegularMetric('metric1', formData)).toBe(true);
expect(isRegularMetric('metric2', formData)).toBe(true);
});
it('should return false for non-metrics', () => {
test('should return false for non-metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isRegularMetric('non_metric', formData)).toBe(false);
});
it('should return false for percentage metrics', () => {
test('should return false for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
});
});
describe('isPercentMetric', () => {
it('should return true for percentage metrics', () => {
test('should return true for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
});
it('should return false for non-percentage metrics', () => {
test('should return false for non-percentage metrics', () => {
const formData = createFormData(['regular_metric'], []);
expect(isPercentMetric('regular_metric', formData)).toBe(false);
});
it('should return false for regular metrics', () => {
test('should return false for regular metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isPercentMetric('metric1', formData)).toBe(false);
});

View File

@@ -33,34 +33,34 @@ describe('ColumnOption', () => {
render(<ColumnTypeLabel {...props} {...overrides} />);
}
it('is a valid element', () => {
test('is a valid element', () => {
expect(isValidElement(<ColumnTypeLabel {...defaultProps} />)).toBe(true);
});
it('string type shows ABC icon', () => {
test('string type shows ABC icon', () => {
renderColumnTypeLabel({ type: GenericDataType.String });
expect(screen.getByLabelText('string type icon')).toBeVisible();
});
it('int type shows # icon', () => {
test('int type shows # icon', () => {
renderColumnTypeLabel({ type: GenericDataType.Numeric });
expect(screen.getByLabelText('numeric type icon')).toBeVisible();
});
it('bool type shows 1|0 icon', () => {
test('bool type shows 1|0 icon', () => {
renderColumnTypeLabel({ type: GenericDataType.Boolean });
expect(screen.getByLabelText('boolean type icon')).toBeVisible();
});
it('expression type shows function icon', () => {
test('expression type shows function icon', () => {
renderColumnTypeLabel({ type: 'expression' });
expect(screen.getByLabelText('function type icon')).toBeVisible();
});
it('metric type shows sigma icon', () => {
test('metric type shows sigma icon', () => {
renderColumnTypeLabel({ type: 'metric' });
expect(screen.getByLabelText('metric type icon')).toBeVisible();
});
it('unknown type shows question mark', () => {
test('unknown type shows question mark', () => {
renderColumnTypeLabel({ type: undefined });
expect(screen.getByLabelText('unknown type icon')).toBeVisible();
});
it('datetime type displays', () => {
test('datetime type displays', () => {
renderColumnTypeLabel({ type: GenericDataType.Temporal });
expect(screen.getByLabelText('temporal type icon')).toBeVisible();
});

View File

@@ -19,7 +19,7 @@
import { sections } from '../src';
describe('@superset-ui/chart-controls', () => {
it('exports sections', () => {
test('exports sections', () => {
expect(sections).toBeDefined();
expect(sections.datasourceAndVizType).toBeDefined();
});

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