Compare commits

...

24 Commits

Author SHA1 Message Date
Evan
5e0b430a03 fix(migration): chain down_revision to current master head (31dae2559c05)
Master's single head advanced to 31dae2559c05; this migration still
pointed at 33d7e0e21daa, making it a sibling and producing multiple
alembic heads. Re-point so the chain is linear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:39:16 -07:00
Evan Rusackas
b725e11199 Merge branch 'master' into fix/security-menu-case-mysql 2026-06-10 16:19:57 -07:00
Dylan Cavalcante
f79a88c685 test(core): add unit tests for split function (#40819)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:12:35 -07:00
dependabot[bot]
b1d965932d chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.60.0 to 8.60.1 in /superset-websocket (#40888)
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-10 12:46:38 -07:00
dependabot[bot]
7d046340dc chore(deps): bump ag-grid-react from 35.3.0 to 35.3.1 in /superset-frontend/packages/superset-ui-core (#40924)
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-10 12:46:24 -07:00
dependabot[bot]
aa872cd0a1 chore(deps): bump dompurify from 3.4.9 to 3.4.8 in /superset-frontend/packages/superset-ui-core (#40938)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:45:33 -07:00
dependabot[bot]
b2c5a1ecb3 chore(deps): bump jsonpath-ng from 1.7.0 to 1.8.0 (#40940)
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-10 12:45:21 -07:00
dependabot[bot]
6cd9bdee0b chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.3 to 0.10.13 in /superset-frontend (#40925)
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-10 12:44:40 -07:00
dependabot[bot]
a8a1d9c17d chore(deps): bump morgan from 1.10.1 to 1.11.0 in /superset-websocket/utils/client-ws-app (#40921)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:43:33 -07:00
dependabot[bot]
97058d2cf0 chore(deps): bump fuse.js from 7.3.0 to 7.4.1 in /superset-frontend (#40922)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:43:19 -07:00
dependabot[bot]
ef57409209 chore(deps): bump ag-grid-community from 35.3.0 to 35.3.1 in /superset-frontend/packages/superset-ui-core (#40923)
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-10 12:43:06 -07:00
dependabot[bot]
5f06e66cf1 chore(deps): bump @deck.gl/mapbox from 9.3.2 to 9.3.3 in /superset-frontend (#40927)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:42:22 -07:00
dependabot[bot]
11af932099 chore(deps): bump dompurify from 3.4.7 to 3.4.8 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#40937)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:42:06 -07:00
dependabot[bot]
c9c05d8d0a chore(deps-dev): update thrift requirement from <1.0.0,>=0.14.1 to >=0.23.0,<1.0.0 (#40942)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:36:51 -07:00
dependabot[bot]
0f59705806 chore(deps): bump wtforms from 3.2.1 to 3.2.2 (#40943)
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-10 12:36:26 -07:00
dependabot[bot]
320965612d chore(deps-dev): update clickhouse-connect requirement from <2.0,>=0.13.0 to >=1.1.1,<2.0 (#40944)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 12:36:03 -07:00
dependabot[bot]
c3df60c12b chore(deps): bump selenium from 4.32.0 to 4.44.0 (#40945)
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-10 12:34:01 -07:00
dependabot[bot]
4f69949c10 chore(deps-dev): bump eslint-plugin-storybook from 10.4.1 to 10.4.2 in /superset-frontend (#40949)
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-10 12:31:47 -07:00
bogdanmoale
3380496e9f feat(i18n): add Romanian (ro) translations (#36712)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-10 12:25:42 -07:00
Michael S. Molina
248ccadecd fix(extensions): load extensions async to avoid blocking initial page render (#40915)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:23:38 -03:00
Joe Li
cc5a3ddd05 test(dashboard-filter): RTL coverage for horizontal filter bar (#40782)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:53:56 -07:00
Evan
95b46bcf02 fix(migration): point down_revision to actual master head (33d7e0e21daa)
Previous commits used a1b2c3d4e5f6 then ce6bd21901ab, both of which are
mid-chain on master. The true head is 33d7e0e21daa (add_semantic_layers).
Branching off mid-chain caused 'Multiple head revisions' in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:42:00 -07:00
Evan
7f804262ed fix(migration): chain to correct head and transfer role bindings on merge
- Point down_revision to ce6bd21901ab (the actual current chain head,
  not a1b2c3d4e5f6 which already had two successors)
- In the "both rows exist" merge path, transfer ab_permission_view_role
  bindings from the lowercase PVM to the surviving uppercase PVM before
  deleting the duplicate, so no role silently loses a Security permission

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:29:57 -07:00
Evan
2063484ef4 fix(security): correct class_permission_name case for Security menu views
Five views that back the Security menu navbar items declared
`class_permission_name = "security"` (lowercase). Flask-AppBuilder uses
this name to look up or insert the corresponding `ab_view_menu` row. On
MySQL's default case-insensitive collation, the lookup finds whatever
row exists first, meaning the DB can end up storing `"security"` instead
of `"Security"`. Because Superset's Python comparisons in
`sync_role_definitions` and `SupersetSecurityManager` are case-sensitive,
`"security" != "Security"` → the Security menu disappears from the UI on
MySQL/MariaDB deployments.

Fixes:
- Change `class_permission_name` to `"Security"` in users_list.py,
  roles.py, groups.py, logs.py, and user_registrations.py.
- Add migration `b4a3f2e1d0c9` that normalises any existing lowercase
  `"security"` row: renames it to `"Security"` if it is the only row,
  or merges its `ab_permission_view` entries into the correctly-cased
  row and deletes the duplicate if both exist.

Closes #40330

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:17:16 -07:00
41 changed files with 17460 additions and 350 deletions

View File

@@ -64,7 +64,7 @@ dependencies = [
"holidays>=0.45, <1",
"humanize",
"isodate",
"jsonpath-ng>=1.6.1, <2",
"jsonpath-ng>=1.8.0, <2",
"Mako>=1.2.2",
"markdown>=3.10.2",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
@@ -94,7 +94,7 @@ dependencies = [
"PyJWT>=2.4.0, <3.0",
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.14.0, <5.0",
"selenium>=4.44.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
@@ -107,7 +107,7 @@ dependencies = [
"typing-extensions>=4, <5",
"waitress; sys_platform == 'win32'",
"watchdog>=6.0.0",
"wtforms>=2.3.3, <4",
"wtforms>=3.2.2, <4",
"wtforms-json",
"xlsxwriter>=3.2.9, <3.3",
]
@@ -121,7 +121,7 @@ bigquery = [
"sqlalchemy-bigquery>=1.15.0",
"google-cloud-bigquery>=3.10.0",
]
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
d1 = [
@@ -161,7 +161,7 @@ hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.14.1, <1.0.0",
"thrift>=0.23.0, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.23"]
@@ -195,7 +195,7 @@ spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.14.1, <1",
"thrift>=0.23.0, <1",
]
tdengine = [
"taospy>=2.7.21",

View File

@@ -50,7 +50,7 @@ cattrs==25.1.1
# via requests-cache
celery==5.5.2
# via apache-superset (pyproject.toml)
certifi==2025.6.15
certifi==2026.5.20
# via
# requests
# selenium
@@ -194,7 +194,7 @@ jinja2==3.1.6
# via
# flask
# flask-babel
jsonpath-ng==1.7.0
jsonpath-ng==1.8.0
# via apache-superset (pyproject.toml)
jsonschema==4.23.0
# via
@@ -286,8 +286,6 @@ pillow==12.2.0
# via apache-superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11
# via jsonpath-ng
polyline==2.0.2
# via apache-superset (pyproject.toml)
prison==0.2.1
@@ -380,7 +378,7 @@ rpds-py==0.25.0
# referencing
rsa==4.9.1
# via google-auth
selenium==4.32.0
selenium==4.44.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
@@ -423,7 +421,7 @@ sshtunnel==0.4.0
# via apache-superset (pyproject.toml)
tabulate==0.10.0
# via apache-superset (pyproject.toml)
trio==0.30.0
trio==0.33.0
# via
# selenium
# trio-websocket
@@ -480,7 +478,7 @@ wrapt==1.17.2
# via deprecated
wsproto==1.2.0
# via trio-websocket
wtforms==3.2.1
wtforms==3.2.2
# via
# apache-superset (pyproject.toml)
# flask-appbuilder

View File

@@ -112,7 +112,7 @@ celery==5.5.2
# via
# -c requirements/base-constraint.txt
# apache-superset
certifi==2025.6.15
certifi==2026.5.20
# via
# -c requirements/base-constraint.txt
# httpcore
@@ -471,7 +471,7 @@ jmespath==1.1.0
# via
# boto3
# botocore
jsonpath-ng==1.7.0
jsonpath-ng==1.8.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -674,10 +674,6 @@ platformdirs==4.3.8
# virtualenv
pluggy==1.5.0
# via pytest
ply==3.11
# via
# -c requirements/base-constraint.txt
# jsonpath-ng
polib==1.2.0
# via apache-superset
polyline==2.0.2
@@ -925,7 +921,7 @@ s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.32.0
selenium==4.44.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -1023,7 +1019,7 @@ tqdm==4.67.1
# prophet
trino==0.330.0
# via apache-superset
trio==0.30.0
trio==0.33.0
# via
# -c requirements/base-constraint.txt
# selenium
@@ -1125,7 +1121,7 @@ wsproto==1.2.0
# via
# -c requirements/base-constraint.txt
# trio-websocket
wtforms==3.2.1
wtforms==3.2.2
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -48,6 +48,7 @@ module.exports = {
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-export-namespace-from',
['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-class-static-block',
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],

View File

@@ -95,7 +95,7 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.3.0",
"fuse.js": "^7.4.1",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
@@ -178,13 +178,13 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.3",
"@formatjs/intl-durationformat": "^0.10.13",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.1",
"@storybook/addon-links": "10.4.1",
"@storybook/react-webpack5": "10.4.1",
"@storybook/addon-docs": "10.4.2",
"@storybook/addon-links": "10.4.2",
"@storybook/react-webpack5": "10.4.2",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
@@ -242,7 +242,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.1",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -272,7 +272,7 @@
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.1",
"storybook": "10.4.2",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",
@@ -3939,50 +3939,38 @@
}
},
"node_modules/@formatjs/bigdecimal": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.0.tgz",
"integrity": "sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.5.tgz",
"integrity": "sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.2.0.tgz",
"integrity": "sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/bigdecimal": "0.2.0",
"@formatjs/fast-memoize": "3.1.1",
"@formatjs/intl-localematcher": "0.8.2"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz",
"integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz",
"integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==",
"dev": true,
"license": "MIT"
},
"node_modules/@formatjs/intl-durationformat": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.3.tgz",
"integrity": "sha512-xRS3GaOlsQLwz0n56SvaddwEnl2NLPKBvYg2M32ak/27dodmVxFJz3j7Nqj7EwKyHTu3f/e+BeoKPrIDUSXTuQ==",
"version": "0.10.13",
"resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.13.tgz",
"integrity": "sha512-A1dBcOh1YrcRf/AbmZHFVXgIYkpAaFgyGaYavO/KutbqEXY3HI63o2E1ctmxmllfg3qn3TZGtZux42EFwHNTbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "3.2.0",
"@formatjs/intl-localematcher": "0.8.2"
"@formatjs/bigdecimal": "0.2.5",
"@formatjs/intl-localematcher": "0.8.9"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz",
"integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.1"
"@formatjs/fast-memoize": "3.1.5"
}
},
"node_modules/@gar/promise-retry": {
@@ -9696,16 +9684,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.1.tgz",
"integrity": "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.2.tgz",
"integrity": "sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.4.1",
"@storybook/csf-plugin": "10.4.2",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.4.1",
"@storybook/react-dom-shim": "10.4.2",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -9716,7 +9704,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1"
"storybook": "^10.4.2"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9724,45 +9712,10 @@
}
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.1.tgz",
"integrity": "sha512-WdPepGBxDGOUDjYd8KxMtcf+us/2PAcnBczl77XtrnxxHNs0jWesxKkiJ9yiuGrge4BPhDeAj6rxjbBoaHxLBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.1",
"vite": "*",
"webpack": "*"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/@storybook/addon-links": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.1.tgz",
"integrity": "sha512-h/5D23GwMuHA55sB7XDyhByF9psF7UFmaQOn72pjNAarew5eOpue5A+jXk3AKEYokHbvgQaoz+FrvWo9GEfSKQ==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.2.tgz",
"integrity": "sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9775,7 +9728,7 @@
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1"
"storybook": "^10.4.2"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9787,13 +9740,13 @@
}
},
"node_modules/@storybook/builder-webpack5": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz",
"integrity": "sha512-3Ah4jUjg8nEms/5JV6odtQj9+pQ1DT/04s/V6dZKThGdl85YTrYUZV5OTgbNxYbmQn/TwpWWjQlcW8ulpo2WBw==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.2.tgz",
"integrity": "sha512-nhmV0+nThCgy1y5742SS7c4vJrd5/1KfCXCNfsJ1v4Rkq7NIQnUhEIBwkSaY63lqH7FRHlFxIjwGS63veiCJuw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.4.1",
"@storybook/core-webpack": "10.4.2",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
@@ -9814,7 +9767,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.4.1"
"storybook": "^10.4.2"
},
"peerDependenciesMeta": {
"typescript": {
@@ -9823,9 +9776,9 @@
}
},
"node_modules/@storybook/core-webpack": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.1.tgz",
"integrity": "sha512-Wert/4ou5WRl8WYWWS8bBW7Lxa/ASMEuQ3EVuG3SITAtPNvKDKqTFBjZLx9eJSefkX6fJ3yG85FFUOPsv6GemQ==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.2.tgz",
"integrity": "sha512-qnYKMruU8lvI4yaq2PA9Gmxjrc7EZ3DRBI/cVKwEgOIREoxzr1F1IE7t7+325k9Phylue7E5rD3A7yjxeEKUyw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9836,7 +9789,42 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.4.1"
"storybook": "^10.4.2"
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.2.tgz",
"integrity": "sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.2",
"vite": "*",
"webpack": "*"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
},
"rollup": {
"optional": true
},
"vite": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/@storybook/global": {
@@ -9858,13 +9846,13 @@
}
},
"node_modules/@storybook/preset-react-webpack": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.1.tgz",
"integrity": "sha512-uAR/C/oDZYhReaYpD4Rd5S4VWcXP2XO8+BwXwanKt4UHbYfOw7AQgBTeZ/6Wns/0xIXhOoA1rxO5TA2wDLUjLA==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.2.tgz",
"integrity": "sha512-21ld380f0/jTTitkfhTKgP3FBnVAgMu1P1ymrRyiFYJVSJBA5YejndFFBo0ugq9iGGsHXrVdOphC/OJKbTSWRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.4.1",
"@storybook/core-webpack": "10.4.2",
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/semver": "^7.7.1",
"magic-string": "^0.30.5",
@@ -9881,7 +9869,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1"
"storybook": "^10.4.2"
},
"peerDependenciesMeta": {
"typescript": {
@@ -9890,14 +9878,14 @@
}
},
"node_modules/@storybook/react": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.1.tgz",
"integrity": "sha512-WuYz4NaUk4gmFAMliSpCbV8w6jP5OY9juBfw1huwzu2S/k5FhnVXwmrUaL0fmf3Bq/7NgkzmBBbZr6I6LuHayQ==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.2.tgz",
"integrity": "sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "10.4.1",
"@storybook/react-dom-shim": "10.4.2",
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
@@ -9910,7 +9898,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1",
"storybook": "^10.4.2",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -9990,9 +9978,9 @@
}
},
"node_modules/@storybook/react-dom-shim": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz",
"integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.2.tgz",
"integrity": "sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==",
"dev": true,
"license": "MIT",
"funding": {
@@ -10004,7 +9992,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1"
"storybook": "^10.4.2"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10016,15 +10004,15 @@
}
},
"node_modules/@storybook/react-webpack5": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.1.tgz",
"integrity": "sha512-2jF231DrEk70I8+wVakCnKtpweGFNfxdaov883Rve0TFvhxZs42Y9PpKzSf4rusvSrWc9jdWuJ2k7ERbS50MLg==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.2.tgz",
"integrity": "sha512-x7xwGLxU0w6/qi29/cHhua8qiCvfE05ku4pPLTXF8TsP/zfGsY8tbdlKO2+YKp+iBG8vafVc//ZXOAty1oypDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-webpack5": "10.4.1",
"@storybook/preset-react-webpack": "10.4.1",
"@storybook/react": "10.4.1"
"@storybook/builder-webpack5": "10.4.2",
"@storybook/preset-react-webpack": "10.4.2",
"@storybook/react": "10.4.2"
},
"funding": {
"type": "opencollective",
@@ -10033,7 +10021,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.1",
"storybook": "^10.4.2",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -19421,9 +19409,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.1.tgz",
"integrity": "sha512-sLEvd/7lg/LtXwMjj3iFxZtoeAC/8l1Qhuw3Noa8iF8i0UIgAejUs7k6DNSqHkwrPR8caWT4+3fxdMXs1iGLTg==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.4.2.tgz",
"integrity": "sha512-l3/vzLRmb8VSi3X1Bo6/Pa+64naw1jFsZE5jPPA4izvVdNhH1rF4rGuOC3kDTU926qKVBQtKua8D24XWQtvcGg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19431,7 +19419,7 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "^10.4.1"
"storybook": "^10.4.2"
}
},
"node_modules/eslint-plugin-testing-library": {
@@ -20933,9 +20921,9 @@
}
},
"node_modules/fuse.js": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.0.tgz",
"integrity": "sha512-3UqmoSFwzX1sNB1YSk+Co0EdH29XCW2p9g48OAiy93cjKqzuABsqw2VIgSN3CmsT/wo6pIJ3F0Jxeiiby8rhIQ==",
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.4.1.tgz",
"integrity": "sha512-AY7lKAXK71hi3WgUvDy6oZL67UEHOOtvCAwVdOXHyJd6ZzftBy7QqxuXt4HxmmAhYjmp/YCuOELZtIvAdlZ+fw==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
@@ -39048,9 +39036,9 @@
}
},
"node_modules/storybook": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.1.tgz",
"integrity": "sha512-V1Zd2e+gBFufqAQVZ1JR8KLqALsEZ3JYSBnWwQbKa6zCfWWanR6AFMyuOkLt2gZOgGp3h2Riuz88pGNVTQSG0A==",
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.2.tgz",
"integrity": "sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -45375,7 +45363,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.3",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -45423,9 +45411,9 @@
}
},
"plugins/preset-chart-deckgl/node_modules/@deck.gl/mapbox": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz",
"integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==",
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.3.tgz",
"integrity": "sha512-aUPqrwF6wkx+EtvKA3SaiK+UROMnZSmgEJWZ1qSKFSiH//kPuo5imbtXyan8sGhOet7NjnfEwJqFA3EBk7zDLA==",
"license": "MIT",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0"

