Compare commits

...

24 Commits

Author SHA1 Message Date
Joe Li
98f6d627ed Merge branch 'master' into fix-no-top-level-tab 2026-05-11 09:19:34 -07:00
dependabot[bot]
1d1a0e6fec chore(deps-dev): update sqlalchemy-firebird requirement from <0.8,>=0.7.0 to >=0.7.0,<2.2 (#39755)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 09:00:12 -07:00
dependabot[bot]
494c29f5bf chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.59.1 to 8.59.2 in /superset-frontend (#39878)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:59:49 -07:00
dependabot[bot]
ad7075d2aa chore(deps): bump fast-uri from 3.0.6 to 3.1.2 in /docs (#39979)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:59:26 -07:00
dependabot[bot]
3e1cfc6d69 chore(deps): bump @babel/plugin-transform-modules-systemjs from 7.27.1 to 7.29.4 in /docs (#39981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:59:04 -07:00
dependabot[bot]
fcf3f6c0d5 chore(deps-dev): update pinotdb requirement from <6.0.0,>=5.0.0 to >=5.0.0,<10.0.0 (#39985)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:58:44 -07:00
dependabot[bot]
14ba666594 chore(deps-dev): update ibm-db-sa requirement from <=0.4.0,>0.3.8 to >0.3.8,<=0.4.4 (#39986)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:58:14 -07:00
dependabot[bot]
1c795418d2 chore(deps-dev): bump pyinstrument from 4.4.0 to 5.1.2 (#39987)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:57:47 -07:00
dependabot[bot]
6271272e60 chore(deps): bump nh3 from 0.2.21 to 0.3.5 (#39988)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:57:05 -07:00
dependabot[bot]
2cf4a2c31f chore(deps-dev): bump databricks-sql-connector from 4.1.2 to 4.2.6 (#39989)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:54:57 -07:00
dependabot[bot]
2adb6f64eb chore(deps): bump baseline-browser-mapping from 2.10.27 to 2.10.29 in /docs (#40013)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:54:43 -07:00
dependabot[bot]
5a453fe95d chore(deps-dev): bump wait-on from 9.0.5 to 9.0.6 in /superset-frontend (#40014)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 08:54:26 -07:00
Mehmet Salih Yavuz
245fffca79 fix(dashboard): Clear All filters now stages changes until Apply (#39778)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-05-11 17:15:35 +03:00
Mehmet Salih Yavuz
372b50e19d fix(dashboard): row limit warning missing for non-table charts (#39911) 2026-05-11 17:14:55 +03:00
Oleg Ovcharuk
d83b0c5ce3 feat: support creating datasets for schema-less databases (#39433)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-05-11 08:30:13 -04:00
Joe Li
ff323ba328 Merge branch 'master' into fix-no-top-level-tab 2026-05-07 10:02:44 -07:00
Joe Li
78fada7f43 Merge branch 'master' into fix-no-top-level-tab 2026-05-06 19:18:19 -07:00
Joe Li
b71c556560 fix(dashboard): derive min-height assertion from theme.sizeUnit
Use supersetTheme.sizeUnit * 4 instead of hard-coded '16px' so the
test stays in sync with the source rule and resilient to theme
token changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:10:19 -07:00
Joe Li
7d6513f946 Merge branch 'master' into fix-no-top-level-tab 2026-05-05 15:11:11 -07:00
Joe Li
d25c07586d Merge branch 'master' into fix-no-top-level-tab 2026-05-04 12:33:46 -07:00
Joe Li
93066ade52 fix(dashboard): assert concrete min-height value and centralize emotion-jest types
- Assert min-height is '16px' (theme.sizeUnit * 4) instead of /\S+/
  to catch zero-height regressions
- Move @emotion/jest type reference from per-file directive to
  src/types/emotion-jest.d.ts for global availability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 12:26:09 -07:00
Joe Li
3fe59024ef fix(dashboard): use toHaveStyleRule and getByTestId in droptarget test
Replace CSSOM scan with @emotion/jest toHaveStyleRule matcher using
the target option to verify the .empty-droptarget rule on StyledHeader.
Switch from container.querySelector to getByTestId for consistency
with other tests in this file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 17:14:47 -07:00
Joe Li
0b442ef9ae fix(dashboard): address review feedback and fix test type errors
- Assert the actual .empty-droptarget element exists on the Droppable
  (addresses Copilot review feedback on PR #39423)
- Add @emotion/jest type reference to fix pre-existing toHaveStyleRule
  TS errors in this test file
- Prettier formatting fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 13:35:04 -07:00
Joe Li
5bb88b2b74 fix(dashboard): restore top-level tab drop target height for dashboards with content
PR #37891 moved the DashboardHeader out of the root Droppable, leaving
it with zero height when no top-level tabs exist. This made it impossible
to drag a Tabs component onto dashboards that already have content.

Add min-height to .empty-droptarget in StyledHeader, matching the pattern
used by DashboardGrid for its drop targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 09:49:14 -07:00
33 changed files with 1187 additions and 193 deletions

View File

@@ -69,7 +69,7 @@
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.27",
"baseline-browser-mapping": "^2.10.29",
"caniuse-lite": "^1.0.30001792",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",

View File

@@ -261,6 +261,15 @@
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/code-frame@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==
dependencies:
"@babel/helper-validator-identifier" "^7.28.5"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
version "7.28.0"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
@@ -303,6 +312,17 @@
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/generator@^7.29.0":
version "7.29.1"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50"
integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==
dependencies:
"@babel/parser" "^7.29.0"
"@babel/types" "^7.29.0"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
version "7.27.3"
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
@@ -404,6 +424,11 @@
resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz"
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
"@babel/helper-plugin-utils@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8"
integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==
"@babel/helper-remap-async-to-generator@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz"
@@ -435,11 +460,6 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz"
@@ -474,6 +494,13 @@
dependencies:
"@babel/types" "^7.28.6"
"@babel/parser@^7.29.0":
version "7.29.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.3.tgz#116f70a77958307fceac27747573032f8a62f88e"
integrity sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==
dependencies:
"@babel/types" "^7.29.0"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz"
@@ -758,14 +785,14 @@
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/plugin-transform-modules-systemjs@^7.27.1":
version "7.27.1"
resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz"
integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==
version "7.29.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20"
integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==
dependencies:
"@babel/helper-module-transforms" "^7.27.1"
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/traverse" "^7.27.1"
"@babel/helper-module-transforms" "^7.28.6"
"@babel/helper-plugin-utils" "^7.28.6"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/traverse" "^7.29.0"
"@babel/plugin-transform-modules-umd@^7.27.1":
version "7.27.1"
@@ -1163,6 +1190,19 @@
"@babel/types" "^7.28.6"
debug "^4.3.1"
"@babel/traverse@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==
dependencies:
"@babel/code-frame" "^7.29.0"
"@babel/generator" "^7.29.0"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.29.0"
"@babel/template" "^7.28.6"
"@babel/types" "^7.29.0"
debug "^4.3.1"
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.4.4":
version "7.28.6"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz"
@@ -1171,6 +1211,14 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@braintree/sanitize-url@^7.0.4":
version "7.1.1"
resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz"
@@ -5794,10 +5842,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.27, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.27"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3"
integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==
baseline-browser-mapping@^2.10.29, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.29"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz#47bdc13027af28d341f367a4f35a07ce872e27b4"
integrity sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==
batch@0.6.1:
version "0.6.1"
@@ -8118,9 +8166,9 @@ fast-safe-stringify@^2.0.7:
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fast-uri@^3.0.1:
version "3.0.6"
resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz"
integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
fastq@^1.6.0:
version "1.19.1"

View File

@@ -71,7 +71,7 @@ dependencies = [
"marshmallow>=3.0, <4",
"marshmallow-union>=0.1",
"msgpack>=1.0.0, <1.2",
"nh3>=0.2.11, <0.3",
"nh3>=0.2.11, <0.4",
"numpy>1.23.5, <2.3",
"packaging",
# --------------------------
@@ -131,10 +131,10 @@ d1 = [
]
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
databricks = [
"databricks-sql-connector==4.1.2",
"databricks-sql-connector==4.2.6",
"databricks-sqlalchemy==1.0.5",
]
db2 = ["ibm-db-sa>0.3.8, <=0.4.0"]
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.4, <2"]
@@ -152,7 +152,7 @@ fastmcp = [
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.7.0,<1.0",
]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
@@ -179,7 +179,7 @@ ocient = [
]
oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <6.0.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.37.0, <2"]
postgres = ["psycopg2-binary==2.9.12"]
presto = ["pyhive[presto]>=0.6.5"]
@@ -224,7 +224,7 @@ development = [
"progress>=1.5,<2",
"psutil",
"pyfakefs",
"pyinstrument>=4.0.2,<5",
"pyinstrument>=4.0.2,<6",
"pylint",
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
"pytest-asyncio",

View File

@@ -222,7 +222,7 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.58.2",
"babel-jest": "^30.0.2",
"babel-loader": "^10.1.1",
@@ -290,7 +290,7 @@
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.5",
"wait-on": "^9.0.6",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
@@ -14358,17 +14358,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
"integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/type-utils": "8.59.1",
"@typescript-eslint/utils": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/scope-manager": "8.59.2",
"@typescript-eslint/type-utils": "8.59.2",
"@typescript-eslint/utils": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -14381,20 +14381,42 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/parser": "^8.59.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1"
"@typescript-eslint/tsconfig-utils": "^8.59.2",
"@typescript-eslint/types": "^8.59.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14404,10 +14426,27 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14419,16 +14458,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.1",
"@typescript-eslint/tsconfig-utils": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/project-service": "8.59.2",
"@typescript-eslint/tsconfig-utils": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14447,16 +14486,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
"integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1"
"@typescript-eslint/scope-manager": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/typescript-estree": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14471,13 +14510,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/types": "8.59.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14499,9 +14538,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14551,16 +14590,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
"integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/scope-manager": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/typescript-estree": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3"
},
"engines": {
@@ -14575,15 +14614,37 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1"
"@typescript-eslint/tsconfig-utils": "^8.59.2",
"@typescript-eslint/types": "^8.59.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14593,10 +14654,27 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14608,16 +14686,16 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.1",
"@typescript-eslint/tsconfig-utils": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/project-service": "8.59.2",
"@typescript-eslint/tsconfig-utils": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14636,13 +14714,13 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/types": "8.59.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14664,9 +14742,9 @@
}
},
"node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14777,15 +14855,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
"integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1",
"@typescript-eslint/utils": "8.59.1",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/typescript-estree": "8.59.2",
"@typescript-eslint/utils": "8.59.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -14801,15 +14879,37 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1"
"@typescript-eslint/tsconfig-utils": "^8.59.2",
"@typescript-eslint/types": "^8.59.2",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14819,10 +14919,27 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14834,16 +14951,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.59.1",
"@typescript-eslint/tsconfig-utils": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/visitor-keys": "8.59.1",
"@typescript-eslint/project-service": "8.59.2",
"@typescript-eslint/tsconfig-utils": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/visitor-keys": "8.59.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -14862,16 +14979,16 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
"integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.1",
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/typescript-estree": "8.59.1"
"@typescript-eslint/scope-manager": "8.59.2",
"@typescript-eslint/types": "8.59.2",
"@typescript-eslint/typescript-estree": "8.59.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -14886,13 +15003,13 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
"version": "8.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.59.1",
"@typescript-eslint/types": "8.59.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -14914,9 +15031,9 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16882,13 +16999,13 @@
}
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
@@ -31577,9 +31694,9 @@
}
},
"node_modules/joi": {
"version": "18.1.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz",
"integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==",
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
"integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -47858,14 +47975,14 @@
}
},
"node_modules/wait-on": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.5.tgz",
"integrity": "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA==",
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.6.tgz",
"integrity": "sha512-KR+Te+NBg6DmPVil4anyIO72mpt/QDHjRo3nVFkwRgb26oweUp3DDW2szO3EeUY4cqafWy4rQuOOeEk4n+7Oeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"axios": "^1.15.0",
"joi": "^18.1.2",
"axios": "^1.16.0",
"joi": "^18.2.1",
"lodash": "^4.18.1",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
@@ -50893,7 +51010,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "^9.3.2",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",

View File

@@ -303,7 +303,7 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.58.2",
"babel-jest": "^30.0.2",
"babel-loader": "^10.1.1",
@@ -371,7 +371,7 @@
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
"wait-on": "^9.0.5",
"wait-on": "^9.0.6",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",

View File

@@ -0,0 +1,220 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPost, apiPut } from '../../helpers/api/requests';
import { apiPostDashboard } from '../../helpers/api/dashboard';
import { DashboardPage } from '../../pages/DashboardPage';
const DATASET_NAME = 'birth_names';
const FILTER_COLUMN = 'gender';
async function findDatasetIdByName(page: any, name: string): Promise<number> {
const rison = `(filters:!((col:table_name,opr:eq,value:'${name}')))`;
const resp = await page.request.get(`api/v1/dataset/?q=${rison}`);
const body = await resp.json();
if (!body.result?.length) {
throw new Error(`Dataset ${name} not found`);
}
return body.result[0].id;
}
testWithAssets(
'Clear all filters waits for Apply (sc-105059)',
async ({ page, testAssets }) => {
const datasetId = await findDatasetIdByName(page, DATASET_NAME);
// Create a chart that the dashboard filter will target
const chartParams = {
datasource: `${datasetId}__table`,
viz_type: 'big_number_total',
metric: 'count',
adhoc_filters: [],
header_font_size: 0.4,
subheader_font_size: 0.15,
};
const chartResp = await apiPost(page, 'api/v1/chart/', {
slice_name: `clear_all_repro_${Date.now()}`,
viz_type: 'big_number_total',
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(chartParams),
});
expect(chartResp.ok()).toBe(true);
const chart = await chartResp.json();
const chartId: number = chart.id ?? chart.result?.id;
testAssets.trackChart(chartId);
// Create dashboard with chart in position_json and a native filter in json_metadata
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
const chartLayoutKey = `CHART-${chartId}`;
const positionJson = {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['ROW-1'],
parents: ['ROOT_ID'],
},
'ROW-1': {
type: 'ROW',
id: 'ROW-1',
children: [chartLayoutKey],
parents: ['ROOT_ID', 'GRID_ID'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
[chartLayoutKey]: {
type: 'CHART',
id: chartLayoutKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
meta: {
chartId,
width: 6,
height: 50,
sliceName: 'clear_all_repro',
},
},
};
const jsonMetadata = {
native_filter_configuration: [
{
id: filterId,
name: 'Gender',
filterType: 'filter_select',
type: 'NATIVE_FILTER',
targets: [
{
datasetId,
column: { name: FILTER_COLUMN },
},
],
controlValues: {
multiSelect: false,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
defaultDataMask: { filterState: {}, extraFormData: {} },
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [] },
chartsInScope: [chartId],
},
],
chart_configuration: {},
cross_filters_enabled: false,
global_chart_configuration: {
scope: { rootPath: ['ROOT_ID'], excluded: [] },
chartsInScope: [chartId],
},
};
const dashResp = await apiPostDashboard(page, {
dashboard_title: `clear_all_repro_${Date.now()}`,
published: true,
position_json: JSON.stringify(positionJson),
json_metadata: JSON.stringify(jsonMetadata),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
testAssets.trackDashboard(dashboardId);
// Associate chart with the dashboard so it actually renders
const linkResp = await apiPut(page, `api/v1/chart/${chartId}`, {
dashboards: [dashboardId],
});
expect(linkResp.ok()).toBe(true);
// Visit dashboard
const dashboardPage = new DashboardPage(page);
await dashboardPage.gotoById(dashboardId);
await dashboardPage.waitForLoad();
await dashboardPage.waitForChartsToLoad();
// The Gender select should be visible in the filter bar
const filterCombobox = page
.locator('[data-test="form-item-value"]')
.first()
.locator('[role="combobox"]');
await filterCombobox.click();
await page
.locator('.ant-select-item-option', { hasText: /^boy$/ })
.first()
.click();
// Close the dropdown
await page.keyboard.press('Escape');
const applyBtn = page.locator(
'[data-test="filter-bar__apply-button"], [data-test="filterbar-action-buttons"] button[type="submit"]',
);
// Wait for chart data to come back after Apply
const firstApplyResponse = page.waitForResponse(
r =>
r.url().includes('/api/v1/chart/data') &&
r.request().method() === 'POST',
{ timeout: 10_000 },
);
await applyBtn.first().click();
await firstApplyResponse;
await dashboardPage.waitForChartsToLoad();
// Now track POST /api/v1/chart/data requests around Clear All
const postsAfterClearAll: string[] = [];
const handler = (req: any) => {
if (
req.url().includes('/api/v1/chart/data') &&
req.method() === 'POST'
) {
postsAfterClearAll.push(req.url());
}
};
page.on('request', handler);
const clearBtn = page.locator('[data-test="filter-bar__clear-button"]');
await clearBtn.click();
// Allow time for any debounced reload to fire if the bug is present
await page.waitForTimeout(2000);
page.off('request', handler);
// BUG: on master, the Clear All triggers an immediate dispatch which
// re-runs the chart query before the user clicks Apply. After the fix,
// no chart/data request should fire until Apply is clicked.
expect(
postsAfterClearAll,
'Clear All must not reload charts until Apply is clicked',
).toEqual([]);
// After Apply, the chart should reload
const applyAfterClearPromise = page.waitForResponse(
r =>
r.url().includes('/api/v1/chart/data') &&
r.request().method() === 'POST',
{ timeout: 10_000 },
);
await applyBtn.first().click();
await applyAfterClearPromise;
},
);

View File

@@ -191,6 +191,8 @@ export function DatabaseSelector({
}: DatabaseSelectorProps) {
const showCatalogSelector = !!db?.allow_multi_catalog;
const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>();
const showSchemaSelector =
(db?.supports_schemas ?? currentDb?.supports_schemas) !== false;
const [errorPayload, setErrorPayload] = useState<SupersetError | null>();
const [currentCatalog, setCurrentCatalog] = useState<
CatalogOption | null | undefined
@@ -260,6 +262,12 @@ export function DatabaseSelector({
database_name: row.database_name,
backend: row.backend,
allow_multi_catalog: row.allow_multi_catalog,
supports_schemas:
(
row as DatabaseObject & {
engine_information?: { supports_schemas?: boolean };
}
).engine_information?.supports_schemas !== false,
order,
}));
@@ -597,7 +605,7 @@ export function DatabaseSelector({
{renderDatabaseSelect()}
{renderError()}
{showCatalogSelector && renderCatalogSelect()}
{renderSchemaSelect()}
{showSchemaSelector && renderSchemaSelect()}
</DatabaseSelectorWrapper>
);
}

View File

@@ -24,6 +24,7 @@ export type DatabaseValue = {
id: number;
database_name: string;
backend?: string;
supports_schemas?: boolean;
};
export type DatabaseObject = {
@@ -31,6 +32,7 @@ export type DatabaseObject = {
database_name: string;
backend?: string;
allow_multi_catalog?: boolean;
supports_schemas?: boolean;
};
export interface DatabaseSelectorProps {

View File

@@ -260,6 +260,52 @@ test('table multi select retain all the values selected', async () => {
expect(selections[1]).toHaveTextContent('table_c');
});
test('calls onTableSelectChange for schema-less database without schema', async () => {
fetchMock.get(catalogApiRoute, { result: [] });
fetchMock.get(schemaApiRoute, { result: [] });
fetchMock.get(tablesApiRoute, getTableMockFunction());
const callback = jest.fn();
const props = createProps({
database: {
id: 1,
database_name: 'ydb',
backend: 'ydb',
supports_schemas: false,
},
schema: undefined,
onTableSelectChange: callback,
});
render(<TableSelector {...props} />, { useRedux: true, store });
const tableSelect = screen.getByRole('combobox', {
name: 'Select table or type to search tables',
});
await act(async () => {
await userEvent.click(tableSelect);
});
await waitFor(
() => {
expect(screen.getByText('table_a')).toBeInTheDocument();
},
{ timeout: 10000 },
);
await act(async () => {
await userEvent.click(screen.getByText('table_a'));
});
await waitFor(
() => {
expect(callback).toHaveBeenCalled();
},
{ timeout: 10000 },
);
}, 15000);
test('TableOption renders correct icons for different table types', () => {
// Test regular table
const tableTable = {

View File

@@ -190,6 +190,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
dbId: database?.id,
catalog: currentCatalog,
schema: currentSchema,
supportsSchemas: database?.supports_schemas,
onSuccess: (data, isFetched) => {
setErrorPayload(null);
if (isFetched) {
@@ -247,7 +248,8 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
const internalTableChange = (
selectedOptions: TableOption | TableOption[] | undefined,
) => {
if (currentSchema) {
setTableSelectValue(selectedOptions);
if (currentSchema || database?.supports_schemas === false) {
onTableSelectChange?.(
Array.isArray(selectedOptions)
? selectedOptions.map(option => option?.value)
@@ -255,8 +257,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
currentCatalog,
currentSchema,
);
} else {
setTableSelectValue(selectedOptions);
}
};
@@ -302,7 +302,8 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
);
function renderTableSelect() {
const disabled = (currentSchema && !formMode && readOnly) || !currentSchema;
const disabled =
readOnly || (database?.supports_schemas !== false && !currentSchema);
const label = t('Table');

View File

@@ -24,6 +24,7 @@ import {
screen,
} from 'spec/helpers/testing-library';
import { FeatureFlag } from '@superset-ui/core';
import { supersetTheme } from '@apache-superset/core/theme';
import {
OPEN_FILTER_BAR_WIDTH,
CLOSED_FILTER_BAR_WIDTH,
@@ -487,6 +488,47 @@ test('should render ParentSize wrapper with height 100% for tabs', async () => {
expect(tabPanels.length).toBeGreaterThan(0);
});
test('should apply min-height to the top-level tab drop target so tabs can be dropped on dashboards with content', () => {
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
100,
jest.fn(),
]);
(fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
(setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
const { getByTestId } = render(<DashboardBuilder />, {
useRedux: true,
store: storeWithState({
...mockState,
dashboardLayout: undoableDashboardLayout,
dashboardState: { ...mockState.dashboardState, editMode: true },
}),
useDnd: true,
useTheme: true,
});
const headerWrapper = getByTestId('dashboard-header-wrapper');
// The Droppable inside the header should have the empty-droptarget class
// when there are no top-level tabs and edit mode is active. Without this
// class (and its associated min-height CSS rule), the drop target has zero
// height and users cannot drag tabs onto dashboards that already have
// content.
const droptarget = headerWrapper.querySelector('.empty-droptarget');
expect(droptarget).toBeInTheDocument();
// Verify the StyledHeader CSS defines a non-zero min-height for
// .empty-droptarget, derived from theme.sizeUnit * 4 to stay in sync
// with the source rule in DashboardBuilder.tsx.
expect(headerWrapper).toHaveStyleRule(
'min-height',
`${supersetTheme.sizeUnit * 4}px`,
{
target: '.empty-droptarget',
},
);
});
test('should maintain layout when switching between tabs', async () => {
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
100,

View File

@@ -100,6 +100,10 @@ const StyledHeader = styled.div<{ filterBarWidth: number }>`
z-index: 99;
max-width: calc(100vw - ${filterBarWidth}px);
.empty-droptarget {
min-height: ${theme.sizeUnit * 4}px;
}
.empty-droptarget:before {
position: absolute;
content: '';

View File

@@ -763,6 +763,123 @@ test('Should show row count warning for table chart with server pagination when
mockUseUiConfig.mockRestore();
});
test('Should show row count warning for non-table chart when row limit is reached', () => {
const props = createProps({
formData: {
...createProps().formData,
viz_type: VizType.Bar,
row_limit: 10,
},
slice: {
...createProps().slice,
form_data: {
...createProps().slice.form_data,
viz_type: VizType.Bar,
row_limit: 10,
},
viz_type: VizType.Bar,
},
});
const barChartState = {
...initialState,
charts: {
[props.slice.slice_id]: {
id: MOCKED_CHART_ID,
chartStatus: 'rendered',
queriesResponse: [
{
sql_rowcount: 10,
data: Array(10).fill({}),
},
],
},
},
};
const mockUseUiConfig = useUiConfig as jest.MockedFunction<
typeof useUiConfig
>;
mockUseUiConfig.mockReturnValue({
hideTitle: false,
hideTab: false,
hideNav: false,
hideChartControls: false,
emitDataMasks: false,
showRowLimitWarning: true,
});
render(<SliceHeader {...props} />, {
useRedux: true,
useRouter: true,
initialState: barChartState,
});
expect(screen.getByTestId('warning')).toBeInTheDocument();
mockUseUiConfig.mockRestore();
});
test('Should show row count warning for ag-grid table chart with server pagination when limit is reached', () => {
const props = createProps({
formData: {
...createProps().formData,
viz_type: VizType.TableAgGrid,
row_limit: 10,
server_pagination: true,
},
slice: {
...createProps().slice,
form_data: {
...createProps().slice.form_data,
viz_type: VizType.TableAgGrid,
row_limit: 10,
server_pagination: true,
},
viz_type: VizType.TableAgGrid,
},
});
const agGridWithPaginationState = {
...initialState,
charts: {
[props.slice.slice_id]: {
id: MOCKED_CHART_ID,
chartStatus: 'rendered',
queriesResponse: [
{
sql_rowcount: 10,
data: Array(10).fill({}),
},
{
data: [{ rowcount: 50 }],
},
],
},
},
};
const mockUseUiConfig = useUiConfig as jest.MockedFunction<
typeof useUiConfig
>;
mockUseUiConfig.mockReturnValue({
hideTitle: false,
hideTab: false,
hideNav: false,
hideChartControls: false,
emitDataMasks: false,
showRowLimitWarning: true,
});
render(<SliceHeader {...props} />, {
useRedux: true,
useRouter: true,
initialState: agGridWithPaginationState,
});
expect(screen.getByTestId('warning')).toBeInTheDocument();
mockUseUiConfig.mockRestore();
});
test('Should NOT show row count warning for table chart with server pagination when limit is NOT reached', () => {
const props = createProps({
formData: {

View File

@@ -26,7 +26,7 @@ import {
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { getExtensionsRegistry, QueryData } from '@superset-ui/core';
import { getExtensionsRegistry, QueryData, VizType } from '@superset-ui/core';
import {
css,
styled,
@@ -206,9 +206,12 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
const rowLimit = Number(formData.row_limit ?? 0);
const isTableChart = formData.viz_type === 'table';
const countFromSecondQuery =
isTableChart && secondQueryResponse?.data?.[0]?.rowcount;
const isTableChart =
formData.viz_type === VizType.Table ||
formData.viz_type === VizType.TableAgGrid;
const countFromSecondQuery = isTableChart
? secondQueryResponse?.data?.[0]?.rowcount
: undefined;
const sqlRowCount =
countFromSecondQuery != null

View File

@@ -27,6 +27,7 @@ import {
RefObject,
} from 'react';
import type { ChartCustomization, JsonObject } from '@superset-ui/core';
import { VizType } from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import { debounce } from 'lodash';
@@ -495,7 +496,9 @@ const Chart = (props: ChartProps) => {
const resultType = isPivot ? 'post_processed' : 'full';
let actualRowCount: number | undefined;
const isTableViz = (formData as JsonObject)?.viz_type === 'table';
const vizType = (formData as JsonObject)?.viz_type;
const isTableViz =
vizType === VizType.Table || vizType === VizType.TableAgGrid;
if (
isTableViz &&

View File

@@ -486,7 +486,7 @@ test('FilterBar renders correctly when filter has complete extraFormData', async
expect(screen.getByTestId(getTestId('filter-icon'))).toBeInTheDocument();
});
test('handleClearAll dispatches updateDataMask with value undefined for filter_select', async () => {
test('Clear All stages filter_select clear without dispatching until Apply', async () => {
const filterId = 'NATIVE_FILTER-clear-select';
const updateDataMaskSpy = jest.spyOn(dataMaskActions, 'updateDataMask');
const selectFilter = createFilter({
@@ -513,7 +513,9 @@ test('handleClearAll dispatches updateDataMask with value undefined for filter_s
activeTabs: ['ROOT_ID'],
},
dataMask: {
[filterId]: createDataMask(filterId, ['East']),
[filterId]: createDataMask(filterId, ['East'], {
filters: [{ col: 'region', op: 'IN', val: ['East'] }],
}),
},
nativeFilters: {
filters: { [filterId]: selectFilter },
@@ -533,14 +535,24 @@ test('handleClearAll dispatches updateDataMask with value undefined for filter_s
userEvent.click(clearBtn);
});
// Clear All must not dispatch — staging only
expect(updateDataMaskSpy).not.toHaveBeenCalled();
// Apply commits the staged clear
const applyBtn = screen.getByTestId(getTestId('apply-button'));
expect(applyBtn).not.toBeDisabled();
await act(async () => {
userEvent.click(applyBtn);
});
expect(updateDataMaskSpy).toHaveBeenCalledWith(filterId, {
filterState: { value: undefined },
id: filterId,
filterState: { value: undefined, validateStatus: undefined },
extraFormData: {},
});
updateDataMaskSpy.mockRestore();
});
test('handleClearAll dispatches updateDataMask with [null, null] for filter_range', async () => {
test('Clear All stages filter_range clear with [null, null], dispatched on Apply', async () => {
fetchMock.post('glob:*/api/v1/chart/data', {
result: [{ data: [{ min: 0, max: 100 }] }],
});
@@ -570,7 +582,9 @@ test('handleClearAll dispatches updateDataMask with [null, null] for filter_rang
activeTabs: ['ROOT_ID'],
},
dataMask: {
[filterId]: createDataMask(filterId, [10, 50]),
[filterId]: createDataMask(filterId, [10, 50], {
filters: [{ col: 'age', op: '>=', val: 10 }],
}),
},
nativeFilters: {
filters: { [filterId]: rangeFilter },
@@ -590,14 +604,21 @@ test('handleClearAll dispatches updateDataMask with [null, null] for filter_rang
userEvent.click(clearBtn);
});
expect(updateDataMaskSpy).not.toHaveBeenCalled();
const applyBtn = screen.getByTestId(getTestId('apply-button'));
await act(async () => {
userEvent.click(applyBtn);
});
expect(updateDataMaskSpy).toHaveBeenCalledWith(filterId, {
filterState: { value: [null, null] },
id: filterId,
filterState: { value: [null, null], validateStatus: undefined },
extraFormData: {},
});
updateDataMaskSpy.mockRestore();
});
test('handleClearAll only dispatches for filters present in dataMask', async () => {
test('Clear All + Apply only dispatches for filters present in dataMask', async () => {
const idInMask = 'NATIVE_FILTER-has-value';
const idNotInMask = 'NATIVE_FILTER-no-value';
const updateDataMaskSpy = jest.spyOn(dataMaskActions, 'updateDataMask');
@@ -631,7 +652,9 @@ test('handleClearAll only dispatches for filters present in dataMask', async ()
activeTabs: ['ROOT_ID'],
},
dataMask: {
[idInMask]: createDataMask(idInMask, ['v']),
[idInMask]: createDataMask(idInMask, ['v'], {
filters: [{ col: 'x', op: 'IN', val: ['v'] }],
}),
},
nativeFilters: {
filters: {
@@ -652,10 +675,16 @@ test('handleClearAll only dispatches for filters present in dataMask', async ()
await act(async () => {
userEvent.click(clearBtn);
});
expect(updateDataMaskSpy).not.toHaveBeenCalled();
const applyBtn = screen.getByTestId(getTestId('apply-button'));
await act(async () => {
userEvent.click(applyBtn);
});
expect(updateDataMaskSpy).toHaveBeenCalledTimes(1);
expect(updateDataMaskSpy).toHaveBeenCalledWith(idInMask, {
filterState: { value: undefined },
id: idInMask,
filterState: { value: undefined, validateStatus: undefined },
extraFormData: {},
});
updateDataMaskSpy.mockRestore();
@@ -790,18 +819,86 @@ test('FilterBar Clear All only clears in-scope filters, not out-of-scope ones',
await act(async () => {
userEvent.click(clearButton);
});
expect(updateDataMaskSpy).not.toHaveBeenCalled();
// Verify only the in-scope filter was cleared, not the out-of-scope ones
const clearedFilterIds = updateDataMaskSpy.mock.calls.map(call => call[0]);
expect(clearedFilterIds).toContain(inScopeFilterId);
expect(clearedFilterIds).not.toContain(outOfScopeRequiredFilterId);
expect(clearedFilterIds).not.toContain(outOfScopeNonRequiredFilterId);
// After Apply: only the in-scope filter was cleared. Out-of-scope filters
// retain their original values (Apply re-dispatches them unchanged).
const applyButton = screen.getByTestId(getTestId('apply-button'));
await act(async () => {
userEvent.click(applyButton);
});
// Verify the in-scope filter was cleared with the correct value
expect(updateDataMaskSpy).toHaveBeenCalledWith(inScopeFilterId, {
filterState: { value: undefined },
id: inScopeFilterId,
filterState: { value: undefined, validateStatus: undefined },
extraFormData: {},
});
// Out-of-scope filters keep their existing values; not cleared
const outOfScopeRequiredCall = updateDataMaskSpy.mock.calls.find(
call => call[0] === outOfScopeRequiredFilterId,
);
expect(outOfScopeRequiredCall?.[1]?.filterState?.value).toEqual(['value2']);
const outOfScopeNonRequiredCall = updateDataMaskSpy.mock.calls.find(
call => call[0] === outOfScopeNonRequiredFilterId,
);
expect(outOfScopeNonRequiredCall?.[1]?.filterState?.value).toEqual([
'value3',
]);
updateDataMaskSpy.mockRestore();
});
test('Clear All on a required filter disables Apply via validateStatus', async () => {
const filterId = 'NATIVE_FILTER-required-clear';
const updateDataMaskSpy = jest.spyOn(dataMaskActions, 'updateDataMask');
const requiredFilter = createFilter({
id: filterId,
name: 'Required Region',
filterType: 'filter_select',
targets: [{ datasetId: 7, column: { name: 'region' } }],
controlValues: { enableEmptyFilter: true },
chartsInScope: [18],
});
const state = {
...stateWithoutNativeFilters,
dashboardInfo: {
id: 1,
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.Vertical,
metadata: {
native_filter_configuration: [requiredFilter],
chart_configuration: {},
},
},
dashboardState: {
...stateWithoutNativeFilters.dashboardState,
activeTabs: ['ROOT_ID'],
},
dataMask: {
[filterId]: createDataMask(filterId, ['East'], {
filters: [{ col: 'region', op: 'IN', val: ['East'] }],
}),
},
nativeFilters: {
filters: { [filterId]: requiredFilter },
filtersState: {},
},
};
const props = createOpenedBarProps();
renderFilterBar(props, state);
await act(async () => {
jest.advanceTimersByTime(300);
});
const clearBtn = screen.getByTestId(getTestId('clear-button'));
await act(async () => {
userEvent.click(clearBtn);
});
// No dispatch yet; Apply should be disabled because the required filter is empty
expect(updateDataMaskSpy).not.toHaveBeenCalled();
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
updateDataMaskSpy.mockRestore();
});

View File

@@ -498,17 +498,20 @@ const FilterBar: FC<FiltersBarProps> = ({
// Range filters use [null, null] as the cleared value; others use undefined
const clearedValue =
filterType === 'filter_range' ? [null, null] : undefined;
const clearedDataMask = {
filterState: { value: clearedValue },
extraFormData: {},
};
const isRequired = !!filter.controlValues?.enableEmptyFilter;
if (dataMaskSelected[id]) {
dispatch(updateDataMask(id, clearedDataMask));
// Stage the cleared value locally; do NOT dispatch to Redux here.
// Persistence happens when the user clicks Apply.
setDataMaskSelected(draft => {
if (draft[id].filterState?.value !== undefined) {
draft[id].filterState!.value = clearedValue;
}
draft[id].extraFormData = {};
if (draft[id].filterState) {
draft[id].filterState!.validateStatus = isRequired
? 'error'
: undefined;
}
});
newClearAllTriggers[id] = true;
}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { forwardRef, RefObject } from 'react';
import { QueryData } from '@superset-ui/core';
import { QueryData, VizType } from '@superset-ui/core';
import { css, SupersetTheme } from '@apache-superset/core/theme';
import {
CachedLabel,
@@ -68,7 +68,9 @@ export const ChartPills = forwardRef(
const firstQueryResponse = queriesResponse?.[0];
// For table charts with server pagination, check second query for total count
const isTableChart = formData?.viz_type === 'table';
const isTableChart =
formData?.viz_type === VizType.Table ||
formData?.viz_type === VizType.TableAgGrid;
const hasCountQuery = queriesResponse && queriesResponse.length > 1;
const countFromSecondQuery = hasCountQuery
? queriesResponse[1]?.data?.[0]?.rowcount

View File

@@ -0,0 +1,59 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, waitFor } from 'spec/helpers/testing-library';
import { SupersetClient } from '@superset-ui/core';
import DatasetPanelWrapper from 'src/features/datasets/AddDataset/DatasetPanel';
jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);
afterEach(() => {
jest.restoreAllMocks();
});
test('fetches table metadata for schema-less database without schema', async () => {
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
json: {
name: 'my_table',
columns: [{ name: 'id', type: 'INTEGER', longType: 'INTEGER' }],
},
} as any);
render(
<DatasetPanelWrapper
tableName="my_table"
dbId={1}
database={{ supports_schemas: false }}
/>,
{ useRouter: true },
);
await waitFor(() => {
expect(getSpy).toHaveBeenCalledWith(
expect.objectContaining({
endpoint: expect.stringContaining('/api/v1/database/1/table_metadata/'),
}),
);
});
});

View File

@@ -22,6 +22,7 @@ import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import { DatasetObject } from 'src/features/datasets/AddDataset/types';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { type DatabaseObject } from 'src/components';
import { toQueryString } from 'src/utils/urlUtils';
import DatasetPanel from './DatasetPanel';
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
@@ -39,9 +40,9 @@ interface IColumnProps {
*/
tableName: string;
/**
* Name of the schema
* Name of the schema (optional for databases that don't support schemas)
*/
schema: string;
schema?: string | null;
}
export interface IDatasetPanelWrapperProps {
@@ -58,6 +59,10 @@ export interface IDatasetPanelWrapperProps {
*/
catalog?: string | null;
schema?: string | null;
/**
* The selected database object (used to check engine capabilities)
*/
database?: Partial<DatabaseObject> | null;
setHasColumns?: Function;
datasets?: DatasetObject[] | undefined;
}
@@ -67,6 +72,7 @@ const DatasetPanelWrapper = ({
dbId,
catalog,
schema,
database,
setHasColumns,
datasets,
}: IDatasetPanelWrapperProps) => {
@@ -128,12 +134,13 @@ const DatasetPanelWrapper = ({
useEffect(() => {
tableNameRef.current = tableName;
if (tableName && schema && dbId) {
getTableMetadata({ tableName, dbId, schema });
const schemaRequired = database?.supports_schemas !== false;
if (tableName && dbId && (schema || !schemaRequired)) {
getTableMetadata({ tableName, dbId, schema: schema || undefined });
}
// getTableMetadata is a const and should not be in dependency array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableName, dbId, schema]);
}, [tableName, dbId, schema, database]);
return (
<DatasetPanel

View File

@@ -358,6 +358,66 @@ test('useDatasetsList skips fetching when db.id is undefined', () => {
expect(result.current.datasetNames).toEqual([]);
});
test('useDatasetsList fetches datasets for schema-less databases without schema filter', async () => {
const schemalessDb = {
id: 2,
database_name: 'ydb',
owners: [1] as [number],
supports_schemas: false,
};
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
json: {
count: 1,
result: [{ id: 10, table_name: 'my_table', schema: null }],
},
} as unknown as JsonResponse);
const { result } = renderHook(() => useDatasetsList(schemalessDb, null));
await waitFor(() => {
expect(result.current.datasets).toHaveLength(1);
});
expect(result.current.datasetNames).toEqual(['my_table']);
expect(getSpy).toHaveBeenCalledTimes(1);
// Verify the API was called without a schema filter
const callArg = getSpy.mock.calls[0]?.[0]?.endpoint;
expect(callArg).toBeDefined();
const risonParam = new URL(callArg!, 'http://localhost').searchParams.get(
'q',
);
expect(risonParam).toBeTruthy();
const decoded = rison.decode(risonParam!) as {
filters: Array<{ col: string; opr: string; value: unknown }>;
};
// Only database filter and sql filter — no schema filter
const schemaFilter = decoded.filters.find(f => f.col === 'schema');
expect(schemaFilter).toBeUndefined();
const dbFilter = decoded.filters.find(f => f.col === 'database');
expect(dbFilter).toEqual({ col: 'database', opr: 'rel_o_m', value: 2 });
});
test('useDatasetsList skips fetching when schema-less database id is undefined', () => {
const getSpy = jest.spyOn(SupersetClient, 'get');
const schemalessDb = {
database_name: 'ydb',
owners: [1] as [number],
supports_schemas: false,
} as typeof mockDb & { supports_schemas: boolean };
const { result } = renderHook(() => useDatasetsList(schemalessDb, null));
// No db.id — should NOT call API even for schema-less DB
expect(getSpy).not.toHaveBeenCalled();
expect(result.current.datasets).toEqual([]);
});
test('useDatasetsList encodes schemas with spaces and special characters in endpoint URL', async () => {
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
json: { count: 0, result: [] },

View File

@@ -37,7 +37,8 @@ const useDatasetsList = (
schema: string | null | undefined,
) => {
const [datasets, setDatasets] = useState<DatasetObject[]>([]);
const encodedSchema = schema ? encodeURIComponent(schema) : undefined;
const supportsSchemas = db?.supports_schemas !== false;
const encodedSchema = schema ? encodeURIComponent(schema) : null;
const getDatasetsList = useCallback(async (filters: object[]) => {
let results: DatasetObject[] = [];
@@ -77,14 +78,16 @@ const useDatasetsList = (
useEffect(() => {
const filters = [
{ col: 'database', opr: 'rel_o_m', value: db?.id },
{ col: 'schema', opr: 'eq', value: encodedSchema },
...(supportsSchemas
? [{ col: 'schema', opr: 'eq', value: encodedSchema }]
: []),
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: true },
];
if (schema && db?.id !== undefined) {
if (db?.id !== undefined && (schema || !supportsSchemas)) {
getDatasetsList(filters);
}
}, [db?.id, schema, encodedSchema, getDatasetsList]);
}, [db?.id, schema, encodedSchema, supportsSchemas, getDatasetsList]);
const datasetNames = useMemo(
() => datasets?.map(dataset => dataset.table_name),

View File

@@ -240,6 +240,35 @@ describe('useTables hook', () => {
expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1);
});
test('fetches tables without schema when supportsSchemas is false', async () => {
const expectDbId = 'db1';
const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`;
fetchMock.get(tableApiRoute, fakeApiResult);
fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, {
count: 0,
result: [],
});
fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, {
result: fakeSchemaApiResult,
});
const { result, waitFor } = renderHook(
() =>
useTables({
dbId: expectDbId,
supportsSchemas: false,
}),
{
wrapper: createWrapper({
useRedux: true,
store,
}),
},
);
// Tables are fetched even though no schema is provided or validated against schemaOptions
await waitFor(() => expect(result.current.data).toEqual(expectedData));
expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1);
});
test('returns refreshed data after expires', async () => {
const expectDbId = 'db1';
const expectedSchema = 'schema1';

View File

@@ -96,7 +96,9 @@ type TableMetadataResponse = {
export type TableExtendedMetadata = Record<string, string>;
type Params = Omit<FetchTablesQueryParams, 'forceRefresh'>;
type Params = Omit<FetchTablesQueryParams, 'forceRefresh'> & {
supportsSchemas?: boolean;
};
const tableApi = api.injectEndpoints({
endpoints: builder => ({
@@ -166,7 +168,14 @@ export const {
} = tableApi;
export function useTables(options: Params) {
const { dbId, catalog, schema, onSuccess, onError } = options || {};
const {
dbId,
catalog,
schema,
supportsSchemas = true,
onSuccess,
onError,
} = options || {};
const isMountedRef = useRef(false);
const { currentData: schemaOptions, isFetching } = useSchemas({
dbId,
@@ -177,9 +186,9 @@ export function useTables(options: Params) {
[schemaOptions],
);
const enabled = Boolean(
dbId && schema && !isFetching && schemaOptionsMap.has(schema),
);
const enabled = supportsSchemas
? Boolean(dbId && schema && !isFetching && schemaOptionsMap.has(schema))
: Boolean(dbId);
const result = useTablesQuery(
{ dbId, catalog, schema, forceRefresh: false },

View File

@@ -122,6 +122,7 @@ export default function AddDataset() {
dbId={dataset?.db?.id}
catalog={dataset?.catalog}
schema={dataset?.schema}
database={dataset?.db}
setHasColumns={setHasColumns}
datasets={datasets}
/>

View File

@@ -0,0 +1,20 @@
/**
* 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.
*/
/// <reference types="@emotion/jest" />

View File

@@ -44,7 +44,7 @@ class TablesDatabaseCommand(BaseCommand):
self,
db_id: int,
catalog_name: str | None,
schema_name: str,
schema_name: str | None,
force: bool,
):
self._db_id = db_id
@@ -55,6 +55,8 @@ class TablesDatabaseCommand(BaseCommand):
def run(self) -> dict[str, Any]:
self.validate()
self._catalog_name = self._catalog_name or self._model.get_default_catalog()
if not self._model.db_engine_spec.supports_schemas:
self._schema_name = None
try:
tables = security_manager.get_datasources_accessible_by_user(
database=self._model,

View File

@@ -1067,6 +1067,9 @@ class EngineInformationSchema(Schema):
supports_oauth2 = fields.Boolean(
metadata={"description": "The database supports OAuth2"}
)
supports_schemas = fields.Boolean(
metadata={"description": "The database uses schemas to organize tables"}
)
class DatabaseConnectionSchema(Schema):

View File

@@ -577,6 +577,10 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
# Does the DB engine spec support cross-catalog queries?
supports_cross_catalog_queries = False
# Does the DB engine support schemas? When set to False the schema selector is
# hidden in the dataset creation UI and schema is not required for table access.
supports_schemas = True
# Does the engine supports OAuth 2.0? This requires logic to be added to one of the
# the user impersonation methods to handle personal tokens.
supports_oauth2 = False
@@ -2523,6 +2527,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
"disable_ssh_tunneling": cls.disable_ssh_tunneling,
"supports_dynamic_catalog": cls.supports_dynamic_catalog,
"supports_oauth2": cls.supports_oauth2,
"supports_schemas": cls.supports_schemas,
}
@classmethod

View File

@@ -51,6 +51,7 @@ class YDBEngineSpec(BaseEngineSpec):
disable_ssh_tunneling = False
supports_file_upload = False
supports_schemas = False
allows_alias_in_orderby = True

View File

@@ -3427,6 +3427,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": True,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3455,6 +3456,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": True,
"disable_ssh_tunneling": True,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3513,6 +3515,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3558,6 +3561,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": True,
"supports_oauth2": True,
"supports_schemas": True,
},
"supports_oauth2": True,
},
@@ -3616,6 +3620,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3630,6 +3635,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3664,6 +3670,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},
@@ -3678,6 +3685,7 @@ class TestDatabaseApi(SupersetTestCase):
"supports_dynamic_catalog": False,
"disable_ssh_tunneling": False,
"supports_oauth2": False,
"supports_schemas": True,
},
"supports_oauth2": False,
},

View File

@@ -148,6 +148,78 @@ def test_tables_with_catalog(
)
@pytest.fixture
def database_without_schema_support(mocker: MockerFixture) -> MagicMock:
"""
Mock a database that does not support schemas (e.g. YDB).
"""
mocker.patch("superset.commands.database.tables.db")
database = mocker.MagicMock()
database.database_name = "test_database"
database.get_default_catalog.return_value = None
database.db_engine_spec.supports_schemas = False
database.get_all_table_names_in_schema.return_value = {
("table1", None, None),
("table2", None, None),
}
database.get_all_view_names_in_schema.return_value = set()
database.get_all_materialized_view_names_in_schema.return_value = set()
DatabaseDAO = mocker.patch("superset.commands.database.tables.DatabaseDAO") # noqa: N806
DatabaseDAO.find_by_id.return_value = database
return database
def test_tables_without_schema_support(
mocker: MockerFixture,
database_without_schema_support: MagicMock,
) -> None:
"""
Test that schema is overridden to None for databases that don't support schemas.
Any schema name passed to the command is ignored.
"""
get_datasources_accessible_by_user = mocker.patch.object(
security_manager,
"get_datasources_accessible_by_user",
side_effect=[
{
DatasourceName("table1", None), # type: ignore[arg-type]
DatasourceName("table2", None), # type: ignore[arg-type]
},
set(), # Empty set for views
set(), # Empty set for materialized views
],
)
db = mocker.patch("superset.commands.database.tables.db")
db.session.query().filter().options().all.return_value = []
# Schema name should be overridden to None when supports_schemas=False
payload = TablesDatabaseCommand(1, None, "any_schema", False).run()
assert payload["count"] == 2
assert {item["value"] for item in payload["result"]} == {"table1", "table2"}
# Verify schema was set to None when calling the underlying DB methods
database_without_schema_support.get_all_table_names_in_schema.assert_called_with(
catalog=None,
schema=None,
force=False,
cache=database_without_schema_support.table_cache_enabled,
cache_timeout=database_without_schema_support.table_cache_timeout,
)
# Verify security_manager was called with schema=None
get_datasources_accessible_by_user.assert_any_call(
database=database_without_schema_support,
catalog=None,
schema=None,
datasource_names=mocker.ANY,
)
def test_tables_without_catalog(
mocker: MockerFixture,
database_without_catalog: MockerFixture,

View File

@@ -243,6 +243,7 @@ def test_database_connection(
"supports_dynamic_catalog": False,
"supports_file_upload": True,
"supports_oauth2": True,
"supports_schemas": True,
},
"expose_in_sqllab": True,
"extra": '{\n "metadata_params": {},\n "engine_params": {},\n "metadata_cache_timeout": {},\n "schemas_allowed_for_file_upload": []\n}\n', # noqa: E501
@@ -332,6 +333,7 @@ def test_database_connection(
"supports_dynamic_catalog": False,
"supports_file_upload": True,
"supports_oauth2": True,
"supports_schemas": True,
},
"expose_in_sqllab": True,
"force_ctas_schema": None,