Compare commits

...

23 Commits

Author SHA1 Message Date
sadpandajoe
3d24afdabf fix(sqllab): move outline to inner .ace_editor to avoid clipping popovers
Address Copilot/Bito review: the previous EditorOutline wrapper used
overflow: hidden to clip the border-radius, which would also clip
Ace's autocomplete dropdown and gutter/annotation tooltips that Ace
mounts as descendants of .ace_editor. Apply the border + border-radius
directly to the .ace_editor element instead so the outline still
renders and popovers are no longer clipped at the wrapper boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 00:05:05 +00:00
sadpandajoe
c7a09acd01 fix(sqllab): shrink Template Parameters editor height and add outline
Reduce editor height from 800px to 360px so the popover fits on
laptop viewports without overflowing. Replace the broken
StyledEditorHost styled wrapper (whose &.ace_editor selector never
matched because the class lives on a deeper DOM node) with an
EditorOutline div that applies border + border-radius + overflow:hidden
directly, ensuring the outline is always visible in both light and
dark themes via theme.colorBorder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 18:27:46 +00:00
dependabot[bot]
1f95a6c486 chore(deps): bump simplejson from 3.20.1 to 4.1.1 (#41082)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:00:42 -07:00
dependabot[bot]
e93cbd6c38 chore(deps): bump croniter from 6.0.0 to 6.2.2 (#41086)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:59:00 -07:00
dependabot[bot]
dca8af770c chore(deps-dev): bump typescript-eslint from 8.60.1 to 8.61.0 in /superset-websocket (#41087)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 10:58:39 -07:00
dependabot[bot]
81c1181519 chore(deps-dev): bump typescript-eslint from 8.60.1 to 8.61.0 in /docs (#41092)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 10:56:44 -07:00
dependabot[bot]
387c62919e chore(deps): bump hot-shots from 15.0.0 to 16.0.0 in /superset-websocket (#41107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 10:56:22 -07:00
dependabot[bot]
77d7483f27 chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.13 to 0.10.14 in /superset-frontend (#41109)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 10:54:22 -07:00
dependabot[bot]
1a8d08152d chore(deps): bump fuse.js from 7.4.1 to 7.4.2 in /superset-frontend (#41110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 10:54:06 -07:00
Bob Jo
257dafeec5 fix(query): don't mutate ad-hoc ORDER BY expressions when building queries (#40993)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 13:03:39 -04:00
Alexandru Soare
6d08e79259 feat(security): Add extension hooks for custom access control, ownership, and asset lifecycle (#40707) 2026-06-16 15:25:03 +03:00
Geidō
01ed81785e fix(dashboard): required filters reliably apply default + Apply enables on change (#40470) 2026-06-16 11:23:05 +03:00
Vighnesh Tule
7b4efacbc2 fix(charts): add default padding to match other charts (#36895)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-06-15 21:05:17 -07:00
Amin Ghadersohi
7cb4990403 feat(mcp): add create_dataset tool to register physical tables as datasets (#40340)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:25:29 -04:00
dependabot[bot]
c90b2571d7 chore(deps-dev): bump xlrd from 2.0.1 to 2.0.2 (#41083)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:19:43 -07:00
dependabot[bot]
1a4941eee5 chore(deps-dev): bump hdbcli from 2.28.20 to 2.28.21 (#41084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:19:33 -07:00
dependabot[bot]
d839cca995 chore(deps-dev): update pyocient requirement from <2,>=1.0.15 to >=1.0.15,<4 (#40941)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 18:18:25 -07:00
dependabot[bot]
0ec7e7df99 chore(deps): bump dompurify from 3.4.8 to 3.4.9 in /superset-frontend (#41089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:16:36 -07:00
dependabot[bot]
9d8287e1bd chore(deps-dev): bump @typescript-eslint/parser from 8.60.1 to 8.61.0 in /superset-websocket (#41090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:16:21 -07:00
dependabot[bot]
0c696cea7e chore(deps): bump google-auth-library from 10.6.2 to 10.7.0 in /superset-frontend (#41091)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:16:05 -07:00
dependabot[bot]
fe625a917e chore(deps-dev): bump @typescript-eslint/parser from 8.60.1 to 8.61.0 in /docs (#41093)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:14:51 -07:00
dependabot[bot]
a69f9eb00d chore(deps-dev): bump oxlint from 1.68.0 to 1.69.0 in /superset-frontend (#41094)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:14:27 -07:00
Evan Rusackas
1311d040ba feat(deckgl): add point radius controls for GeoJSON layer (#33247)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-15 17:38:43 -07:00
46 changed files with 1996 additions and 395 deletions

View File

@@ -101,7 +101,7 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",
@@ -109,7 +109,7 @@
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.60.1",
"typescript-eslint": "^8.61.0",
"webpack": "^5.107.2"
},
"browserslist": {

View File

@@ -4922,110 +4922,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.60.1", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz#c1060bb8fa4be80624d3f3dec8dd9caca373af76"
integrity sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/type-utils" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/type-utils" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.60.1", "@typescript-eslint/parser@^8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.1.tgz#a9d7f30850384d34b41f4687dd8944823c09e289"
integrity sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
dependencies:
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.1.tgz#eb29712f58d72c222fc727162e92f2ab4670971b"
integrity sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==
"@typescript-eslint/project-service@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.60.1"
"@typescript-eslint/types" "^8.60.1"
"@typescript-eslint/tsconfig-utils" "^8.61.0"
"@typescript-eslint/types" "^8.61.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz#2f875962eaad0a0789cc3c36aea9b4ddeb2dd9c8"
integrity sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==
"@typescript-eslint/scope-manager@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
dependencies:
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/tsconfig-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
"@typescript-eslint/tsconfig-utils@^8.60.1":
"@typescript-eslint/tsconfig-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
"@typescript-eslint/type-utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz#1ae45f0f2a701354beea4a58c2161e40a5e3c379"
integrity sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==
"@typescript-eslint/tsconfig-utils@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
"@typescript-eslint/type-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
dependencies:
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
"@typescript-eslint/types@^8.60.1":
"@typescript-eslint/types@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
"@typescript-eslint/typescript-estree@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz#016630b119228bf483ddc652703a6a038f3fdd74"
integrity sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==
"@typescript-eslint/types@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
"@typescript-eslint/typescript-estree@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
dependencies:
"@typescript-eslint/project-service" "8.60.1"
"@typescript-eslint/tsconfig-utils" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/visitor-keys" "8.60.1"
"@typescript-eslint/project-service" "8.61.0"
"@typescript-eslint/tsconfig-utils" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.1.tgz#31cf566095602d9fe8ad91837d2eb520b8de762b"
integrity sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==
"@typescript-eslint/utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.60.1"
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys@8.60.1":
version "8.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz#165d1d8901137b944efaf18f00ab5ecb57f06995"
integrity sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==
"@typescript-eslint/visitor-keys@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
dependencies:
"@typescript-eslint/types" "8.60.1"
"@typescript-eslint/types" "8.61.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -14499,15 +14499,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.60.1:
version "8.60.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.1.tgz#13db05c6eabb89669deec44545b788a0e9aee640"
integrity sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==
typescript-eslint@^8.61.0:
version "8.61.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.60.1"
"@typescript-eslint/parser" "8.60.1"
"@typescript-eslint/typescript-estree" "8.60.1"
"@typescript-eslint/utils" "8.60.1"
"@typescript-eslint/eslint-plugin" "8.61.0"
"@typescript-eslint/parser" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
typescript@~6.0.3:
version "6.0.3"

View File

@@ -43,7 +43,7 @@ dependencies = [
"click-option-group",
"colorama",
"flask-cors>=6.0.0, <7.0",
"croniter>=0.3.28",
"croniter>=6.2.2",
"cron-descriptor",
"cryptography>=42.0.4, <47.0.0",
"deprecation>=2.1.0, <2.2.0",
@@ -97,7 +97,7 @@ dependencies = [
"selenium>=4.44.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
"simplejson>=4.1.1",
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris
@@ -144,7 +144,7 @@ dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol>=2.4.0, <8.0"]
excel = ["xlrd>=1.2.0, <1.3"]
excel = ["xlrd>=2.0.2, <2.1"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
@@ -156,7 +156,7 @@ firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==0.4.0"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
@@ -173,7 +173,7 @@ motherduck = ["apache-superset[duckdb]"]
mysql = ["mysqlclient>=2.1.0, <3"]
ocient = [
"sqlalchemy-ocient>=1.0.0",
"pyocient>=1.0.15, <2",
"pyocient>=1.0.15, <4",
"shapely",
"geojson",
]

View File

@@ -84,7 +84,7 @@ colorama==0.4.6
# flask-appbuilder
cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.0.0
croniter==6.2.2
# via apache-superset (pyproject.toml)
cryptography==46.0.7
# via
@@ -384,7 +384,7 @@ setuptools==80.9.0
# via -r requirements/base.in
shillelagh==1.4.4
# via apache-superset (pyproject.toml)
simplejson==3.20.1
simplejson==4.1.1
# via apache-superset (pyproject.toml)
six==1.17.0
# via

View File

@@ -174,7 +174,7 @@ cron-descriptor==1.4.5
# via
# -c requirements/base-constraint.txt
# apache-superset
croniter==6.0.0
croniter==6.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -939,7 +939,7 @@ shillelagh==1.4.4
# via
# -c requirements/base-constraint.txt
# apache-superset
simplejson==3.20.1
simplejson==4.1.1
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -95,14 +95,14 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.1",
"fuse.js": "^7.4.2",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"google-auth-library": "^10.7.0",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -178,7 +178,7 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.13",
"@formatjs/intl-durationformat": "^0.10.14",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
@@ -261,7 +261,7 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.68.0",
"oxlint": "^1.69.0",
"po2json": "^0.4.5",
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
@@ -3955,38 +3955,38 @@
}
},
"node_modules/@formatjs/bigdecimal": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.5.tgz",
"integrity": "sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==",
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.6.tgz",
"integrity": "sha512-aPzKsGQOkQRHUEbyO/ZtYfr4EqaBQnSs6U4tzTla1xBnIdEHgY2GqEqso28UMwWRkzKqqTj5+/6BmuOsRkfn2A==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz",
"integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==",
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.6.tgz",
"integrity": "sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/intl-durationformat": {
"version": "0.10.13",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.13.tgz",
"integrity": "sha512-A1dBcOh1YrcRf/AbmZHFVXgIYkpAaFgyGaYavO/KutbqEXY3HI63o2E1ctmxmllfg3qn3TZGtZux42EFwHNTbg==",
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.14.tgz",
"integrity": "sha512-qVrbKGJZwoGFLmQyMBn4Pk44WCpBKINwLf+isrEd4RpvKs5nChRofHanWJqSu8TjuKJjk1qJjDIFkA/hJX/a9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/bigdecimal": "0.2.5",
"@formatjs/intl-localematcher": "0.8.9"
"@formatjs/bigdecimal": "0.2.6",
"@formatjs/intl-localematcher": "0.8.10"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.9.tgz",
"integrity": "sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==",
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.10.tgz",
"integrity": "sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.5"
"@formatjs/fast-memoize": "3.1.6"
}
},
"node_modules/@gar/promise-retry": {
@@ -8324,9 +8324,9 @@
]
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.68.0.tgz",
"integrity": "sha512-wEdsIspexXLLMCPAEOcCuFLMt6aE3AzTuA/nQKLPRnoJ+EQTturmGheDkhHuuVHx0GbutjQ3JKmEn+Gz6Ag28Q==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.69.0.tgz",
"integrity": "sha512-DKQQbD5cZ/MYfDgDI7YGyGD9FSxABlsBsYFo5p26lloob543tP9+4N3guwdXIYJN+7HSZxLe8YJuwcOWw5qnHg==",
"cpu": [
"arm"
],
@@ -8341,9 +8341,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.68.0.tgz",
"integrity": "sha512-6aZRNNXQTsYtgaus8HTb9nuCcsrQTlKXGnktwvwW0n/SooRWNxNb3925grDkC63aEYZuCIyOVLV16IdYIoC2aQ==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.69.0.tgz",
"integrity": "sha512-lEhb+I5pr4inux+JFwfCa1HRq3Os7NirEFQ0H1I35SVEHPm6byX0Ah47xmRha3qi6LAkxUcxViL8o/9PivjzBg==",
"cpu": [
"arm64"
],
@@ -8358,9 +8358,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.68.0.tgz",
"integrity": "sha512-lVTbsE3kO4bLpZELgjRZuAJc8kP98wb83yMXWH8gaPaFZ+cM2IDeZto4ByoUAYj0Mxv2rvw+A1ssZequSepVSg==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.69.0.tgz",
"integrity": "sha512-GY2YE8lOZW59BW1Ia1y+1gR0XyjrZRvVWHAr8LGeGhYHE0OQJ/7cRKXTkx1P+E9/6awEc3SX8a68SFTjh/E//A==",
"cpu": [
"arm64"
],
@@ -8375,9 +8375,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.68.0.tgz",
"integrity": "sha512-nCmw2XrmQskjBUh/sfP5yKs93V68LijQgjd1cuuZ/q4SCARngLYs60/qqyzuMsg8QQ9KArDI98hxs/RDGE4KRQ==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.69.0.tgz",
"integrity": "sha512-ax1oZnOjHX3LB7myQyHEaQkDwfLb6str3/nSP6O7EVUviQGNkEGzGV0EqcBJWK+Ufwx0l4xPgyYayurvhAdl2Q==",
"cpu": [
"x64"
],
@@ -8392,9 +8392,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.68.0.tgz",
"integrity": "sha512-TI4ovQJliYE9V6e06cEv+qEI9uj7Ao65fmif4er4HD+aouyYyh0P31q2jh3KtqsOHHcQqv2PZ61TjJFLpBDGWQ==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.69.0.tgz",
"integrity": "sha512-kHWeHv4g2h8NY+mpCxzCtY4uerMJWTN/TSnNj1CPbakFpHEJ6cTya2wWV0pDSYWOJ2+0UiEbhn3AtXxHtsnKjg==",
"cpu": [
"x64"
],
@@ -8409,9 +8409,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.68.0.tgz",
"integrity": "sha512-LcNnEi9g71Cmry5ZpLbKT+oVv+/zYG3hYVAbBBB5X85nOQZSk8l92CnDkxJMcxUg0NCnMCOFZuaVDlMyv4tYJw==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.69.0.tgz",
"integrity": "sha512-gq84vM1a1oEehXo27YCDzGVcxPsZDI1yswZwz2Da1/cbnWtrL16XZZnz0G/+gIU8edtHpfjxq5c+vWEHqJfWoQ==",
"cpu": [
"arm"
],
@@ -8426,9 +8426,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.68.0.tgz",
"integrity": "sha512-OovHahL3FX4UaK+hgSf11llUx2vszqjSdQQ61Ck9InOEI/ptZoC4XSQJurITqItVvd53JSlmkLMeaNjM1PoQew==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.69.0.tgz",
"integrity": "sha512-kIqEa98JQ0VRyrcncxA417m2AzasqTlD+FyVT1AksjvjkqQcvm7pBWYvoW3/mpyOP2XYvi5nSCCTIe6De1yu5g==",
"cpu": [
"arm"
],
@@ -8443,13 +8443,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.68.0.tgz",
"integrity": "sha512-YbzTglnHLzzi9zv5or8Ztz5fykAoZE8W9iM42/bOrF4HBSB6rJTqdLQWuoP76EHQw9DuKl76K1QmFlG29sPJXQ==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.69.0.tgz",
"integrity": "sha512-j+xYiXozxGWx2cpjCrwwGR4awTxPFsRv3JZrv23RCogEPMc4R7UqjHW47p/RG0aRlbWiROCJ8coUfCwy0dvzHA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8460,13 +8463,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.68.0.tgz",
"integrity": "sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.69.0.tgz",
"integrity": "sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8477,13 +8483,16 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.68.0.tgz",
"integrity": "sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.69.0.tgz",
"integrity": "sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8494,13 +8503,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.68.0.tgz",
"integrity": "sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.69.0.tgz",
"integrity": "sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8511,13 +8523,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.68.0.tgz",
"integrity": "sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.69.0.tgz",
"integrity": "sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8528,13 +8543,16 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.68.0.tgz",
"integrity": "sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.69.0.tgz",
"integrity": "sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8545,13 +8563,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.68.0.tgz",
"integrity": "sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.69.0.tgz",
"integrity": "sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8562,13 +8583,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.68.0.tgz",
"integrity": "sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.69.0.tgz",
"integrity": "sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8579,9 +8603,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.68.0.tgz",
"integrity": "sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.69.0.tgz",
"integrity": "sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==",
"cpu": [
"arm64"
],
@@ -8596,9 +8620,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.68.0.tgz",
"integrity": "sha512-fHNtVqPHSYE7UFDSLVFUjxQjnSVXxseNJmRW+XuP4pXXDwePdPda43NL7/BBCFTxHjycOc44JNDaOPtFDNui9A==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.69.0.tgz",
"integrity": "sha512-9RExaLgmaw6IoIkU9cTpT71mLfI0xZ86iZH8x518LVsOkjquJMYqb9P7KpC8lgd1t0Dxs41p2pxynq4XR3Ttzw==",
"cpu": [
"arm64"
],
@@ -8613,9 +8637,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.68.0.tgz",
"integrity": "sha512-NnKXr4Wgo4nps3erhrE0f8shBvBPZMHg72nDsvX0JyrRvsNiP3f1JNvbCKh+A6VFvpF7ZoJxu904P3cKMhvZnA==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.69.0.tgz",
"integrity": "sha512-1907kRPF8/PrcIw1E7LMs9JbVrpgnt/MvFdss3an8oDkYNAACXzTntV3t3869ZZhMZxb2AzRGbz1pA/jdFatXA==",
"cpu": [
"ia32"
],
@@ -8630,9 +8654,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.68.0.tgz",
"integrity": "sha512-zg5pA+84AlU6XHJ3ruiRxziO71QTrz8nLsk6u01JGS5+tL9/bnlakFiklFrcy4R1/V7ktWtaNitN3JZWmKnf6g==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.69.0.tgz",
"integrity": "sha512-w8SOXv3mT9Fi6jY8OXdXCfnvX/3KNLXGNr4HEz2TA7S4Mv/PYAOmpB8y/ge40mxvBMgGNaSaaDwZpAsQn7HtWA==",
"cpu": [
"x64"
],
@@ -20887,9 +20911,9 @@
}
},
"node_modules/fuse.js": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.1.tgz",
"integrity": "sha512-AY7lKAXK71hi3WgUvDy6oZL67UEHOOtvCAwVdOXHyJd6ZzftBy7QqxuXt4HxmmAhYjmp/YCuOELZtIvAdlZ+fw==",
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.2.tgz",
"integrity": "sha512-LVbzjD4WA6UP5B1UnP8wuaXJiLnqMdM/E4fiJXTJ5haJ5b/MBNsK29h2fm6swEoQaVQjvYFWKLE2RanyZIoRVQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
@@ -21856,9 +21880,9 @@
}
},
"node_modules/google-auth-library": {
"version": "10.6.2",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
"integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz",
"integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
@@ -32587,9 +32611,9 @@
}
},
"node_modules/oxlint": {
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.68.0.tgz",
"integrity": "sha512-dXcbq+xsmLrMy6T8d0euf3IYUfLmjHIE11pOxiUSi5LHkFZaYPv568R6sEjcavVpUxoaQe66UBuK4HEi74NxpA==",
"version": "1.69.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.69.0.tgz",
"integrity": "sha512-ypZkK/aDc5NQV8zIR6s2H2Tl3aNW8FmJ1m9+2qsaYuRenl8vgnHNCGwTHviWJdUQzglOlHFchgopdtGhSy17Rw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -32602,25 +32626,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxlint/binding-android-arm-eabi": "1.68.0",
"@oxlint/binding-android-arm64": "1.68.0",
"@oxlint/binding-darwin-arm64": "1.68.0",
"@oxlint/binding-darwin-x64": "1.68.0",
"@oxlint/binding-freebsd-x64": "1.68.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.68.0",
"@oxlint/binding-linux-arm-musleabihf": "1.68.0",
"@oxlint/binding-linux-arm64-gnu": "1.68.0",
"@oxlint/binding-linux-arm64-musl": "1.68.0",
"@oxlint/binding-linux-ppc64-gnu": "1.68.0",
"@oxlint/binding-linux-riscv64-gnu": "1.68.0",
"@oxlint/binding-linux-riscv64-musl": "1.68.0",
"@oxlint/binding-linux-s390x-gnu": "1.68.0",
"@oxlint/binding-linux-x64-gnu": "1.68.0",
"@oxlint/binding-linux-x64-musl": "1.68.0",
"@oxlint/binding-openharmony-arm64": "1.68.0",
"@oxlint/binding-win32-arm64-msvc": "1.68.0",
"@oxlint/binding-win32-ia32-msvc": "1.68.0",
"@oxlint/binding-win32-x64-msvc": "1.68.0"
"@oxlint/binding-android-arm-eabi": "1.69.0",
"@oxlint/binding-android-arm64": "1.69.0",
"@oxlint/binding-darwin-arm64": "1.69.0",
"@oxlint/binding-darwin-x64": "1.69.0",
"@oxlint/binding-freebsd-x64": "1.69.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.69.0",
"@oxlint/binding-linux-arm-musleabihf": "1.69.0",
"@oxlint/binding-linux-arm64-gnu": "1.69.0",
"@oxlint/binding-linux-arm64-musl": "1.69.0",
"@oxlint/binding-linux-ppc64-gnu": "1.69.0",
"@oxlint/binding-linux-riscv64-gnu": "1.69.0",
"@oxlint/binding-linux-riscv64-musl": "1.69.0",
"@oxlint/binding-linux-s390x-gnu": "1.69.0",
"@oxlint/binding-linux-x64-gnu": "1.69.0",
"@oxlint/binding-linux-x64-musl": "1.69.0",
"@oxlint/binding-openharmony-arm64": "1.69.0",
"@oxlint/binding-win32-arm64-msvc": "1.69.0",
"@oxlint/binding-win32-ia32-msvc": "1.69.0",
"@oxlint/binding-win32-x64-msvc": "1.69.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=0.22.1",
@@ -44462,7 +44486,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.8",
"dompurify": "^3.4.9",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",
@@ -44583,9 +44607,9 @@
}
},
"packages/superset-ui-core/node_modules/dompurify": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
"integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz",
"integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -44946,7 +44970,7 @@
"dependencies": {
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^3.4.8",
"dompurify": "^3.4.9",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"nvd3-fork": "^2.0.5",
@@ -44962,9 +44986,9 @@
}
},
"plugins/legacy-preset-chart-nvd3/node_modules/dompurify": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
"integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz",
"integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"

View File

@@ -178,14 +178,14 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.4.1",
"fuse.js": "^7.4.2",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"google-auth-library": "^10.7.0",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -261,7 +261,7 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.13",
"@formatjs/intl-durationformat": "^0.10.14",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
@@ -344,7 +344,7 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.68.0",
"oxlint": "^1.69.0",
"po2json": "^0.4.5",
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",

View File

@@ -43,7 +43,7 @@
"d3-time": "^3.1.0",
"d3-time-format": "^4.1.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.8",
"dompurify": "^3.4.9",
"fetch-retry": "^6.0.0",
"handlebars": "^4.7.9",
"jed": "^1.1.1",

View File

@@ -34,7 +34,7 @@
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.18.1",
"nvd3-fork": "^2.0.5",
"dompurify": "^3.4.8",
"dompurify": "^3.4.9",
"prop-types": "^15.8.1",
"urijs": "^1.19.11"
},

View File

@@ -21,6 +21,7 @@ import { css, styled } from '@apache-superset/core/theme';
export default styled.div`
${({ theme }) => css`
/* Base table styles */
padding: ${theme.sizeUnit * 5}px;
table {
width: 100%;
min-width: auto;

View File

@@ -1613,8 +1613,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
pageSize={pageSize}
serverPaginationData={serverPaginationData}
pageSizeOptions={pageSizeOptions}
width={widthFromState}
height={heightFromState}
width={Math.max(0, widthFromState - theme.sizeUnit * 10)}
height={Math.max(0, heightFromState - theme.sizeUnit * 10)}
serverPagination={serverPagination}
onServerPaginationChange={handleServerPaginationChange}
onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)}

View File

@@ -17,7 +17,11 @@
* under the License.
*/
import type { ReactElement } from 'react';
import type { ControlPanelSectionConfig } from '@superset-ui/chart-controls';
import type {
ControlPanelSectionConfig,
CustomControlItem,
} from '@superset-ui/chart-controls';
import { isCustomControlItem } from '@superset-ui/chart-controls';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render } from '@testing-library/react';
import { SqlaFormData } from '@superset-ui/core';
@@ -28,6 +32,7 @@ import DeckGLGeoJson, {
computeGeoJsonIconOptionsFromJsOutput,
computeGeoJsonIconOptionsFromFormData,
getPoints,
getLayer,
} from './Geojson';
import controlPanel from './controlPanel';
@@ -295,3 +300,158 @@ test('DeckGLGeoJson falls back to legacy map_style when provider-specific style
}),
);
});
const baseFormData: SqlaFormData = {
datasource: 'test_datasource',
viz_type: 'deck_geojson',
slice_id: 1,
fill_color_picker: { r: 0, g: 0, b: 255, a: 1 },
stroke_color_picker: { r: 0, g: 0, b: 0, a: 1 },
};
const baseLayerArgs = {
onContextMenu: jest.fn(),
filterState: undefined,
setDataMask: jest.fn(),
payload: { data: { type: 'FeatureCollection', features: [] } },
setTooltip: jest.fn(),
emitCrossFilters: false,
};
test('getLayer preserves rendering for existing charts without new point radius fields', () => {
// Simulate form data from an existing chart that only has point_radius_scale
const legacyFormData = {
...baseFormData,
point_radius_scale: 200,
// point_radius and point_radius_units intentionally absent
};
const layer = getLayer({ formData: legacyFormData, ...baseLayerArgs });
const { props } = layer;
// Should match deck.gl defaults, NOT the new control panel defaults
expect(props.getPointRadius).toBe(1); // deck.gl default, not 10
expect(props.pointRadiusUnits).toBe('meters'); // deck.gl default, not 'pixels'
expect(props.pointRadiusScale).toBe(200); // user's saved value preserved
});
test('getLayer uses control panel defaults for new charts', () => {
const newChartFormData = {
...baseFormData,
point_radius: 10,
point_radius_units: 'pixels',
point_radius_scale: 1,
};
const layer = getLayer({ formData: newChartFormData, ...baseLayerArgs });
const { props } = layer;
expect(props.getPointRadius).toBe(10);
expect(props.pointRadiusUnits).toBe('pixels');
expect(props.pointRadiusScale).toBe(1);
});
test('getLayer falls back to defaults when legacy fields are null', () => {
// The old point_radius_scale control had `default: null`, so legacy charts
// can have null persisted; it must fall back to 1, not coerce to 0.
const nullFormData = {
...baseFormData,
point_radius: null,
point_radius_scale: null,
};
const layer = getLayer({ formData: nullFormData, ...baseLayerArgs });
const { props } = layer;
expect(props.getPointRadius).toBe(1);
expect(props.pointRadiusScale).toBe(1);
});
test('getLayer preserves an explicit zero radius scale', () => {
const zeroFormData = {
...baseFormData,
point_radius_scale: 0,
};
const layer = getLayer({ formData: zeroFormData, ...baseLayerArgs });
const { props } = layer;
expect(props.pointRadiusScale).toBe(0);
});
test('getLayer coerces free-form string radius values to numbers', () => {
// Free-form SelectControls can store user-typed values as strings
const stringFormData = {
...baseFormData,
point_radius: '3',
point_radius_scale: '0.25',
};
const layer = getLayer({ formData: stringFormData, ...baseLayerArgs });
const { props } = layer;
expect(props.getPointRadius).toBe(3);
expect(props.pointRadiusScale).toBe(0.25);
});
type ControlConfig = {
default?: unknown;
validators?: unknown[];
choices?: [unknown, unknown][];
renderTrigger?: boolean;
};
const controlItems = controlPanel.controlPanelSections
.filter(
(s: ControlPanelSectionConfig | null): s is ControlPanelSectionConfig =>
s !== null,
)
.flatMap((section: ControlPanelSectionConfig) => section.controlSetRows)
.flat();
const findControlConfig = (name: string): ControlConfig | undefined =>
(controlItems.filter(isCustomControlItem) as CustomControlItem[]).find(
(item: CustomControlItem) => item.name === name,
)?.config as ControlConfig | undefined;
test('controlPanel exposes a Point Radius control defaulting to 10', () => {
const config = findControlConfig('point_radius');
expect(config).toBeDefined();
expect(config?.default).toBe(10);
expect(config?.renderTrigger).toBe(true);
expect(config?.validators).toHaveLength(1);
expect(config?.choices).toEqual(
expect.arrayContaining([
[1, '1'],
[10, '10'],
[100, '100'],
]),
);
});
test('controlPanel Point Radius Scale defaults to 1 with fractional choices', () => {
const config = findControlConfig('point_radius_scale');
expect(config).toBeDefined();
expect(config?.default).toBe(1);
expect(config?.renderTrigger).toBe(true);
expect(config?.validators).toHaveLength(1);
expect(config?.choices).toEqual(
expect.arrayContaining([
[0.1, '0.1'],
[1, '1'],
[10, '10'],
]),
);
});
test('controlPanel Point Radius Units defaults to pixels', () => {
const config = findControlConfig('point_radius_units');
expect(config).toBeDefined();
expect(config?.default).toBe('pixels');
expect(config?.renderTrigger).toBe(true);
expect(config?.choices?.map(([value]) => value)).toEqual([
'pixels',
'meters',
'common',
]);
});

View File

@@ -254,6 +254,15 @@ export const computeGeoJsonIconOptionsFromFormData = (
iconSizeUnits: fd.icon_size_unit,
});
// Free-form SelectControls can yield string values, and legacy charts may have
// null persisted for these fields, so coerce to a number (falling back to the
// provided default for null/undefined/NaN input, while preserving an explicit 0)
// before handing them to deck.gl's numeric layer props.
const toNumber = (value: unknown, fallback: number) => {
const num = Number(value ?? fallback);
return Number.isFinite(num) ? num : fallback;
};
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
formData,
onContextMenu,
@@ -328,7 +337,11 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
getFillColor(feature, filterState?.value),
getLineColor,
getLineWidth: fd.line_width || 1,
pointRadiusScale: fd.point_radius_scale,
// Use deck.gl defaults as fallbacks for backward compatibility with existing charts.
// New charts will get control panel defaults (point_radius=10, units='pixels', scale=1).
getPointRadius: toNumber(fd.point_radius, 1),
pointRadiusUnits: fd.point_radius_units ?? 'meters',
pointRadiusScale: toNumber(fd.point_radius_scale, 1),
lineWidthUnits: fd.line_width_unit,
pointType,
...labelOpts,

View File

@@ -22,6 +22,8 @@ import {
legacyValidateInteger,
isFeatureEnabled,
FeatureFlag,
validateNumber,
validateInteger,
} from '@superset-ui/core';
import { formatSelectOptions } from '../../utilities/utils';
import {
@@ -352,15 +354,56 @@ const config: ControlPanelConfig = {
},
],
[
{
name: 'point_radius',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Point Radius'),
description: t(
'The radius of point features, in the units specified below. ' +
'The final rendered size is this value multiplied by Point Radius Scale.',
),
validators: [validateInteger],
default: 10,
choices: formatSelectOptions([1, 5, 10, 20, 50, 100]),
renderTrigger: true,
},
},
{
name: 'point_radius_scale',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Point Radius Scale'),
validators: [legacyValidateInteger],
default: null,
choices: formatSelectOptions([0, 100, 200, 300, 500]),
description: t(
'A multiplier applied to the point radius. ' +
'Use this to uniformly scale all points.',
),
validators: [validateNumber],
default: 1,
choices: formatSelectOptions([0.1, 0.5, 1, 2, 5, 10]),
renderTrigger: true,
},
},
],
[
{
name: 'point_radius_units',
config: {
type: 'SelectControl',
label: t('Point Radius Units'),
description: t(
'The unit for point radius. Use "pixels" for consistent ' +
'screen-space sizing regardless of zoom level.',
),
default: 'pixels',
choices: [
['pixels', t('Pixels')],
['meters', t('Meters')],
['common', t('Common (unit per pixel at zoom 0)')],
],
renderTrigger: true,
},
},
],

View File

@@ -30,9 +30,10 @@ import {
import { EditorHost } from 'src/core/editors';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
const StyledEditorHost = styled(EditorHost)`
&.ace_editor {
const EditorOutline = styled.div`
& .ace_editor {
border: 1px solid ${({ theme }) => theme.colorBorder};
border-radius: ${({ theme }) => theme.borderRadius}px;
}
`;
@@ -87,14 +88,16 @@ const TemplateParamsEditor = ({
</a>{' '}
{t('syntax.')}
</StyledParagraph>
<StyledEditorHost
id={`template-params-${queryEditorId}`}
height="800px"
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
language={language === 'yaml' ? 'yaml' : 'json'}
width="100%"
value={code}
/>
<EditorOutline>
<EditorHost
id={`template-params-${queryEditorId}`}
height="360px"
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
language={language === 'yaml' ? 'yaml' : 'json'}
width="100%"
value={code}
/>
</EditorOutline>
</div>
);

View File

@@ -295,6 +295,7 @@ export interface ListViewProps<T extends object = any> {
name: ReactNode;
onSelect: (rows: any[]) => any;
type?: 'primary' | 'secondary' | 'danger';
hidden?: (rows: any[]) => boolean;
}>;
bulkSelectEnabled?: boolean;
disableBulkSelect?: () => void;
@@ -509,7 +510,16 @@ export function ListView<T extends object = any>({
{t('Deselect all')}
</span>
<div className="divider" />
{bulkActions.map(action => (
{bulkActions
.filter(
action =>
!action.hidden?.(
selectedFlatRows.map(
(r: any) => r.original,
),
),
)
.map(action => (
<Button
data-test="bulk-select-action"
data-test-action-key={action.key}

View File

@@ -299,7 +299,10 @@ test('checkIsApplyDisabled returns true when required filter is missing value in
);
});
test('checkIsApplyDisabled handles filter count mismatch', () => {
test('checkIsApplyDisabled enables Apply when Selected has a filter value not yet in Applied', () => {
// Regression: when a required filter's default isn't applied (Applied missing
// the entry) and the user types a value, Selected gains an entry Applied
// doesn't have. Apply must be enabled so the user can commit the value.
const dataMaskSelected: DataMaskStateWithId = {
'filter-1': {
id: 'filter-1',
@@ -322,7 +325,7 @@ test('checkIsApplyDisabled handles filter count mismatch', () => {
const filters = [createFilter('filter-1'), createFilter('filter-2')];
expect(checkIsApplyDisabled(dataMaskSelected, dataMaskApplied, filters)).toBe(
true,
false,
);
});

View File

@@ -74,13 +74,9 @@ export const checkIsApplyDisabled = (
const selectedExtraFormData = getOnlyExtraFormData(dataMaskSelected);
const appliedExtraFormData = getOnlyExtraFormData(dataMaskApplied);
// Check counts first
const selectedCount = Object.keys(selectedExtraFormData).length;
const appliedCount = Object.keys(appliedExtraFormData).length;
if (selectedCount !== appliedCount) return true;
// Check for changes
// Check for changes. ignoreUndefined drops empty keys on both sides so that
// a filter present in Selected with a real value but absent (or undefined)
// in Applied is correctly detected as a change.
const dataEqual = areObjectsEqual(
selectedExtraFormData,
appliedExtraFormData,

View File

@@ -42,7 +42,9 @@ const isUserDashboardOwner = (
user: UserWithPermissionsAndRoles | UndefinedUser,
) =>
isUserWithPermissionsAndRoles(user) &&
dashboard.owners.some(owner => owner.id === user.userId);
[...dashboard.owners, ...(dashboard.extra_owners ?? [])].some(
owner => owner.id === user.userId,
);
export const canUserEditDashboard = (
dashboard: Dashboard,

View File

@@ -121,6 +121,38 @@ test('when user edits a filter without changing targets, their selection is pres
);
});
test('when a required range filter was cleared to [null, null], modifying it applies the new default instead of the cleared state', () => {
// Regression for the PR #40470 review: [null, null] is a range filter's
// canonical "cleared" value. It must count as "no value" so the empty state
// does not wipe a newly-defined default — consistent with `loadedHasValue`
// in fillNativeFilters.
const initialState: DataMaskStateWithId = {
'NATIVE_FILTER-1': {
id: 'NATIVE_FILTER-1',
...getInitialDataMask('NATIVE_FILTER-1'),
filterState: { value: [null, null] },
},
};
const oldFilters = {
'NATIVE_FILTER-1': createFilter('NATIVE_FILTER-1', 'col_a', {
enableEmptyFilter: true,
}),
};
const modifiedFilter: Filter = {
...createFilter('NATIVE_FILTER-1', 'col_a', { enableEmptyFilter: true }),
defaultDataMask: { filterState: { value: [10, 20] } },
};
const action = createModifyAction(modifiedFilter, oldFilters);
const result = reducer(initialState, action);
// The cleared [null, null] state must not be preserved; the new default wins.
expect(result['NATIVE_FILTER-1']?.filterState?.value).toEqual([10, 20]);
});
// Runtime data from the server can contain null entries in
// chart_customization_config even though the TS type does not include | null
// yet. These helpers build HYDRATE_DASHBOARD actions that mirror that reality.

View File

@@ -109,10 +109,50 @@ function fillNativeFilters(
) {
filterConfig.forEach((filter: Filter) => {
const dataMask = initialDataMask || {};
const loaded = dataMask[filter.id];
// The shallow spread of `loaded` would override `filter.defaultDataMask`
// even when the loaded mask is incomplete (e.g. a permalink captured
// mid-initialization), wiping out a valid default. For REQUIRED filters
// with a default, fall back to the default when the loaded mask carries
// no real value OR is missing the extraFormData needed to filter charts.
// Non-required filters keep current behavior so a user's explicit clear
// isn't undone.
const isRequired = !!filter.controlValues?.enableEmptyFilter;
const loadedValue = loaded?.filterState?.value;
const loadedHasValue =
loadedValue !== undefined &&
loadedValue !== null &&
!(
// Treat all-null arrays (range filters use [null, null] as their
// canonical cleared value) and empty arrays as "no value".
(Array.isArray(loadedValue) && loadedValue.every(v => v === null))
);
const loadedHasExtraFormData =
!!loaded?.extraFormData && Object.keys(loaded.extraFormData).length > 0;
const defaultHasExtraFormData =
!!filter.defaultDataMask?.extraFormData &&
Object.keys(filter.defaultDataMask.extraFormData).length > 0;
// Restore when:
// (1) loaded value is empty — classic "default wiped by stale permalink", OR
// (2) loaded has a value but no extraFormData and the default does — the
// "value present in UI but not applied to charts" gap-window case where
// a permalink was captured before FilterValue produced extraFormData.
const shouldRestoreDefault =
isRequired &&
!!filter.defaultDataMask &&
(!loadedHasValue || (!loadedHasExtraFormData && defaultHasExtraFormData));
mergedDataMask[filter.id] = {
...getInitialDataMask(filter.id), // take initial data
...filter.defaultDataMask, // if something new came from BE - take it
...dataMask[filter.id],
...loaded,
...(shouldRestoreDefault
? {
filterState: filter.defaultDataMask?.filterState,
extraFormData: filter.defaultDataMask?.extraFormData,
}
: {}),
};
if (
currentFilters &&
@@ -155,12 +195,28 @@ function updateDataMaskForFilterChanges(
// Check if targets are equal
const areTargetsEqual = isEqual(prevFilterDef?.targets, filter?.targets);
// Preserve state only if filter exists, has enableEmptyFilter=true and targets match
// For required filters, only preserve existing state when it actually has
// a value — otherwise the empty existing state would wipe the (possibly
// newly-defined) default. defaultToFirstItem filters keep the old behavior:
// FilterValue re-resolves the first item at runtime, so preserving the
// mid-init empty state is fine.
const isRequired = !!filter.controlValues?.enableEmptyFilter;
const isFirstItem = !!filter.controlValues?.defaultToFirstItem;
const existingValue = existingFilter?.filterState?.value;
const hasExistingValue =
existingValue !== undefined &&
existingValue !== null &&
// Treat all-null arrays (range filters use [null, null] as their
// canonical cleared value) and empty arrays as "no value", consistent
// with `loadedHasValue` in fillNativeFilters above. `[].every()` is
// true, so this also covers the empty-array case.
!(Array.isArray(existingValue) && existingValue.every(v => v === null));
const shouldPreserveState =
existingFilter &&
areTargetsEqual &&
(filter.controlValues?.enableEmptyFilter ||
filter.controlValues?.defaultToFirstItem);
(isRequired || isFirstItem) &&
(isFirstItem || hasExistingValue);
mergedDataMask[filter.id] = {
...getInitialDataMask(filter.id),

View File

@@ -177,9 +177,11 @@ export const hydrateExplore =
can_copy_clipboard: granularExport
? findPermission('can_copy_clipboard', 'Superset', user?.roles)
: findPermission('can_csv', 'Superset', user?.roles),
can_overwrite: ensureIsArray(slice?.owners).includes(
user?.userId as number,
),
can_overwrite:
ensureIsArray(slice?.owners).includes(user?.userId as number) ||
ensureIsArray(metadata?.extra_owners).some(
(o: { id: number }) => o.id === user?.userId,
),
isDatasourceMetaLoading: false,
isStarred: false,
triggerRender: false,

View File

@@ -75,6 +75,7 @@ interface SaveModalProps extends RouteComponentProps {
alert?: string;
sliceName?: string;
slice?: Record<string, any>;
can_overwrite?: boolean;
datasource?: Record<string, any>;
dashboardId: '' | number | null;
isVisible: boolean;
@@ -128,7 +129,8 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
canOverwriteSlice(): boolean {
return (
(isUserAdmin(this.props.user) ||
(this.props.can_overwrite ||
isUserAdmin(this.props.user) ||
this.props.slice?.owners?.includes(this.props.user.userId)) &&
!this.props.slice?.is_managed_externally
);
@@ -819,6 +821,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
interface StateProps {
datasource: any;
slice: any;
can_overwrite: boolean;
user: UserWithPermissionsAndRoles;
dashboards: any;
alert: any;
@@ -833,6 +836,7 @@ function mapStateToProps({
return {
datasource: explore.datasource,
slice: explore.slice,
can_overwrite: explore.can_overwrite,
user,
dashboards: saveModal.dashboards,
alert: saveModal.saveModalAlert,

View File

@@ -88,6 +88,7 @@ export interface ExplorePageInitialData {
created_on_humanized: string;
changed_on_humanized: string;
owners: string[];
extra_owners?: { id: number; first_name: string; last_name: string }[];
created_by?: string;
changed_by?: string;
color_namespace?: string;

View File

@@ -34,6 +34,7 @@ export interface Dashboard {
changed_on: string;
charts: string[]; // just chart names, unfortunately...
owners: Owner[];
extra_owners?: Owner[];
roles: Role[];
theme?: {
id: number;

View File

@@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"cookie": "^1.1.1",
"hot-shots": "^15.0.0",
"hot-shots": "^16.0.0",
"ioredis": "^5.11.1",
"jsonwebtoken": "^9.0.3",
"lodash": "^4.18.1",
@@ -26,7 +26,7 @@
"@types/node": "^25.9.2",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
@@ -37,7 +37,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1"
"typescript-eslint": "^8.61.0"
},
"engines": {
"node": "^24.16.0",
@@ -1844,17 +1844,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
"integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/type-utils": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/type-utils": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1867,7 +1867,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1883,16 +1883,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
"integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1908,14 +1908,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
"integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.60.1",
"@typescript-eslint/types": "^8.60.1",
"@typescript-eslint/tsconfig-utils": "^8.61.0",
"@typescript-eslint/types": "^8.61.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1930,14 +1930,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
"integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1"
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1948,9 +1948,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
"integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1965,15 +1965,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
"integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1990,9 +1990,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
"integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2004,16 +2004,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
"integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.60.1",
"@typescript-eslint/tsconfig-utils": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/project-service": "8.61.0",
"@typescript-eslint/tsconfig-utils": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2071,16 +2071,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
"integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1"
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2095,13 +2095,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
"integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -3465,9 +3465,9 @@
}
},
"node_modules/hot-shots": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-15.0.0.tgz",
"integrity": "sha512-89EmKbvjVbDdFmUcvMl1x9XaKdEzg1VDLElbKaQCPC88wrus6O5XlCyZ+KbwZk9Dy4BNcsyfEHMfSkUtRZHBQg==",
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-16.0.0.tgz",
"integrity": "sha512-1WHuq7vv0Hj6wiSmR89XpxxiNnw9s1W50yGJExC3/PSqVv+Kr7GSk3rz0jsTWjhIkF1c5Nz9mpLdzJ+CqKKwMg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -6188,16 +6188,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
"integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.60.1",
"@typescript-eslint/parser": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1"
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7927,16 +7927,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
"integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/type-utils": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/type-utils": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -7951,75 +7951,75 @@
}
},
"@typescript-eslint/parser": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
"integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3"
}
},
"@typescript-eslint/project-service": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
"integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.60.1",
"@typescript-eslint/types": "^8.60.1",
"@typescript-eslint/tsconfig-utils": "^8.61.0",
"@typescript-eslint/types": "^8.61.0",
"debug": "^4.4.3"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
"integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1"
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
"integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
"integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
}
},
"@typescript-eslint/types": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
"integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
"integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
"dev": true,
"requires": {
"@typescript-eslint/project-service": "8.60.1",
"@typescript-eslint/tsconfig-utils": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/project-service": "8.61.0",
"@typescript-eslint/tsconfig-utils": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -8054,24 +8054,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
"integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1"
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
"integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"eslint-visitor-keys": "^5.0.0"
},
"dependencies": {
@@ -9020,9 +9020,9 @@
}
},
"hot-shots": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-15.0.0.tgz",
"integrity": "sha512-89EmKbvjVbDdFmUcvMl1x9XaKdEzg1VDLElbKaQCPC88wrus6O5XlCyZ+KbwZk9Dy4BNcsyfEHMfSkUtRZHBQg==",
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-16.0.0.tgz",
"integrity": "sha512-1WHuq7vv0Hj6wiSmR89XpxxiNnw9s1W50yGJExC3/PSqVv+Kr7GSk3rz0jsTWjhIkF1c5Nz9mpLdzJ+CqKKwMg==",
"requires": {
"unix-dgram": "2.x"
}
@@ -11021,15 +11021,15 @@
"dev": true
},
"typescript-eslint": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
"integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "8.60.1",
"@typescript-eslint/parser": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1"
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0"
}
},
"uglify-js": {

View File

@@ -18,7 +18,7 @@
"license": "Apache-2.0",
"dependencies": {
"cookie": "^1.1.1",
"hot-shots": "^15.0.0",
"hot-shots": "^16.0.0",
"ioredis": "^5.11.1",
"jsonwebtoken": "^9.0.3",
"lodash": "^4.18.1",
@@ -34,7 +34,7 @@
"@types/node": "^25.9.2",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
@@ -45,7 +45,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1"
"typescript-eslint": "^8.61.0"
},
"engines": {
"node": "^24.16.0",

View File

@@ -21,7 +21,7 @@ from io import BytesIO
from typing import Any, cast, Optional
from zipfile import is_zipfile, ZipFile
from flask import redirect, request, Response, send_file, url_for
from flask import current_app, redirect, request, Response, send_file, url_for
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
@@ -310,6 +310,8 @@ class ChartRestApi(BaseSupersetModelRestApi):
try:
dash = ChartDAO.get_by_id_or_uuid(id_or_uuid)
result = self.chart_get_response_schema.dump(dash)
if resolver := current_app.config.get("EXTRA_OWNERS_RESOLVER"):
result["extra_owners"] = resolver(dash)
return self.response(200, result=result)
except ChartNotFoundError:
return self.response_404()

View File

@@ -16,6 +16,7 @@
# under the License.
from typing import Any
from flask import current_app
from flask_babel import lazy_gettext as _
from sqlalchemy import and_, or_
from sqlalchemy.orm import aliased
@@ -109,7 +110,22 @@ class ChartFilter(BaseFilter): # pylint: disable=too-few-public-methods
query = query.join(
models.Database, table_alias.database_id == models.Database.id
)
return query.filter(get_dataset_access_filters(self.model))
extra_access_filters = []
extra_filters = current_app.config.get("EXTRA_ACCESS_QUERY_FILTERS", {})
if extra_charts_filter := extra_filters.get("charts"):
user_id = get_user_id()
if user_id:
extra_access_filters.append(
self.model.id.in_(extra_charts_filter(user_id))
)
return query.filter(
or_(
get_dataset_access_filters(self.model),
*extra_access_filters,
)
)
class ChartHasCreatedByFilter(BaseFilter): # pylint: disable=too-few-public-methods

View File

@@ -19,7 +19,7 @@ from datetime import datetime
from functools import partial
from typing import Any, Optional
from flask import g
from flask import current_app, g
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
@@ -58,7 +58,10 @@ class CreateChartCommand(CreateMixin, BaseCommand):
self.validate()
self._properties["last_saved_at"] = datetime.now()
self._properties["last_saved_by"] = g.user
return ChartDAO.create(attributes=self._properties)
chart = ChartDAO.create(attributes=self._properties)
if after_create := current_app.config.get("AFTER_ASSET_CREATE"):
after_create(chart, "chart")
return chart
def validate(self) -> None:
exceptions = []

View File

@@ -18,6 +18,7 @@ import logging
from functools import partial
from typing import Any, Optional
from flask import current_app
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
@@ -52,6 +53,8 @@ class CreateDashboardCommand(CreateMixin, BaseCommand):
dashboard,
data=json.loads(json_metadata),
)
if after_create := current_app.config.get("AFTER_ASSET_CREATE"):
after_create(dashboard, "dashboard")
return dashboard
def validate(self) -> None:

View File

@@ -19,7 +19,7 @@ import logging
from abc import ABC
from typing import Any, cast, Optional
from flask import request
from flask import current_app, request
from flask_babel import lazy_gettext as _
from sqlalchemy.exc import SQLAlchemyError
@@ -160,10 +160,15 @@ class GetExploreCommand(BaseCommand, ABC):
metadata = None
if slc:
extra_owners = []
if resolver := current_app.config.get("EXTRA_OWNERS_RESOLVER"):
extra_owners = resolver(slc)
metadata = {
"created_on_humanized": slc.created_on_humanized,
"changed_on_humanized": slc.changed_on_humanized,
"owners": [owner.get_full_name() for owner in slc.owners],
"extra_owners": extra_owners,
"dashboards": [
{"id": dashboard.id, "dashboard_title": dashboard.dashboard_title}
for dashboard in slc.dashboards

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
from collections import Counter
from typing import Any, Optional, TYPE_CHECKING
from flask import g
from flask import current_app, g
from flask_appbuilder.security.sqla.models import Role, User
from superset import security_manager
@@ -58,8 +58,11 @@ def populate_owner_list(
if not owner_ids and default_to_user:
return [g.user]
if not (security_manager.is_admin() or get_user_id() in owner_ids):
# make sure non-admins can't remove themselves as owner by mistake
owners.append(g.user)
# Make sure non-admins can't remove themselves as owner by mistake.
# Skip auto-add when an EXTRA_OWNERS_RESOLVER is configured — the
# resolver handles access independently of the owners list.
if not current_app.config.get("EXTRA_OWNERS_RESOLVER"):
owners.append(g.user)
for owner_id in owner_ids:
owner = security_manager.get_user_by_id(owner_id)
if not owner:

View File

@@ -2632,6 +2632,27 @@ class ExtraDynamicQueryFilters(TypedDict, total=False):
EXTRA_DYNAMIC_QUERY_FILTERS: ExtraDynamicQueryFilters = {}
# Extra access query filters inject additional OR conditions into
# ChartFilter and DashboardAccessFilter, enabling external systems
# (e.g. folder permissions) to grant asset visibility.
# The callable receives the current user ID and returns a subquery of asset IDs.
class ExtraAccessQueryFilters(TypedDict, total=False):
charts: Callable[[int], Query]
dashboards: Callable[[int], Query]
# Extension hooks for deployments to plug in custom access logic.
# Additional query filters for chart/dashboard list views.
EXTRA_ACCESS_QUERY_FILTERS: ExtraAccessQueryFilters = {}
# Bypass raise_for_access for specific assets. Return True to skip checks.
EXTRA_RAISE_FOR_ACCESS_BYPASS: Callable[..., bool] | None = None
# Resolve extra owners for a resource. Also used for ownership checks and
# to skip auto-adding the current user to owners on create.
EXTRA_OWNERS_RESOLVER: Callable[..., list[Any]] | None = None
# Post-create hook for charts/dashboards. Receives (model, asset_type).
AFTER_ASSET_CREATE: Callable[[Any, str], None] | None = None
# The migrations that add catalog permissions might take a considerably long time
# to execute as it has to create permissions to all schemas and catalogs from all
# other catalogs accessible by the credentials. This flag allows to skip the

View File

@@ -519,6 +519,8 @@ class DashboardRestApi(CustomTagsOptimizationMixin, BaseSupersetModelRestApi):
schema = self.dashboard_get_response_schema
result = schema.dump(dash)
if resolver := current_app.config.get("EXTRA_OWNERS_RESOLVER"):
result["extra_owners"] = resolver(dash)
add_extra_log_payload(
dashboard_id=dash.id, action=f"{self.__class__.__name__}.get"
)

View File

@@ -16,7 +16,7 @@
# under the License.
from typing import Any, Optional
from flask import g
from flask import current_app, g
from flask_appbuilder.security.sqla.models import Role
from flask_babel import lazy_gettext as _
from sqlalchemy import and_, or_
@@ -182,11 +182,21 @@ class DashboardAccessFilter(BaseFilter): # pylint: disable=too-few-public-metho
feature_flagged_filters.append(condition)
extra_access_filters = []
extra_filters = current_app.config.get("EXTRA_ACCESS_QUERY_FILTERS", {})
if extra_dashboards_filter := extra_filters.get("dashboards"):
user_id = get_user_id()
if user_id:
extra_access_filters.append(
Dashboard.id.in_(extra_dashboards_filter(user_id))
)
query = query.filter(
or_(
Dashboard.id.in_(owner_ids_query),
Dashboard.id.in_(datasource_perm_query),
*feature_flagged_filters,
*extra_access_filters,
)
)

View File

@@ -162,6 +162,7 @@ Alerts & Reports:
Dataset Management:
- list_datasets: List datasets with advanced filters (1-based pagination)
- get_dataset_info: Get detailed dataset information by ID (includes columns/metrics)
- create_dataset: Register a physical table as a dataset against an existing DB connection (requires write access)
- create_virtual_dataset: Save a SQL query as a virtual dataset for charting (requires write access)
- query_dataset: Query a dataset using its semantic layer (saved metrics, dimensions, filters) without needing a saved chart
@@ -422,7 +423,7 @@ Input format:
{_feature_availability}Permission Awareness:
{_instance_info_role_bullet}- ALWAYS check the user's roles BEFORE suggesting write operations (creating datasets,
charts, or dashboards). SQL execution is a separate permission — see execute_sql below.
- Write tools (generate_chart, generate_dashboard, update_chart, create_virtual_dataset,
- Write tools (generate_chart, generate_dashboard, update_chart, create_dataset, create_virtual_dataset,
save_sql_query, add_chart_to_existing_dashboard, update_chart_preview) require write
permissions. These tools are only listed for users who have the necessary access.
If a write tool does not appear in the tool list, the current user lacks write access.
@@ -631,9 +632,9 @@ def create_mcp_app(
# Create default MCP instance for backward compatibility
mcp = create_mcp_app()
# Initialize MCP dependency injection BEFORE importing tools/prompts
# This replaces the abstract @tool and @prompt decorators in superset_core.api.mcp
# with concrete implementations that can register with the mcp instance
# Initialize MCP dependency injection BEFORE importing tools/prompts.
# Replaces the stub @tool/@prompt decorators in superset_core.mcp.decorators
# with concrete implementations bound to this mcp instance.
from superset.core.mcp.core_mcp_injection import ( # noqa: E402
initialize_core_mcp_dependencies,
)
@@ -658,6 +659,7 @@ warnings.filterwarnings(
module=r"google\..*",
)
# Import all MCP tools to register them with the mcp instance
# NOTE: Always add new tool imports here when creating new MCP tools.
# Tools use the @tool decorator from `superset-core` and register automatically
@@ -698,6 +700,7 @@ from superset.mcp_service.database.tool import ( # noqa: F401, E402
list_databases,
)
from superset.mcp_service.dataset.tool import ( # noqa: F401, E402
create_dataset,
create_virtual_dataset,
get_dataset_info,
list_datasets,
@@ -835,8 +838,9 @@ def init_fastmcp_server(
Returns:
The global FastMCP instance configured with the provided settings
"""
# Read branding from Flask config's APP_NAME
from superset.mcp_service.flask_singleton import app as flask_app
# circular import: flask_singleton imports from superset.extensions which
# re-enters mcp_service during startup; must stay lazy inside the function.
from superset.mcp_service.flask_singleton import app as flask_app # noqa: PLC0415
# Derive branding from Superset's APP_NAME config (defaults to "Superset")
app_name = flask_app.config.get("APP_NAME", "Superset")

View File

@@ -21,7 +21,7 @@ Pydantic schemas for dataset-related responses
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timezone
from typing import Annotated, Any, Dict, List, Literal
from pydantic import (
@@ -325,8 +325,6 @@ class DatasetError(BaseModel):
@classmethod
def create(cls, error: str, error_type: str) -> "DatasetError":
"""Create a standardized DatasetError with timestamp."""
from datetime import datetime, timezone
return cls(
error=error,
error_type=error_type,
@@ -410,6 +408,76 @@ class GetDatasetInfoRequest(MetadataCacheControl):
return parsed
class CreateDatasetRequest(BaseModel):
"""Request schema for create_dataset to register a physical table as a dataset."""
model_config = ConfigDict(populate_by_name=True)
database_id: Annotated[
int,
Field(
description="ID of the database connection to register the table against"
),
]
schema_: Annotated[
str | None,
Field(
default=None,
alias="schema",
serialization_alias="schema",
max_length=250,
description="Schema (namespace) where the table lives, e.g. 'public'. "
"Omit or pass None for databases without schema namespaces (e.g. SQLite).",
),
]
catalog: Annotated[
str | None,
Field(
default=None,
max_length=250,
description="Catalog where the table lives. Omit for databases without "
"catalog support.",
),
]
table_name: Annotated[
str,
Field(
min_length=1,
max_length=250,
description="Name of the physical table to register as a dataset",
),
]
owners: Annotated[
List[int] | None,
Field(
default=None,
description="Optional list of owner user IDs. "
"Defaults to the calling user.",
),
]
@field_validator("schema_", "catalog", mode="before")
@classmethod
def _normalize_optional_str(cls, v: object) -> object:
"""Strip whitespace and convert blank strings to None.
Non-string values pass through unchanged so Pydantic's type validation
rejects them, rather than silently treating a malformed value (e.g. an
int or dict) as an omitted namespace.
"""
if isinstance(v, str):
return v.strip() or None
return v
@field_validator("table_name", mode="before")
@classmethod
def _strip_table_name(cls, v: object) -> object:
"""Strip leading/trailing whitespace from table_name."""
if isinstance(v, str):
return v.strip()
return v
class CreateVirtualDatasetRequest(BaseModel):
"""Request schema for create_virtual_dataset."""
@@ -734,7 +802,7 @@ def serialize_dataset_object(dataset: Any) -> DatasetInfo | None:
if isinstance(params, str):
try:
params = json.loads(params)
except Exception:
except (json.JSONDecodeError, TypeError):
params = None
columns = [
TableColumnInfo(

View File

@@ -15,14 +15,16 @@
# specific language governing permissions and limitations
# under the License.
from .create_dataset import create_dataset
from .create_virtual_dataset import create_virtual_dataset
from .get_dataset_info import get_dataset_info
from .list_datasets import list_datasets
from .query_dataset import query_dataset
__all__ = [
"create_dataset",
"create_virtual_dataset",
"list_datasets",
"get_dataset_info",
"list_datasets",
"query_dataset",
]

View File

@@ -0,0 +1,171 @@
# 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.
"""
Create dataset FastMCP tool
Registers a physical table as a Superset dataset against an existing
database connection — the programmatic equivalent of Data → Datasets → +Dataset.
Returns the same DatasetInfo shape as get_dataset_info so the caller can feed
the resulting dataset_id directly into generate_chart.
"""
import logging
from fastmcp import Context
from superset_core.mcp.decorators import tool, ToolAnnotations
from superset.commands.dataset.create import CreateDatasetCommand
from superset.commands.dataset.exceptions import (
DatasetCreateFailedError,
DatasetInvalidError,
)
from superset.extensions import event_logger
from superset.mcp_service.dataset.schemas import (
CreateDatasetRequest,
DatasetError,
DatasetInfo,
serialize_dataset_object,
)
logger = logging.getLogger(__name__)
def _classify_invalid_error(exc: DatasetInvalidError) -> DatasetError:
"""Map DatasetInvalidError sub-exceptions to typed DatasetError responses."""
classnames = exc.get_list_classnames()
messages = exc.normalized_messages()
if "DatabaseNotFoundValidationError" in classnames:
return DatasetError.create(
error="Database not found", error_type="DatabaseNotFoundError"
)
if "DatasetDataAccessIsNotAllowed" in classnames:
return DatasetError.create(
error="Access denied", error_type="AccessDeniedError"
)
if "DatasetExistsValidationError" in classnames:
return DatasetError.create(error=str(messages), error_type="DatasetExistsError")
if "TableNotFoundValidationError" in classnames:
return DatasetError.create(error=str(messages), error_type="TableNotFoundError")
# Other DatasetInvalidError sub-types are returned as generic ValidationError.
# Add explicit branches here when callers need to distinguish them.
return DatasetError.create(error=str(messages), error_type="ValidationError")
@tool(
tags=["mutate"],
class_permission_name="Dataset",
method_permission_name="write",
annotations=ToolAnnotations(
title="Register physical table as dataset",
readOnlyHint=False,
destructiveHint=False,
),
)
async def create_dataset(
request: CreateDatasetRequest, ctx: Context
) -> DatasetInfo | DatasetError:
"""Register a physical table as a Superset dataset.
Wraps POST /api/v1/dataset/ — the same endpoint the UI uses when you click
Data → Datasets → +Dataset. Returns full dataset metadata (same shape as
get_dataset_info) so you can pass the resulting dataset_id straight into
generate_chart.
Required fields:
- database_id: ID of the existing database connection
- table_name: Exact name of the physical table to register
Optional fields:
- schema: Schema/namespace where the table lives (e.g. "public"). Omit for
databases without schema namespaces (e.g. SQLite).
- catalog: Catalog where the table lives. Omit for databases without catalog
support.
- owners: List of user IDs to set as owners (defaults to calling user)
Example:
```json
{
"database_id": 1,
"schema": "public",
"table_name": "orders"
}
```
Returns DatasetInfo on success or DatasetError on failure.
Use list_databases to find the correct database_id.
"""
schema = request.schema_
table_name = request.table_name
catalog = request.catalog
await ctx.info(
"Registering physical table as dataset: database_id=%s, table=%s"
% (request.database_id, f"{schema}.{table_name}" if schema else table_name)
)
try:
dataset_properties: dict[str, object] = {
"database": request.database_id,
"table_name": table_name,
}
if schema is not None:
dataset_properties["schema"] = schema
if catalog is not None:
dataset_properties["catalog"] = catalog
if request.owners is not None:
dataset_properties["owners"] = request.owners
with event_logger.log_context(action="mcp.create_dataset.create"):
dataset = CreateDatasetCommand(dataset_properties).run()
result = serialize_dataset_object(dataset)
if result is None:
return DatasetError.create(
error="Dataset was created but could not be serialized",
error_type="SerializationError",
)
await ctx.info(
"Dataset registered: id=%s, table=%s"
% (
dataset.id,
f"{schema}.{table_name}" if schema else table_name,
)
)
return result
except DatasetInvalidError as exc:
# CreateDatasetCommand.validate() collects validation errors into
# DatasetInvalidError.exceptions, never raising them directly.
# Inspect the wrapped class names for a typed response.
error_response = _classify_invalid_error(exc)
await ctx.warning(
"Dataset validation failed (%s): %s"
% (error_response.error_type, error_response.error)
)
return error_response
except DatasetCreateFailedError:
logger.exception("Dataset creation failed")
await ctx.error("Dataset creation failed")
return DatasetError.create(
error="Dataset creation failed", error_type="CreateFailedError"
)
except Exception as exc:
logger.exception("Unexpected error in create_dataset")
await ctx.error("Unexpected error: %s" % (type(exc).__name__,))
raise

View File

@@ -3138,7 +3138,10 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
for orig_col, ascending in orderby: # noqa: B007
col: Union[AdhocMetric, ColumnElement] = orig_col
if isinstance(col, dict):
col = cast(AdhocMetric, col)
# process a copy, as the dict is shared with `QueryObject.orderby`
# and `QueryContext.cache_values`; writing the processed expression
# back would change the cache key of a rehydrated query context
col = cast(AdhocMetric, dict(col))
if col.get("sqlExpression"):
col["sqlExpression"] = self._process_orderby_expression(
expression=col["sqlExpression"],

View File

@@ -21,6 +21,7 @@ from typing import Any, TYPE_CHECKING
from urllib import parse
import sqlalchemy as sqla
from flask import current_app
from flask_appbuilder import Model
from flask_appbuilder.models.decorators import renders
from markupsafe import escape, Markup
@@ -225,6 +226,11 @@ class Slice( # pylint: disable=too-many-public-methods
"query_context": self.query_context,
"modified": self.modified(),
"owners": [owner.id for owner in self.owners],
"extra_owners": (
[u["id"] for u in resolver(self)]
if (resolver := current_app.config.get("EXTRA_OWNERS_RESOLVER"))
else []
),
"slice_id": self.id,
"slice_name": self.slice_name,
"slice_url": self.slice_url,

View File

@@ -3153,6 +3153,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:raises SupersetSecurityException: If the user cannot access the resource
"""
# pylint: disable=import-outside-toplevel
from flask import current_app
from superset import is_feature_enabled
from superset.connectors.sqla.models import SqlaTable
from superset.models.dashboard import Dashboard
@@ -3160,6 +3162,22 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
from superset.models.sql_lab import Query
from superset.utils.core import shortid
# Extension hook: bypass all permission checks if an external system
# (e.g. folder permissions) grants access to this resource.
if bypass := current_app.config.get("EXTRA_RAISE_FOR_ACCESS_BYPASS"):
if bypass(
user_id=get_user_id(),
dashboard=dashboard,
chart=chart,
datasource=datasource,
query_context=query_context,
):
logger.info(
"EXTRA_RAISE_FOR_ACCESS_BYPASS granted access for user %s",
get_user_id(),
)
return
if sql and database:
query = Query(
database=database,
@@ -4147,6 +4165,17 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
owners = orig_resource.owners if hasattr(orig_resource, "owners") else []
if g.user.is_anonymous or g.user not in owners:
# Extension hook: check if the user is an extra owner
resolver = current_app.config.get("EXTRA_OWNERS_RESOLVER")
if resolver and not g.user.is_anonymous:
extra_owners = resolver(orig_resource)
user_id = g.user.id
if any(
(u.id if hasattr(u, "id") else u.get("id")) == user_id
for u in extra_owners
):
return
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,

View File

@@ -0,0 +1,585 @@
# 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.
"""Unit tests for create_dataset MCP tool."""
import importlib
from unittest.mock import MagicMock, Mock, patch
import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError
from superset.commands.dataset.exceptions import (
DatabaseNotFoundValidationError,
DatasetCreateFailedError,
DatasetDataAccessIsNotAllowed,
DatasetExistsValidationError,
DatasetInvalidError,
TableNotFoundValidationError,
)
from superset.mcp_service.app import mcp
from superset.sql.parse import Table
from superset.utils import json
# Use importlib to get the module object directly, bypassing the __init__.py
# attribute binding that shadows the module name with the exported function.
# This ensures patch.object resolves to the module in all Python versions.
create_dataset_module = importlib.import_module(
"superset.mcp_service.dataset.tool.create_dataset"
)
def _make_mock_dataset(
dataset_id: int = 42,
table_name: str = "orders",
schema: str = "public",
database_name: str = "main_db",
) -> MagicMock:
dataset = MagicMock()
dataset.id = dataset_id
dataset.table_name = table_name
dataset.schema = schema
dataset.description = None
dataset.certified_by = None
dataset.certification_details = None
dataset.changed_by_name = "admin"
dataset.changed_on = None
dataset.changed_on_humanized = None
dataset.created_by_name = "admin"
dataset.created_on = None
dataset.created_on_humanized = None
dataset.tags = []
dataset.owners = []
dataset.is_virtual = False
dataset.is_favorite = None
dataset.database_id = 1
dataset.schema_perm = f"[{database_name}].[{schema}]"
dataset.url = f"/tablemodelview/edit/{dataset_id}"
dataset.database = MagicMock()
dataset.database.database_name = database_name
dataset.sql = None
dataset.main_dttm_col = None
dataset.offset = 0
dataset.cache_timeout = 0
dataset.params = {}
dataset.template_params = {}
dataset.extra = {}
dataset.uuid = f"dataset-uuid-{dataset_id}"
dataset.columns = []
dataset.metrics = []
return dataset
@pytest.fixture
def mcp_server():
return mcp
@pytest.fixture(autouse=True)
def mock_auth():
with patch("superset.mcp_service.auth.get_user_from_request") as mock_get_user:
mock_user = Mock()
mock_user.id = 1
mock_user.username = "admin"
mock_get_user.return_value = mock_user
yield mock_get_user
class TestCreateDataset:
"""Tests for the create_dataset MCP tool."""
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_success(self, mock_command_class, mcp_server) -> None:
"""Happy path: tool creates dataset and returns DatasetInfo."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
assert result.content is not None
data = json.loads(result.content[0].text)
assert data["id"] == 42
assert data["table_name"] == "orders"
assert data["schema"] == "public"
# Verify the command was called with the right properties
call_kwargs = mock_command_class.call_args[0][0]
assert call_kwargs["database"] == 1
assert call_kwargs["schema"] == "public"
assert call_kwargs["table_name"] == "orders"
assert "owners" not in call_kwargs
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_with_owners(
self, mock_command_class, mcp_server
) -> None:
"""Owners list is forwarded to the command when supplied."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 2,
"schema": "sales",
"table_name": "transactions",
"owners": [5, 10],
}
},
)
data = json.loads(result.content[0].text)
assert data["id"] == 42
call_kwargs = mock_command_class.call_args[0][0]
assert call_kwargs["owners"] == [5, 10]
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_already_exists(
self, mock_command_class, mcp_server
) -> None:
"""Returns DatasetExistsError when the table is already registered.
CreateDatasetCommand.validate() wraps DatasetExistsValidationError inside
DatasetInvalidError. The tool must inspect get_list_classnames() to surface
the typed error response.
"""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetInvalidError(
exceptions=[DatasetExistsValidationError(Table("orders", "public", None))]
)
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "DatasetExistsError"
assert "error" in data
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_table_not_found(
self, mock_command_class, mcp_server
) -> None:
"""Returns TableNotFoundError when the physical table does not exist in the DB.
CreateDatasetCommand.validate() wraps TableNotFoundValidationError inside
DatasetInvalidError. The tool must inspect get_list_classnames() to surface
the typed error response.
"""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetInvalidError(
exceptions=[
TableNotFoundValidationError(Table("missing_table", "public", None))
]
)
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "missing_table",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "TableNotFoundError"
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_with_catalog(
self, mock_command_class, mcp_server
) -> None:
"""Catalog field is normalized and forwarded to the command when supplied."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"catalog": " hive ",
"schema": "default",
"table_name": "events",
}
},
)
call_kwargs = mock_command_class.call_args[0][0]
assert call_kwargs["catalog"] == "hive"
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_invalid_error(
self, mock_command_class, mcp_server
) -> None:
"""DatasetInvalidError is returned as ValidationError type."""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetInvalidError()
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "ValidationError"
assert "error" in data
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_create_failed_error(
self, mock_command_class, mcp_server
) -> None:
"""DatasetCreateFailedError is returned as CreateFailedError type."""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetCreateFailedError()
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "CreateFailedError"
assert "Dataset creation failed" in data["error"]
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_database_not_found(
self, mock_command_class, mcp_server
) -> None:
"""Returns DatabaseNotFoundError when CreateDatasetCommand raises it."""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetInvalidError(
exceptions=[DatabaseNotFoundValidationError()]
)
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 999,
"table_name": "orders",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "DatabaseNotFoundError"
assert "Database not found" in data["error"]
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_access_denied(
self, mock_command_class, mcp_server
) -> None:
"""Returns AccessDeniedError when CreateDatasetCommand raises it."""
mock_command = MagicMock()
mock_command.run.side_effect = DatasetInvalidError(
exceptions=[DatasetDataAccessIsNotAllowed("Access is Denied")]
)
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "secret",
"table_name": "restricted_table",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "AccessDeniedError"
assert "Access denied" in data["error"]
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_unexpected_error(
self, mock_command_class, mcp_server
) -> None:
"""Unexpected exceptions are re-raised as ToolError (handled by middleware)."""
mock_command = MagicMock()
mock_command.run.side_effect = RuntimeError("DB connection lost")
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
with pytest.raises(ToolError):
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
@pytest.mark.asyncio
async def test_create_dataset_missing_required_fields(self, mcp_server) -> None:
"""Missing required fields raise a validation error before the tool runs."""
async with Client(mcp_server) as client:
with pytest.raises(ToolError):
await client.call_tool(
"create_dataset",
{
"request": {
# database_id and table_name are omitted intentionally
"schema": "public",
}
},
)
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_returns_full_dataset_info(
self, mock_command_class, mcp_server
) -> None:
"""The returned DatasetInfo includes columns, metrics, and all core fields."""
mock_dataset = _make_mock_dataset(
dataset_id=99, table_name="sales", schema="dw"
)
col = MagicMock()
col.column_name = "amount"
col.verbose_name = "Amount"
col.type = "NUMERIC"
col.is_dttm = False
col.groupby = True
col.filterable = True
col.description = "Sale amount"
mock_dataset.columns = [col]
metric = MagicMock()
metric.metric_name = "total_sales"
metric.verbose_name = "Total Sales"
metric.expression = "SUM(amount)"
metric.description = "Sum of amounts"
metric.d3format = None
mock_dataset.metrics = [metric]
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "dw",
"table_name": "sales",
}
},
)
data = json.loads(result.content[0].text)
assert data["id"] == 99
assert data["table_name"] == "sales"
assert data["schema"] == "dw"
assert data["is_virtual"] is False
assert len(data["columns"]) == 1
assert data["columns"][0]["column_name"] == "amount"
assert len(data["metrics"]) == 1
assert data["metrics"][0]["metric_name"] == "total_sales"
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_table_name_whitespace_normalized(
self, mock_command_class, mcp_server
) -> None:
"""Whitespace in table_name is stripped before forwarding to the command."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": " orders ",
}
},
)
call_kwargs = mock_command_class.call_args[0][0]
assert call_kwargs["table_name"] == "orders"
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_blank_schema_normalized_to_none(
self, mock_command_class, mcp_server
) -> None:
"""Blank schema string is treated as absent: not forwarded to the command."""
mock_dataset = _make_mock_dataset(schema="")
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "",
"table_name": "orders",
}
},
)
call_kwargs = mock_command_class.call_args[0][0]
assert "schema" not in call_kwargs
@patch.object(create_dataset_module, "CreateDatasetCommand")
@pytest.mark.asyncio
async def test_create_dataset_blank_catalog_normalized_to_none(
self, mock_command_class, mcp_server
) -> None:
"""Blank catalog string is treated as absent: not forwarded to the command."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"catalog": "",
"table_name": "orders",
}
},
)
call_kwargs = mock_command_class.call_args[0][0]
assert "catalog" not in call_kwargs
@pytest.mark.asyncio
async def test_create_dataset_non_string_namespace_rejected(
self, mcp_server
) -> None:
"""Non-string schema/catalog values fail validation, not silently dropped."""
async with Client(mcp_server) as client:
for field, value in (("schema", 123), ("catalog", {"name": "hive"})):
with pytest.raises(ToolError):
await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"table_name": "orders",
field: value,
}
},
)
@patch.object(create_dataset_module, "CreateDatasetCommand")
@patch.object(create_dataset_module, "serialize_dataset_object", return_value=None)
@pytest.mark.asyncio
async def test_create_dataset_when_serialize_returns_none(
self, mock_serialize, mock_command_class, mcp_server
) -> None:
"""Returns SerializationError when serialize_dataset_object returns None."""
mock_dataset = _make_mock_dataset()
mock_command = MagicMock()
mock_command.run.return_value = mock_dataset
mock_command_class.return_value = mock_command
async with Client(mcp_server) as client:
result = await client.call_tool(
"create_dataset",
{
"request": {
"database_id": 1,
"schema": "public",
"table_name": "orders",
}
},
)
data = json.loads(result.content[0].text)
assert data["error_type"] == "SerializationError"

View File

@@ -19,6 +19,7 @@
from __future__ import annotations
import copy
from contextlib import contextmanager
from typing import cast, TYPE_CHECKING
from unittest.mock import patch
@@ -30,7 +31,7 @@ from sqlalchemy.orm.session import Session
from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.elements import ColumnElement
from superset.superset_typing import AdhocColumn
from superset.superset_typing import AdhocColumn, AdhocMetric, OrderBy
from superset.utils.core import GenericDataType
from tests.unit_tests.conftest import with_feature_flags
@@ -925,6 +926,118 @@ def test_process_orderby_expression_with_template_processor(
assert result == "processed_column DESC"
def _assert_get_sqla_query_does_not_mutate_orderby(
database: Database,
sql_expression: str,
expected_sql_fragment: str,
) -> None:
"""
Order a query by an ad-hoc SQL metric and assert the caller's orderby
dicts are left untouched.
The processed (Jinja-rendered, sqlglot-normalized) expression must not be
written back into the shared dict, which would change the cache key of a
rehydrated query context (see issue #37114). ``expected_sql_fragment`` is
matched against the whitespace-stripped SQL, as sqlglot may normalize the
output slightly.
"""
from superset.connectors.sqla.models import SqlaTable, TableColumn
table = SqlaTable(
database=database,
schema=None,
table_name="t",
columns=[TableColumn(column_name="a", type="INTEGER")],
)
adhoc_metric: AdhocMetric = {
"expressionType": "SQL",
"sqlExpression": sql_expression,
"label": "my metric",
"hasCustomLabel": True,
}
orderby: list[OrderBy] = [(copy.deepcopy(adhoc_metric), False)]
original_orderby = copy.deepcopy(orderby)
query = table.get_sqla_query(
metrics=[adhoc_metric],
orderby=orderby,
is_timeseries=False,
row_limit=10,
)
with database.get_sqla_engine() as engine:
sql = str(
query.sqla_query.compile(
dialect=engine.dialect, compile_kwargs={"literal_binds": True}
)
)
assert expected_sql_fragment in sql.replace(" ", "")
assert orderby == original_orderby
assert cast(AdhocMetric, orderby[0][0])["sqlExpression"] == sql_expression
def test_get_sqla_query_does_not_mutate_adhoc_orderby(database: Database) -> None:
"""
Test that `get_sqla_query` does not mutate ad-hoc ORDER BY entries.
"""
_assert_get_sqla_query_does_not_mutate_orderby(
database,
"SUM(CASE \r\n WHEN a > 0\r\n THEN 1\r\n END)",
"ORDERBY",
)
@with_feature_flags(ENABLE_TEMPLATE_PROCESSING=True)
def test_get_sqla_query_does_not_mutate_adhoc_orderby_with_jinja(
database: Database,
) -> None:
"""
Test that Jinja in an ad-hoc ORDER BY entry is not rendered back.
"""
_assert_get_sqla_query_does_not_mutate_orderby(
database,
"SUM(CASE WHEN a > {{ 1 + 1 }} THEN 1 END)",
"a>2",
)
def test_cache_key_stable_across_query_build(database: Database) -> None:
"""
Test that `QueryObject.cache_key()` is unchanged by building the query.
"""
from superset.common.query_object import QueryObject
from superset.connectors.sqla.models import SqlaTable, TableColumn
table = SqlaTable(
database=database,
schema=None,
table_name="t",
columns=[TableColumn(column_name="a", type="INTEGER")],
)
adhoc_metric: AdhocMetric = {
"expressionType": "SQL",
"sqlExpression": "SUM(CASE \r\n WHEN a > 0\r\n THEN a\r\nEND)",
"label": "my metric",
"hasCustomLabel": True,
}
query_obj = QueryObject(
datasource=table,
columns=[],
metrics=[adhoc_metric],
orderby=[(copy.deepcopy(adhoc_metric), False)],
is_timeseries=False,
row_limit=10,
)
cache_key_before = query_obj.cache_key()
table.get_query_str_extended(query_obj.to_dict())
assert query_obj.cache_key() == cache_key_before
def test_process_select_expression_basic(
mocker: MockerFixture,
database: Database,

View File

@@ -0,0 +1,204 @@
# 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.
"""Tests for config-based permission extension hooks.
Follows the same pattern as EXTRA_DYNAMIC_QUERY_FILTERS tests in
tests/unit_tests/databases/api_test.py.
"""
from unittest.mock import Mock
import pytest
from superset import db
@pytest.fixture
def sample_chart(app_context: None):
"""Create a minimal chart for testing."""
from superset.models.slice import Slice
chart = Slice(
slice_name="test_permission_hooks_chart",
datasource_type="table",
viz_type="table",
params="{}",
)
db.session.add(chart)
db.session.flush()
yield chart
db.session.delete(chart)
db.session.flush()
@pytest.fixture
def sample_user(app_context: None):
"""Create a minimal user for testing."""
from flask_appbuilder.security.sqla.models import User
user = User(
first_name="test",
last_name="hooks",
username="test_permission_hooks_user",
email="test_hooks@example.com",
)
db.session.add(user)
db.session.flush()
yield user
db.session.delete(user)
db.session.flush()
def test_extra_owners_resolver_injects_into_extra_owners(sample_chart, monkeypatch):
"""EXTRA_OWNERS_RESOLVER populates Slice.data['extra_owners'], not 'owners'."""
from flask import current_app
original_owner_ids = [o.id for o in sample_chart.owners]
# Without config — extra_owners is empty, owners unchanged
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", None)
data = sample_chart.data
assert data["owners"] == original_owner_ids
assert data["extra_owners"] == []
# With config — extra_owners populated, owners unchanged
def _resolver(resource):
return [{"id": 99999, "first_name": "Folder", "last_name": "Editor"}]
resolver_mock = Mock(side_effect=_resolver)
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", resolver_mock)
data = sample_chart.data
assert data["owners"] == original_owner_ids
assert 99999 in data["extra_owners"]
assert resolver_mock.call_count == 1
def test_extra_owners_resolver_empty_returns_unchanged(sample_chart, monkeypatch):
"""EXTRA_OWNERS_RESOLVER returning empty list leaves extra_owners empty."""
from flask import current_app
resolver_mock = Mock(return_value=[])
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", resolver_mock)
data = sample_chart.data
original_owner_ids = [o.id for o in sample_chart.owners]
assert data["owners"] == original_owner_ids
assert data["extra_owners"] == []
assert resolver_mock.call_count == 1
def test_raise_for_access_bypass_skips_checks(app_context: None, monkeypatch):
"""EXTRA_RAISE_FOR_ACCESS_BYPASS returning True skips all permission checks."""
from flask import current_app
from superset import security_manager
bypass_mock = Mock(return_value=True)
monkeypatch.setitem(
current_app.config, "EXTRA_RAISE_FOR_ACCESS_BYPASS", bypass_mock
)
security_manager.raise_for_access(dashboard=None, chart=None)
assert bypass_mock.call_count == 1
def test_raise_for_access_no_bypass_without_config(app_context: None, monkeypatch):
"""Without EXTRA_RAISE_FOR_ACCESS_BYPASS, normal checks proceed."""
from flask import current_app
from superset import security_manager
monkeypatch.setitem(current_app.config, "EXTRA_RAISE_FOR_ACCESS_BYPASS", None)
security_manager.raise_for_access(dashboard=None, chart=None)
def test_ownership_check_allows_non_owner(sample_chart, sample_user, monkeypatch):
"""EXTRA_OWNERS_RESOLVER returning the user allows a non-owner to pass."""
from flask import current_app, g
from superset import security_manager
resolver_mock = Mock(return_value=[sample_user])
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", resolver_mock)
monkeypatch.setattr(g, "user", sample_user, raising=False)
security_manager.raise_for_ownership(sample_chart)
resolver_mock.assert_called_once()
def test_owner_auto_add_skipped_with_resolver(
sample_user, app_context: None, monkeypatch
):
"""When EXTRA_OWNERS_RESOLVER is set, current user is NOT auto-added."""
from flask import current_app, g
from flask_appbuilder.security.sqla.models import User
from superset.commands.utils import populate_owner_list
other_user = User(
first_name="other",
last_name="skip",
username="test_skip_other",
email="skip_other@example.com",
)
db.session.add(other_user)
db.session.flush()
resolver = Mock(return_value=[])
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", resolver)
monkeypatch.setattr(g, "user", sample_user, raising=False)
try:
owners = populate_owner_list([other_user.id], default_to_user=False)
owner_ids = [o.id for o in owners]
# Only the explicitly passed owner — current user NOT auto-added
assert owner_ids == [other_user.id]
assert sample_user.id not in owner_ids
finally:
db.session.delete(other_user)
db.session.flush()
def test_owner_auto_add_without_resolver(sample_user, app_context: None, monkeypatch):
"""Without EXTRA_OWNERS_RESOLVER, current user IS auto-added."""
from flask import current_app, g
from flask_appbuilder.security.sqla.models import User
from superset.commands.utils import populate_owner_list
other_user = User(
first_name="other",
last_name="user",
username="test_other_user",
email="other@example.com",
)
db.session.add(other_user)
db.session.flush()
monkeypatch.setitem(current_app.config, "EXTRA_OWNERS_RESOLVER", None)
monkeypatch.setattr(g, "user", sample_user, raising=False)
try:
owners = populate_owner_list([other_user.id], default_to_user=False)
owner_ids = [o.id for o in owners]
# Both the passed owner AND current user auto-added
assert sample_user.id in owner_ids
assert other_user.id in owner_ids
finally:
db.session.delete(other_user)
db.session.flush()