View File

@@ -178,7 +178,7 @@
"echarts": "^5.6.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.3.5",
"fuse.js": "^7.3.0",
"fuse.js": "^7.4.1",
"geolib": "^3.3.14",
"geostyler": "^18.6.0",
"geostyler-data": "^1.1.0",
@@ -261,13 +261,13 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.3",
"@formatjs/intl-durationformat": "^0.10.13",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.1",
"@storybook/addon-links": "10.4.1",
"@storybook/react-webpack5": "10.4.1",
"@storybook/addon-docs": "10.4.2",
"@storybook/addon-links": "10.4.2",
"@storybook/react-webpack5": "10.4.2",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.40",
@@ -325,7 +325,7 @@
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.4",
"eslint-plugin-storybook": "10.4.1",
"eslint-plugin-storybook": "10.4.2",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -355,7 +355,7 @@
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.1",
"storybook": "10.4.2",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",

View File

@@ -37,7 +37,7 @@
* ```
*/
import { Disposable } from '../common';
import { Disposable, Event } from '../common';
/**
* Represents a menu item that links a view to a command.
@@ -102,3 +102,37 @@ export declare function registerMenuItem(
* ```
*/
export declare function getMenu(location: string): Menu | undefined;
/**
* Event fired when a menu item is registered.
*/
export interface MenuItemRegisteredEvent {
/** The menu item that was registered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is unregistered.
*/
export interface MenuItemUnregisteredEvent {
/** The menu item that was unregistered. */
item: MenuItem;
/** The location where the item was registered. */
location: string;
/** The group the item was placed in. */
group: 'primary' | 'secondary' | 'context';
}
/**
* Event fired when a menu item is registered.
*/
export declare const onDidRegisterMenuItem: Event<MenuItemRegisteredEvent>;
/**
* Event fired when a menu item is unregistered.
*/
export declare const onDidUnregisterMenuItem: Event<MenuItemUnregisteredEvent>;

View File

@@ -36,7 +36,7 @@
*/
import { ReactElement } from 'react';
import { Disposable } from '../common';
import { Disposable, Event } from '../common';
/**
* Represents a contributed view in the application.
@@ -88,3 +88,33 @@ export declare function registerView(
* ```
*/
export declare function getViews(location: string): View[] | undefined;
/**
* Event fired when a view is registered.
*/
export interface ViewRegisteredEvent {
/** The descriptor of the view that was registered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is unregistered.
*/
export interface ViewUnregisteredEvent {
/** The descriptor of the view that was unregistered. */
view: View;
/** The location where the view was registered. */
location: string;
}
/**
* Event fired when a view is registered.
*/
export declare const onDidRegisterView: Event<ViewRegisteredEvent>;
/**
* Event fired when a view is unregistered.
*/
export declare const onDidUnregisterView: Event<ViewUnregisteredEvent>;

View File

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

View File

@@ -35,7 +35,7 @@ test('format milliseconds in human readable format with default options', () =>
});
test('format seconds in human readable format with default options', () => {
const formatter = createDurationFormatter({ multiplier: 1000 });
expect(formatter(-0.5)).toBe('-0s');
expect(formatter(-0.5)).toBe('0s');
expect(formatter(0.5)).toBe('0s');
expect(formatter(1)).toBe('1s');
expect(formatter(30)).toBe('30s');

View File

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

View File

@@ -29,7 +29,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.3",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",

View File

@@ -19,6 +19,7 @@
import {
DataMaskStateWithId,
ExtraFormData,
Filter,
NativeFiltersState,
NativeFilterType,
} from '@superset-ui/core';
@@ -458,6 +459,25 @@ export const mockQueryDataForCountries = [
{ country_name: 'Zimbabwe', 'SUM(SP_POP_TOTL)': 509866860 },
];
export const createSelectNativeFilter = (
id: string,
name: string,
column: string = name,
): Filter => ({
id,
name,
type: NativeFilterType.NativeFilter,
filterType: 'filter_select',
targets: [{ datasetId: 2, column: { name: column } }],
defaultDataMask: { filterState: { value: null }, extraFormData: {} },
controlValues: {},
cascadeParentIds: [],
scope: { rootPath: ['ROOT_ID'], excluded: [] },
description: '',
chartsInScope: [],
tabsInScope: [],
});
export const buildNativeFilter = (
id: string,
name: string,

View File

@@ -22,7 +22,7 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
import { css, styled } from '@apache-superset/core/theme';
import { useComponentDidUpdate } from '@superset-ui/core';
import { Grid } from '@superset-ui/core/components';
import { views } from 'src/core';
import { useViews } from 'src/core';
import { Splitter } from 'src/components/Splitter';
import useEffectEvent from 'src/hooks/useEffectEvent';
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
@@ -96,7 +96,7 @@ const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
setRightWidth(possibleRightWidth);
}
};
const viewItems = views.getViews(ViewLocations.sqllab.rightSidebar) || [];
const viewItems = useViews(ViewLocations.sqllab.rightSidebar) || [];
return (
<StyledContainer>

View File

@@ -31,7 +31,7 @@ import { Icons } from '@superset-ui/core/components/Icons';
import { SqlLabRootState } from 'src/SqlLab/types';
import { ViewLocations } from 'src/SqlLab/contributions';
import PanelToolbar from 'src/components/PanelToolbar';
import { views } from 'src/core';
import { useViews } from 'src/core';
import { resolveView } from 'src/core/views';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import useLogAction from 'src/logger/useLogAction';
@@ -107,7 +107,7 @@ const SouthPane = ({
const editorId = tabViewId ?? id;
const theme = useTheme();
const dispatch = useAppDispatch();
const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
const viewItems = useViews(ViewLocations.sqllab.panels) || [];
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
offline,

View File

@@ -19,7 +19,7 @@
import { styled } from '@apache-superset/core/theme';
import { Flex } from '@superset-ui/core/components';
import ViewListExtension from 'src/components/ViewListExtension';
import { views } from 'src/core';
import { useViews } from 'src/core';
import { SQL_EDITOR_STATUSBAR_HEIGHT } from 'src/SqlLab/constants';
import { ViewLocations } from 'src/SqlLab/contributions';
@@ -38,7 +38,7 @@ const Container = styled(Flex)`
`;
const StatusBar = () => {
const statusBarViews = views.getViews(ViewLocations.sqllab.statusBar) || [];
const statusBarViews = useViews(ViewLocations.sqllab.statusBar) || [];
return (
<>

View File

@@ -17,11 +17,12 @@
* under the License.
*/
import { useMemo } from 'react';
import { useMenu } from 'src/core';
import { css, useTheme } from '@apache-superset/core/theme';
import { Button, Divider, Dropdown } from '@superset-ui/core/components';
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
import { Icons } from '@superset-ui/core/components/Icons';
import { commands, menus } from 'src/core';
import { commands } from 'src/core';
export interface PanelToolbarProps {
viewId: string;
@@ -35,7 +36,7 @@ const PanelToolbar = ({
defaultSecondaryActions,
}: PanelToolbarProps) => {
const theme = useTheme();
const menu = menus.getMenu(viewId);
const menu = useMenu(viewId);
const primaryItems = menu?.primary || [];
const secondaryItems = menu?.secondary || [];

View File

@@ -39,8 +39,7 @@ jest.mock('./EditorProviders', () => ({
getInstance: () => ({
getProvider: jest.fn().mockReturnValue(undefined),
hasProvider: jest.fn().mockReturnValue(false),
onDidRegister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidUnregister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
subscribe: jest.fn().mockReturnValue(() => {}),
}),
},
}));

View File

@@ -26,13 +26,12 @@
* back to the default Ace editor.
*/
import { useState, useEffect, forwardRef } from 'react';
import { useSyncExternalStore, forwardRef } from 'react';
import type { editors } from '@apache-superset/core';
import { useTheme } from '@apache-superset/core/theme';
import EditorProviders from './EditorProviders';
import AceEditorProvider from './AceEditorProvider';
type EditorLanguage = editors.EditorLanguage;
type EditorProps = editors.EditorProps;
type EditorHandle = editors.EditorHandle;
@@ -42,49 +41,6 @@ type EditorHandle = editors.EditorHandle;
*/
export type EditorHostProps = EditorProps;
/**
* Hook to track editor provider changes.
* Returns the provider for the specified language and re-renders when it changes.
*/
const useEditorProvider = (language: EditorLanguage) => {
const manager = EditorProviders.getInstance();
const [provider, setProvider] = useState(() => manager.getProvider(language));
useEffect(() => {
// Helper to safely update provider state, always fetching latest from manager
const updateProvider = () => {
setProvider(prev => {
const current = manager.getProvider(language);
return current !== prev ? current : prev;
});
};
// Subscribe to provider changes
const registerDisposable = manager.onDidRegister(event => {
if (event.editor.languages.includes(language)) {
updateProvider();
}
});
const unregisterDisposable = manager.onDidUnregister(event => {
if (event.editor.languages.includes(language)) {
updateProvider();
}
});
// Check for provider on mount (in case it was registered before this component mounted)
updateProvider();
return () => {
registerDisposable.dispose();
unregisterDisposable.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [language, manager]);
return provider;
};
/**
* EditorHost component that dynamically resolves and renders the appropriate editor.
*
@@ -106,7 +62,12 @@ const useEditorProvider = (language: EditorLanguage) => {
const EditorHost = forwardRef<EditorHandle, EditorHostProps>((props, ref) => {
const { language } = props;
const theme = useTheme();
const provider = useEditorProvider(language);
const manager = EditorProviders.getInstance();
const provider = useSyncExternalStore(
manager.subscribe,
() => manager.getProvider(language),
() => undefined,
);
// Merge theme into props
const propsWithTheme = { ...props, theme };

View File

@@ -93,6 +93,17 @@ class EditorProviders {
*/
private unregisterEmitter = new EventEmitter<EditorUnregisteredEvent>();
private syncListeners: Set<() => void> = new Set();
/**
* Stable-reference subscribe function for useSyncExternalStore.
* Defined as an arrow property so the reference is bound to this instance at construction.
*/
public subscribe = (listener: () => void): (() => void) => {
this.syncListeners.add(listener);
return () => this.syncListeners.delete(listener);
};
// eslint-disable-next-line no-useless-constructor
private constructor() {
// Private constructor for singleton pattern
@@ -145,6 +156,7 @@ class EditorProviders {
// Fire registration event
this.registerEmitter.fire({ editor });
this.syncListeners.forEach(l => l());
// Return disposable for cleanup
return new Disposable(() => {
@@ -176,6 +188,7 @@ class EditorProviders {
// Fire unregistration event
this.unregisterEmitter.fire({ editor });
this.syncListeners.forEach(l => l());
}
/**
@@ -234,6 +247,7 @@ class EditorProviders {
public reset(): void {
this.providers.clear();
this.languageToProvider.clear();
this.syncListeners.clear();
}
}

View File

@@ -24,6 +24,7 @@
* and resolution functions declared in the API types.
*/
import { useSyncExternalStore } from 'react';
import { editors as editorsApi } from '@apache-superset/core';
import { Disposable } from '../models';
import EditorProviders from './EditorProviders';
@@ -109,6 +110,23 @@ export const onDidUnregisterEditor = (
return manager.onDidUnregister(listener);
};
/**
* Hook that returns the editor provider for a specific language and re-renders when it changes.
*
* @param language The language to get an editor for
* @returns The editor provider or undefined if no extension provides one
*/
export const useEditor = (
language: EditorLanguage,
): EditorProvider | undefined => {
const manager = EditorProviders.getInstance();
return useSyncExternalStore(
manager.subscribe,
() => manager.getProvider(language),
() => undefined,
);
};
/**
* Editors API object for use in the extension system.
*/

View File

@@ -24,11 +24,14 @@
* Extensions register menu items as side effects at import time.
*/
import { useSyncExternalStore } from 'react';
import type { menus as menusApi } from '@apache-superset/core';
import { Disposable } from '../models';
type MenuItem = menusApi.MenuItem;
type Menu = menusApi.Menu;
type MenuItemRegisteredEvent = menusApi.MenuItemRegisteredEvent;
type MenuItemUnregisteredEvent = menusApi.MenuItemUnregisteredEvent;
type StoredMenuItem = {
item: MenuItem;
@@ -38,6 +41,27 @@ type StoredMenuItem = {
const menuItems: StoredMenuItem[] = [];
const syncListeners = new Set<() => void>();
const subscribe = (listener: () => void) => {
syncListeners.add(listener);
return () => syncListeners.delete(listener);
};
const registerListeners = new Set<(e: MenuItemRegisteredEvent) => void>();
const unregisterListeners = new Set<(e: MenuItemUnregisteredEvent) => void>();
const menuCache = new Map<string, Menu | undefined>();
const notifyRegister = (event: MenuItemRegisteredEvent) => {
menuCache.clear();
syncListeners.forEach(l => l());
registerListeners.forEach(l => l(event));
};
const notifyUnregister = (event: MenuItemUnregisteredEvent) => {
menuCache.clear();
syncListeners.forEach(l => l());
unregisterListeners.forEach(l => l(event));
};
const registerMenuItem: typeof menusApi.registerMenuItem = (
item: MenuItem,
location: string,
@@ -45,11 +69,13 @@ const registerMenuItem: typeof menusApi.registerMenuItem = (
): Disposable => {
const stored: StoredMenuItem = { item, location, group };
menuItems.push(stored);
notifyRegister({ item, location, group });
return new Disposable(() => {
const index = menuItems.indexOf(stored);
if (index >= 0) {
menuItems.splice(index, 1);
}
notifyUnregister({ item, location, group });
});
};
@@ -77,7 +103,34 @@ const getMenu: typeof menusApi.getMenu = (
return result;
};
export const useMenu = (location: string): Menu | undefined =>
useSyncExternalStore(
subscribe,
() => {
if (!menuCache.has(location)) {
menuCache.set(location, getMenu(location));
}
return menuCache.get(location);
},
() => undefined,
);
export const onDidRegisterMenuItem: typeof menusApi.onDidRegisterMenuItem = (
listener: (e: MenuItemRegisteredEvent) => void,
): Disposable => {
registerListeners.add(listener);
return new Disposable(() => registerListeners.delete(listener));
};
export const onDidUnregisterMenuItem: typeof menusApi.onDidUnregisterMenuItem =
(listener: (e: MenuItemUnregisteredEvent) => void): Disposable => {
unregisterListeners.add(listener);
return new Disposable(() => unregisterListeners.delete(listener));
};
export const menus: typeof menusApi = {
registerMenuItem,
getMenu,
onDidRegisterMenuItem,
onDidUnregisterMenuItem,
};

View File

@@ -24,13 +24,15 @@
* Extensions register views as side effects at import time.
*/
import React, { ReactElement } from 'react';
import React, { ReactElement, useSyncExternalStore } from 'react';
import type { views as viewsApi } from '@apache-superset/core';
import { ErrorBoundary } from 'src/components/ErrorBoundary';
import ExtensionPlaceholder from 'src/extensions/ExtensionPlaceholder';
import { Disposable } from '../models';
type View = viewsApi.View;
type ViewRegisteredEvent = viewsApi.ViewRegisteredEvent;
type ViewUnregisteredEvent = viewsApi.ViewUnregisteredEvent;
const viewRegistry: Map<
string,
@@ -39,6 +41,27 @@ const viewRegistry: Map<
const locationIndex: Map<string, Set<string>> = new Map();
const syncListeners = new Set<() => void>();
const subscribe = (listener: () => void) => {
syncListeners.add(listener);
return () => syncListeners.delete(listener);
};
const registerListeners = new Set<(e: ViewRegisteredEvent) => void>();
const unregisterListeners = new Set<(e: ViewUnregisteredEvent) => void>();
const viewsCache = new Map<string, View[] | undefined>();
const notifyRegister = (event: ViewRegisteredEvent) => {
viewsCache.clear();
syncListeners.forEach(l => l());
registerListeners.forEach(l => l(event));
};
const notifyUnregister = (event: ViewUnregisteredEvent) => {
viewsCache.clear();
syncListeners.forEach(l => l());
unregisterListeners.forEach(l => l(event));
};
const registerView: typeof viewsApi.registerView = (
view: View,
location: string,
@@ -51,10 +74,12 @@ const registerView: typeof viewsApi.registerView = (
const ids = locationIndex.get(location) ?? new Set();
ids.add(id);
locationIndex.set(location, ids);
notifyRegister({ view, location });
return new Disposable(() => {
viewRegistry.delete(id);
locationIndex.get(location)?.delete(id);
notifyUnregister({ view, location });
});
};
@@ -77,7 +102,35 @@ const getViews: typeof viewsApi.getViews = (
.filter((c): c is View => !!c);
};
export const useViews = (location: string): View[] | undefined =>
useSyncExternalStore(
subscribe,
() => {
if (!viewsCache.has(location)) {
viewsCache.set(location, getViews(location));
}
return viewsCache.get(location);
},
() => undefined,
);
export const onDidRegisterView: typeof viewsApi.onDidRegisterView = (
listener: (e: ViewRegisteredEvent) => void,
): Disposable => {
registerListeners.add(listener);
return new Disposable(() => registerListeners.delete(listener));
};
export const onDidUnregisterView: typeof viewsApi.onDidUnregisterView = (
listener: (e: ViewUnregisteredEvent) => void,
): Disposable => {
unregisterListeners.add(listener);
return new Disposable(() => unregisterListeners.delete(listener));
};
export const views: typeof viewsApi = {
registerView,
getViews,
onDidRegisterView,
onDidUnregisterView,
};

View File

@@ -960,3 +960,92 @@ test('Clicking the gear "Add or edit filters and controls" item opens the Filter
expect(await screen.findByTestId('filter-modal')).toBeInTheDocument();
});
test('FilterBar with orientation=Horizontal routes to Horizontal layout instead of Vertical', async () => {
// Migrated from the disabled Cypress spec _skip.horizontalFilterBar.test.ts:
// proves the orientation prop selects the Horizontal subtree. The settings
// gear (FilterBarSettings) is rendered only by Horizontal.tsx — Vertical.tsx
// does not mount it — so its presence is a horizontal-exclusive positive
// signal that won't false-pass if vertical heading copy is tuned. We flush
// all pending fake timers to clear useInitialization's setTimeout
// regardless of the production timeout literal.
const filter = createFilter({
id: 'NATIVE_FILTER-h1',
name: 'Horizontal filter',
});
const dataMask = createDataMask(filter.id);
const state = createStateWithFilter(filter, dataMask, {
filterBarOrientation: FilterBarOrientation.Horizontal,
});
render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
initialState: state,
useDnd: true,
useRedux: true,
useRouter: true,
});
await act(async () => {
jest.runAllTimers();
});
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
});
test('FilterBar with orientation=Horizontal and no filters shows empty state alongside default actions', async () => {
// Covers the second half of sc-107387 task #107390 ("show all default
// actions in horizontal mode"). The original Cypress spec asserted four
// affordances render when the bar is horizontal with no filters: the
// empty-state copy, the settings gear, the action-buttons block, and the
// create-filter entry inside the gear menu. The dropdown contents are
// already covered by FilterBarSettings.test.tsx; here we keep scope to
// the layout-level affordances that are exclusive to Horizontal.tsx.
// Reload-persistence (the rest of #107390) is out of RTL scope and stays
// queued for Playwright.
const state = {
...stateWithoutNativeFilters,
dashboardInfo: {
id: 1,
dash_edit_perm: true,
metadata: {
native_filter_configuration: [],
filterBarOrientation: FilterBarOrientation.Horizontal,
},
},
dashboardState: {
...stateWithoutNativeFilters.dashboardState,
activeTabs: ['ROOT_ID'],
},
nativeFilters: { filters: {}, filtersState: {} },
};
render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
initialState: state,
useDnd: true,
useRedux: true,
useRouter: true,
});
await act(async () => {
jest.runAllTimers();
});
expect(screen.getByTestId('horizontal-filterbar-empty')).toHaveTextContent(
'No filters are currently added to this dashboard.',
);
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
expect(screen.getByTestId('filterbar-action-buttons')).toBeInTheDocument();
});
test('FilterBar with orientation=Vertical renders Vertical layout (sanity counterpart to the horizontal routing test)', () => {
// Paired control for the routing test above: with Vertical orientation,
// the settings gear must NOT be present (Vertical.tsx does not render
// FilterBarSettings). Confirms the routing signal is horizontal-exclusive,
// not a coincidence of when timers fire.
const props = createClosedBarProps();
renderFilterBar(props);
expect(screen.getByText('Filters and controls')).toBeInTheDocument();
expect(
screen.queryByRole('img', { name: 'setting' }),
).not.toBeInTheDocument();
});

View File

@@ -0,0 +1,305 @@
/**
* 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 { Preset } from '@superset-ui/core';
import type { DataMaskStateWithId } from '@superset-ui/core';
import type {
DropdownContainerProps,
DropdownItem,
} from '@superset-ui/core/components/DropdownContainer';
import { SelectFilterPlugin } from 'src/filters/components';
import { FilterBarOrientation } from 'src/dashboard/types';
import { act, render, waitFor, within } from 'spec/helpers/testing-library';
import { createSelectNativeFilter } from 'spec/fixtures/mockNativeFilters';
import FilterControls from './FilterControls';
// Capture every props snapshot DropdownContainer receives, plus the latest
// onOverflowingStateChange callback. Tests drive overflow by invoking the
// callback and then assert against the *next* captured props snapshot —
// these are the values FilterControls itself computed (dropdownTriggerCount,
// dropdownContent, items) so assertions exercise real production logic
// rather than props the test handed in directly.
const dropdownContainerProps: DropdownContainerProps[] = [];
const callbackRef: {
current:
| ((s: { overflowed: string[]; notOverflowed: string[] }) => void)
| null;
} = { current: null };
// Mock the DropdownContainer subpath rather than the barrel
// `@superset-ui/core/components` — mocking the barrel triggers a
// circular re-export chain at requireActual time
// (LabeledErrorBoundInput → ActionButton is undefined at that point).
// The barrel's `export { DropdownContainer } from './DropdownContainer'`
// resolves to this subpath, so the mock is picked up transparently.
jest.mock('@superset-ui/core/components/DropdownContainer', () => {
const React = jest.requireActual('react');
const MockDropdownContainer = React.forwardRef(
(props: DropdownContainerProps, ref: React.Ref<unknown>) => {
dropdownContainerProps.push(props);
callbackRef.current = props.onOverflowingStateChange ?? null;
React.useImperativeHandle(ref, () => ({
open: jest.fn(),
close: jest.fn(),
}));
return (
<div data-test="dropdown-container-mock">
<div data-test="dropdown-items">
{props.items.map((item: DropdownItem) => (
<div key={item.id} data-test="dropdown-item">
{item.element}
</div>
))}
</div>
<div data-test="dropdown-trigger-text">
{props.dropdownTriggerText}
</div>
<div data-test="dropdown-trigger-count">
{props.dropdownTriggerCount}
</div>
{props.dropdownContent && (
<div data-test="dropdown-content-mock">
{props.dropdownContent([])}
</div>
)}
</div>
);
},
);
return { __esModule: true, DropdownContainer: MockDropdownContainer };
});
class OverflowTestPreset extends Preset {
constructor() {
super({
name: 'FilterControls overflow test preset',
plugins: [new SelectFilterPlugin().configure({ key: 'filter_select' })],
});
}
}
new OverflowTestPreset().register();
// Tabless dashboard layout ⇒ useSelectFiltersInScope returns all filters in
// scope without needing to model tab parentage.
const buildHorizontalState = (
filters: ReturnType<typeof createSelectNativeFilter>[],
) => ({
dashboardInfo: {
id: 1,
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.Horizontal,
metadata: {
native_filter_configuration: filters,
},
},
dashboardLayout: {
present: {
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: [] },
},
past: [],
future: [],
},
dashboardState: {
sliceIds: [],
activeTabs: ['ROOT_ID'],
},
charts: {},
nativeFilters: {
filters: filters.reduce(
(acc, f) => ({ ...acc, [f.id]: f }),
{} as Record<string, ReturnType<typeof createSelectNativeFilter>>,
),
filtersState: {},
},
dataMask: {},
sliceEntities: { slices: {} },
datasources: {},
});
const buildDataMaskSelected = (
filters: ReturnType<typeof createSelectNativeFilter>[],
withValueIds: string[] = [],
): DataMaskStateWithId =>
filters.reduce(
(acc, f) => ({
...acc,
[f.id]: {
id: f.id,
filterState: {
value: withValueIds.includes(f.id) ? ['set'] : null,
},
extraFormData: {},
},
}),
{} as DataMaskStateWithId,
);
const renderHorizontal = (
filters: ReturnType<typeof createSelectNativeFilter>[],
dataMaskSelected: DataMaskStateWithId,
) =>
render(
<FilterControls
dataMaskSelected={dataMaskSelected}
onFilterSelectionChange={jest.fn()}
onPendingCustomizationDataMaskChange={jest.fn()}
chartCustomizationValues={[]}
/>,
{
useRedux: true,
useRouter: true,
initialState: buildHorizontalState(filters),
},
);
const latestProps = () =>
dropdownContainerProps[dropdownContainerProps.length - 1];
const fireOverflow = (overflowed: string[], notOverflowed: string[]) => {
if (!callbackRef.current) {
throw new Error('onOverflowingStateChange callback not captured');
}
act(() => {
callbackRef.current!({ overflowed, notOverflowed });
});
};
beforeEach(() => {
dropdownContainerProps.length = 0;
callbackRef.current = null;
});
test('horizontal FilterControls hands every filter to DropdownContainer as an item', async () => {
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
createSelectNativeFilter('NATIVE_FILTER-4', 'zip'),
];
renderHorizontal(filters, buildDataMaskSelected(filters));
await waitFor(() => expect(latestProps()).toBeTruthy());
expect(latestProps().items.map((i: DropdownItem) => i.id)).toEqual([
'NATIVE_FILTER-1',
'NATIVE_FILTER-2',
'NATIVE_FILTER-3',
'NATIVE_FILTER-4',
]);
// dropdownTriggerText is the production string FilterControls passes in.
expect(latestProps().dropdownTriggerText).toBe('More filters');
});
test('with no overflow callback fired, dropdown trigger count is 0 and content is empty', async () => {
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
];
renderHorizontal(
filters,
buildDataMaskSelected(filters, ['NATIVE_FILTER-1']),
);
await waitFor(() => expect(latestProps()).toBeTruthy());
expect(latestProps().dropdownTriggerCount).toBe(0);
// FilterControls only supplies dropdownContent when something overflowed.
expect(latestProps().dropdownContent).toBeUndefined();
});
test('firing overflow with two filters that have values increments the trigger count to 2', async () => {
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
createSelectNativeFilter('NATIVE_FILTER-4', 'zip'),
];
renderHorizontal(
filters,
// Mark the two we plan to overflow as having values; the production
// selector activeOverflowedFiltersInScope filters on dataMask.filterState.value.
buildDataMaskSelected(filters, ['NATIVE_FILTER-3', 'NATIVE_FILTER-4']),
);
await waitFor(() => expect(callbackRef.current).toBeTruthy());
fireOverflow(
['NATIVE_FILTER-3', 'NATIVE_FILTER-4'],
['NATIVE_FILTER-1', 'NATIVE_FILTER-2'],
);
await waitFor(() => {
expect(latestProps().dropdownTriggerCount).toBe(2);
});
});
test('firing overflow with no active values keeps trigger count at 0 but supplies dropdownContent', async () => {
// Reinforces the activeOverflowedFiltersInScope branch in
// FilterControls.tsx: count is the *active* (value-bearing) subset of
// overflowed filters, not the raw overflowed count. If the production
// memo regressed to use overflowedFiltersInScope.length, this fails.
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'country'),
createSelectNativeFilter('NATIVE_FILTER-2', 'region'),
createSelectNativeFilter('NATIVE_FILTER-3', 'city'),
];
renderHorizontal(filters, buildDataMaskSelected(filters));
await waitFor(() => expect(callbackRef.current).toBeTruthy());
fireOverflow(['NATIVE_FILTER-2', 'NATIVE_FILTER-3'], ['NATIVE_FILTER-1']);
await waitFor(() => {
expect(latestProps().dropdownContent).toBeInstanceOf(Function);
});
expect(latestProps().dropdownTriggerCount).toBe(0);
});
test('all 12 overflowed filters are reachable through dropdownContent', async () => {
// Substitutes for the disabled Cypress "scroll within overflow" assertion:
// jsdom has no real layout/scrolling, so we instead prove every overflowed
// filter renders inside the dropdown panel.
const filters = Array.from({ length: 12 }, (_, i) =>
createSelectNativeFilter(`NATIVE_FILTER-${i + 1}`, `filter_${i + 1}`),
);
renderHorizontal(filters, buildDataMaskSelected(filters));
await waitFor(() => expect(callbackRef.current).toBeTruthy());
fireOverflow(
filters.map(f => f.id),
[],
);
// dropdownContent renders FiltersDropdownContent, which renders each
// overflowed filter through the renderer prop. Asserting on identity
// (not just count) catches a regression that rendered the wrong subset
// of filters in the dropdown — e.g. all `filtersInScope` instead of
// the overflowed slice.
const { findByTestId } = within(document.body);
const contentSlot = await findByTestId('dropdown-content-mock');
await waitFor(() => {
const names = within(contentSlot).getAllByTestId('filter-control-name');
expect(names.map(n => n.textContent)).toEqual(filters.map(f => f.name));
});
});

View File

@@ -16,9 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
import { NativeFilterType } from '@superset-ui/core';
import { NativeFilterType, Preset } from '@superset-ui/core';
import type { Filter } from '@superset-ui/core';
import { SelectFilterPlugin } from 'src/filters/components';
import { FilterBarOrientation } from 'src/dashboard/types';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { createSelectNativeFilter } from 'spec/fixtures/mockNativeFilters';
import HorizontalBar from './Horizontal';
import type { HorizontalBarProps } from './types';
// Register the select filter plugin once so FilterControl can render the
// filter name without throwing when the plugin registry is consulted.
class HorizontalFilterBarTestPreset extends Preset {
constructor() {
super({
name: 'Horizontal filter bar test preset',
plugins: [new SelectFilterPlugin().configure({ key: 'filter_select' })],
});
}
}
new HorizontalFilterBarTestPreset().register();
const defaultProps = {
actions: null,
@@ -32,7 +49,7 @@ const defaultProps = {
onPendingCustomizationDataMaskChange: jest.fn(),
};
const renderWrapper = (overrideProps?: Record<string, any>) =>
const renderWrapper = (overrideProps?: Partial<HorizontalBarProps>) =>
waitFor(() =>
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
useRedux: true,
@@ -60,11 +77,13 @@ test('should render', async () => {
test('should not render the empty message', async () => {
await renderWrapper({
// Intentionally minimal — Horizontal only reads filterValues.length
// here, so the missing required Filter fields would never be read.
filterValues: [
{
id: 'test',
type: NativeFilterType.NativeFilter,
},
} as unknown as Filter,
],
});
expect(
@@ -92,3 +111,133 @@ test('should render the loading icon', async () => {
});
expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument();
});
// --- Tests migrated from disabled Cypress spec
// `_skip.horizontalFilterBar.test.ts` (sc-107387). ---
const buildStateWithFilters = (
filters: ReturnType<typeof createSelectNativeFilter>[],
) => ({
dashboardState: {
sliceIds: [],
activeTabs: ['ROOT_ID'],
},
dashboardInfo: {
id: 1,
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.Horizontal,
metadata: {
native_filter_configuration: filters,
},
},
dashboardLayout: {
present: {
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: [] },
},
past: [],
future: [],
},
charts: {},
nativeFilters: {
filters: filters.reduce(
(acc, f) => ({ ...acc, [f.id]: f }),
{} as Record<string, ReturnType<typeof createSelectNativeFilter>>,
),
filtersState: {},
},
dataMask: filters.reduce(
(acc, f) => ({
...acc,
[f.id]: { id: f.id, filterState: { value: null }, extraFormData: {} },
}),
{} as Record<string, unknown>,
),
sliceEntities: { slices: {} },
datasources: {},
});
const renderWithFilters = (
filters: ReturnType<typeof createSelectNativeFilter>[],
overrideProps?: Partial<HorizontalBarProps>,
) =>
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
useRedux: true,
useRouter: true,
initialState: buildStateWithFilters(filters),
});
test('renders default actions slot, settings gear, and empty message together in horizontal mode', async () => {
const sentinelActions = (
<button type="button" data-test="sentinel-actions">
apply
</button>
);
await waitFor(() =>
render(
<HorizontalBar
{...defaultProps}
actions={sentinelActions}
filterValues={[]}
/>,
{
useRedux: true,
useRouter: true,
initialState: {
dashboardState: { sliceIds: [] },
dashboardInfo: {
id: 1,
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.Horizontal,
},
dashboardLayout: { present: {}, past: [], future: [] },
},
},
),
);
expect(
screen.getByText('No filters are currently added to this dashboard.'),
).toBeInTheDocument();
expect(screen.getByRole('img', { name: 'setting' })).toBeInTheDocument();
expect(screen.getByTestId('sentinel-actions')).toBeInTheDocument();
});
test('renders all native filters supplied via filterValues in horizontal mode', async () => {
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'test_1', 'country_name'),
createSelectNativeFilter('NATIVE_FILTER-2', 'test_2', 'country_code'),
createSelectNativeFilter('NATIVE_FILTER-3', 'test_3', 'region'),
];
renderWithFilters(filters, { filterValues: filters });
await waitFor(() => {
const filterNames = screen.getAllByTestId('filter-control-name');
expect(filterNames).toHaveLength(3);
});
['test_1', 'test_2', 'test_3'].forEach(name => {
expect(screen.getByText(name)).toBeInTheDocument();
});
});
test('omits the empty message when at least one filter value is supplied', async () => {
// Companion to the "renders all native filters" test above: the migrated
// Cypress "display newly added filter" scenario reduces, at this layer, to
// proving that supplying a filter value flips off the empty state. The
// upstream user flow (open edit modal, add filter, save) is integration
// territory and not covered here.
const filters = [
createSelectNativeFilter('NATIVE_FILTER-1', 'just_added', 'country_name'),
];
renderWithFilters(filters, { filterValues: filters });
await waitFor(() => {
expect(screen.getByText('just_added')).toBeInTheDocument();
});
expect(
screen.queryByText('No filters are currently added to this dashboard.'),
).not.toBeInTheDocument();
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
// eslint-disable-next-line no-restricted-syntax
import * as supersetCore from '@apache-superset/core';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
@@ -52,20 +52,12 @@ declare global {
const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [initialized, setInitialized] = useState(false);
const userId = useSelector<RootState, number | undefined>(
({ user }) => user.userId,
);
useEffect(() => {
if (initialized) return;
if (!userId) {
// No user logged in — nothing to initialize
setInitialized(true);
return;
}
if (userId == null) return;
// Provide the implementations for @apache-superset/core
window.superset = {
@@ -80,19 +72,10 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
views,
};
const setup = async () => {
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
await ExtensionsLoader.getInstance().initializeExtensions();
}
setInitialized(true);
};
setup();
}, [initialized, userId]);
if (!initialized) {
return null;
}
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
ExtensionsLoader.getInstance().initializeExtensions();
}
}, [userId]);
return <>{children}</>;
};

View File

@@ -25,7 +25,7 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",

View File

@@ -33,7 +33,7 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",

View File

@@ -13,7 +13,7 @@
"express": "~5.2.1",
"http-errors": "~2.0.1",
"jsonwebtoken": "^9.0.3",
"morgan": "~1.10.1",
"morgan": "~1.11.0",
"pug": "~3.0.4"
}
},
@@ -127,17 +127,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -455,17 +444,6 @@
"node": ">=6.6.0"
}
},
"node_modules/express/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@@ -483,18 +461,6 @@
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -867,18 +833,22 @@
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-finished": "~2.4.1",
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/morgan/node_modules/debug": {
@@ -928,9 +898,9 @@
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
@@ -1227,18 +1197,6 @@
"node": ">= 18"
}
},
"node_modules/send/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -1509,16 +1467,6 @@
"qs": "^6.14.0",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"buffer-equal-constant-time": {
@@ -1740,14 +1688,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
@@ -1762,16 +1702,6 @@
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"forwarded": {
@@ -2040,14 +1970,14 @@
}
},
"morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-finished": "~2.4.1",
"on-headers": "~1.1.0"
},
"dependencies": {
@@ -2087,9 +2017,9 @@
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
@@ -2337,16 +2267,6 @@
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"dependencies": {
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
}
}
},
"serve-static": {

View File

@@ -11,7 +11,7 @@
"express": "~5.2.1",
"http-errors": "~2.0.1",
"jsonwebtoken": "^9.0.3",
"morgan": "~1.10.1",
"morgan": "~1.11.0",
"pug": "~3.0.4"
}
}

View File

@@ -459,6 +459,7 @@ LANGUAGES = {
"nl": {"flag": "nl", "name": "Dutch"},
"uk": {"flag": "ua", "name": "Ukrainian"},
"mi": {"flag": "nz", "name": "Māori"},
"ro": {"flag": "ro", "name": "Romanian"},
}
# Turning off i18n by default as translation in most languages are
# incomplete and not well maintained.

View File

@@ -0,0 +1,122 @@
# 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.
"""fix Security view menu case
Revision ID: b4a3f2e1d0c9
Revises: 31dae2559c05
Create Date: 2026-05-29 00:00:00.000000
On MySQL with the default case-insensitive collation, five Superset views that
declare ``class_permission_name = "security"`` (lowercase) cause FAB to look up
or insert a view-menu entry named ``"security"``. MySQL's case-insensitive
``UNIQUE`` constraint means the lookup finds an existing ``"Security"`` row and
uses it, OR a fresh install stores ``"security"`` because that view is
registered before the FAB built-in views register ``"Security"``.
Python string comparisons in ``sync_role_definitions`` and the menu-hiding
logic are case-sensitive, so an all-lowercase ``"security"`` row breaks the
Security menu for MySQL (and MariaDB) deployments.
This migration normalises the row:
* If only the lowercase ``"security"`` row exists: rename it to ``"Security"``.
* If both rows exist (SQLite / PostgreSQL fresh-install with the old code):
reassign every ``ab_permission_view`` from the lowercase row to the
correctly-cased row, then delete the duplicate.
"""
# revision identifiers, used by Alembic.
revision = "b4a3f2e1d0c9"
down_revision = "31dae2559c05"
import logging # noqa: E402
from alembic import op # noqa: E402
from sqlalchemy.orm import Session # noqa: E402
from superset.migrations.shared.security_converge import ( # noqa: E402
PermissionView,
ViewMenu,
)
logger = logging.getLogger("alembic.env")
def upgrade() -> None:
bind = op.get_bind()
session = Session(bind=bind)
# Fetch all view menus and compare in Python (SQL filter_by is
# case-insensitive on MySQL, so we cannot rely on it here).
all_vms: list[ViewMenu] = session.query(ViewMenu).all()
security_upper = next((vm for vm in all_vms if vm.name == "Security"), None)
security_lower = next((vm for vm in all_vms if vm.name == "security"), None)
if security_lower is None:
logger.info("No lowercase 'security' view-menu found; nothing to do.")
return
if security_upper is None:
# Simple rename — only the incorrect row exists.
logger.info(
"Renaming ab_view_menu 'security' -> 'Security' (id=%d)",
security_lower.id,
)
security_lower.name = "Security"
session.flush()
else:
# Both rows exist. Re-home every permission_view from the lowercase
# entry to the correctly-cased entry, then drop the duplicate.
logger.info(
"Both 'security' (id=%d) and 'Security' (id=%d) found; merging.",
security_lower.id,
security_upper.id,
)
pvms_lower: list[PermissionView] = (
session.query(PermissionView)
.filter(PermissionView.view_menu_id == security_lower.id)
.all()
)
for pvm in pvms_lower:
upper_pvm = (
session.query(PermissionView)
.filter(
PermissionView.view_menu_id == security_upper.id,
PermissionView.permission_id == pvm.permission_id,
)
.one_or_none()
)
if upper_pvm:
# Transfer role bindings from the duplicate row to the
# surviving row before discarding the duplicate, so no
# role silently loses a permission.
for role in list(pvm.role):
if upper_pvm not in role.permissions:
role.permissions.append(upper_pvm)
session.flush()
session.delete(pvm)
else:
pvm.view_menu_id = security_upper.id
session.flush()
session.delete(security_lower)
session.commit()
def downgrade() -> None:
# There is no safe way to determine the original state (whether the row was
# lowercase or whether there were two rows), so downgrade is a no-op.
pass

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@ import pandas as pd
import simplejson
from flask_babel.speaklater import LazyString
from jsonpath_ng import parse
from jsonpath_ng.jsonpath import Child, Fields, Root
from simplejson import JSONDecodeError
from superset.constants import PASSWORD_MASK
@@ -304,6 +305,30 @@ def reveal_sensitive(
return revealed_payload
def _render_jsonpath(node: Any) -> str:
"""
Render a JSONPath node as a stable dotted string (e.g. ``foo.bar``).
``str()`` of a jsonpath-ng path is not stable across releases: as of
jsonpath-ng 1.8.0 a ``Child`` node renders with surrounding parentheses
(e.g. ``(foo.bar)``). This helper produces the historic dotted notation so
that the strings returned by :func:`get_masked_fields` remain consistent and
can be round-tripped back through ``parse``.
Falls back to ``str()`` for node kinds that aren't plain field access (e.g.
array indices, slices), preserving their existing representation.
"""
if isinstance(node, Child):
left = _render_jsonpath(node.left)
right = _render_jsonpath(node.right)
return f"{left}.{right}" if left else right
if isinstance(node, Fields):
return ".".join(node.fields)
if isinstance(node, Root):
return ""
return str(node)
def get_masked_fields(
payload: dict[str, Any],
sensitive_fields: set[str],
@@ -321,8 +346,10 @@ def get_masked_fields(
for match in jsonpath_expr.find(payload):
if match.value == PASSWORD_MASK:
# Using `match.full_path` instead of json_path to account
# for wildcards
masked.append(f"$.{match.full_path}")
# for wildcards. Render the path explicitly so the output is
# stable across jsonpath-ng versions (newer releases wrap
# `Child` paths in parentheses when stringified).
masked.append(f"$.{_render_jsonpath(match.full_path)}")
return masked

View File

@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class GroupsListView(BaseSupersetView):
route_base = "/"
class_permission_name = "security"
class_permission_name = "Security"
@expose("/list_groups/")
@has_access

View File

@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class ActionLogView(BaseSupersetView):
route_base = "/"
class_permission_name = "security"
class_permission_name = "Security"
@expose("/actionlog/list")
@has_access

View File

@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class RolesListView(BaseSupersetView):
route_base = "/"
class_permission_name = "security"
class_permission_name = "Security"
@expose("/roles/")
@has_access

View File

@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class UserRegistrationsView(BaseSupersetView):
route_base = "/"
class_permission_name = "security"
class_permission_name = "Security"
@expose("/registrations/")
@has_access

View File

@@ -25,7 +25,7 @@ from .base import BaseSupersetView
class UsersListView(BaseSupersetView):
route_base = "/"
class_permission_name = "security"
class_permission_name = "Security"
@expose("/users/")
@has_access

View File

@@ -0,0 +1,81 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from superset.utils.core import split
def test_split_empty_string():
assert list(split("")) == [""]
def test_split_leading_delimiter():
assert list(split(" a")) == [
"",
"a",
]
def test_split_trailing_delimiter():
assert list(split("a ")) == [
"a",
"",
]
def test_split_only_delimiter():
assert list(split(" ")) == [
"",
"",
]
def test_split_nested_parentheses():
assert list(
split(
"a,(b,(c,d))",
delimiter=",",
)
) == [
"a",
"(b,(c,d))",
]
def test_branch_separator_found():
assert list(split("a b")) == [
"a",
"b",
]
def test_branch_separator_not_found():
assert list(split("ab")) == [
"ab",
]
def test_branch_parentheses():
assert list(split("(a b)")) == [
"(a b)",
]
def test_branch_escaped_quote():
assert list(split(r'"a\"b c" d')) == [
r'"a\"b c"',
"d",
]