You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/15 05:07:31 UTC
(superset) branch master updated: feat(embedded): Add setDataMask API method (#35754)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 69c4011a95a feat(embedded): Add setDataMask API method (#35754)
69c4011a95a is described below
commit 69c4011a95a5d2fbdbb5d360b188c313be06e17d
Author: Michael Gerber <mi...@nxt.engineering>
AuthorDate: Sat Aug 15 07:07:24 2026 +0200
feat(embedded): Add setDataMask API method (#35754)
Co-authored-by: Evan Rusackas <ev...@preset.io>
---
superset-embedded-sdk/package-lock.json | 506 +++++++++++++++++++++++++++++
superset-embedded-sdk/package.json | 1 +
superset-embedded-sdk/src/index.test.ts | 213 ++++++++++++
superset-embedded-sdk/src/index.ts | 22 ++
superset-frontend/src/embedded/api.test.ts | 155 +++++++++
superset-frontend/src/embedded/api.tsx | 57 ++++
superset-frontend/src/embedded/index.tsx | 1 +
7 files changed, 955 insertions(+)
diff --git a/superset-embedded-sdk/package-lock.json b/superset-embedded-sdk/package-lock.json
index 11c654b42b6..41ee4e641dd 100644
--- a/superset-embedded-sdk/package-lock.json
+++ b/superset-embedded-sdk/package-lock.json
@@ -19,6 +19,7 @@
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
+ "jsdom": "^26.1.0",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
@@ -26,6 +27,27 @@
"webpack-cli": "^5.1.4"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/@babel/cli": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz",
@@ -1656,6 +1678,121 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
@@ -2501,6 +2638,16 @@
"acorn": "^8.14.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -2868,6 +3015,34 @@
"node": ">= 8"
}
},
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2886,6 +3061,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2915,6 +3097,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
@@ -3316,6 +3511,60 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -3438,6 +3687,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3498,6 +3754,46 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -3977,6 +4273,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -4033,6 +4336,19 @@
"node": ">=6"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4142,6 +4458,16 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4314,6 +4640,33 @@
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/schema-utils": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
@@ -4447,6 +4800,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -4592,6 +4952,26 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4605,6 +4985,32 @@
"node": ">=8.0"
}
},
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tscw-config": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tscw-config/-/tscw-config-1.1.2.tgz",
@@ -4931,6 +5337,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/watchpack": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
@@ -4944,6 +5363,16 @@
"node": ">=10.13.0"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/webpack": {
"version": "5.105.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
@@ -5070,6 +5499,44 @@
"node": ">=10.13.0"
}
},
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5114,6 +5581,45 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
+ "node_modules/ws": {
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/superset-embedded-sdk/package.json b/superset-embedded-sdk/package.json
index 4ae91a6a9ff..c6b05960189 100644
--- a/superset-embedded-sdk/package.json
+++ b/superset-embedded-sdk/package.json
@@ -43,6 +43,7 @@
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
+ "jsdom": "^26.1.0",
"tscw-config": "^1.1.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
diff --git a/superset-embedded-sdk/src/index.test.ts b/superset-embedded-sdk/src/index.test.ts
new file mode 100644
index 00000000000..ef85128a8aa
--- /dev/null
+++ b/superset-embedded-sdk/src/index.test.ts
@@ -0,0 +1,213 @@
+/**
+ * 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.
+ */
+
+// @vitest-environment jsdom
+
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { Switchboard } from "@superset-ui/switchboard";
+import { embedDashboard } from "./index";
+
+vi.mock("@superset-ui/switchboard");
+
+function makeFakeJWT(claims: any) {
+ // not a valid jwt, but close enough for this code
+ const tokenifiedClaims = Buffer.from(JSON.stringify(claims)).toString(
+ "base64",
+ );
+ return `abc.${tokenifiedClaims}.xyz`;
+}
+
+describe("embedDashboard", () => {
+ let mountPoint: HTMLElement;
+ let mockSwitchboard: Switchboard;
+
+ beforeEach(() => {
+ mountPoint = document.createElement("div");
+ document.body.appendChild(mountPoint);
+
+ mockSwitchboard = {
+ emit: vi.fn(),
+ get: vi.fn(),
+ start: vi.fn(),
+ defineMethod: vi.fn(),
+ } as any;
+
+ // Constructor mocks must use `function`, since arrow functions cannot be
+ // invoked with `new`.
+ vi.mocked(Switchboard).mockImplementation(function () {
+ return mockSwitchboard;
+ } as any);
+
+ // Mock MessageChannel API
+ globalThis.MessageChannel = vi.fn(function (this: any) {
+ this.port1 = {};
+ this.port2 = {};
+ }) as any;
+
+ // Mock iframe load event and sandbox
+ const originalCreateElement = document.createElement.bind(document);
+ vi.spyOn(document, "createElement").mockImplementation(tagName => {
+ const element = originalCreateElement(tagName);
+ if (tagName === "iframe") {
+ // Mock sandbox DOMTokenList
+ (element as any).sandbox = {
+ add: vi.fn(),
+ };
+ // Mock contentWindow for postMessage
+ Object.defineProperty(element, "contentWindow", {
+ writable: true,
+ value: {
+ postMessage: vi.fn(),
+ },
+ });
+ setTimeout(() => {
+ element.dispatchEvent(new Event("load"));
+ }, 0);
+ }
+ return element;
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(mountPoint);
+ vi.restoreAllMocks();
+ });
+
+ test("setDataMask sends dataMask to iframe", async () => {
+ const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
+ const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
+ const testDataMask = {
+ "NATIVE_FILTER-1": {
+ filterState: {
+ value: ["value1", "value2"],
+ },
+ },
+ };
+
+ const dashboard = await embedDashboard({
+ id: "test-id",
+ supersetDomain: "https://superset.example.com",
+ mountPoint,
+ fetchGuestToken: mockFetchGuestToken,
+ });
+
+ dashboard.setDataMask(testDataMask);
+
+ expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
+ dataMask: testDataMask,
+ });
+ });
+
+ test("setDataMask sends empty dataMask", async () => {
+ const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
+ const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
+ const emptyDataMask = {};
+
+ const dashboard = await embedDashboard({
+ id: "test-id",
+ supersetDomain: "https://superset.example.com",
+ mountPoint,
+ fetchGuestToken: mockFetchGuestToken,
+ });
+
+ dashboard.setDataMask(emptyDataMask);
+
+ expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
+ dataMask: emptyDataMask,
+ });
+ });
+
+ test("setDataMask drops the change-trigger flags observeDataMask adds", async () => {
+ const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
+ const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
+ const observedMask = {
+ "NATIVE_FILTER-1": {
+ filterState: {
+ value: ["CA"],
+ },
+ },
+ crossFiltersChanged: false,
+ nativeFiltersChanged: true,
+ };
+
+ const dashboard = await embedDashboard({
+ id: "test-id",
+ supersetDomain: "https://superset.example.com",
+ mountPoint,
+ fetchGuestToken: mockFetchGuestToken,
+ });
+
+ dashboard.setDataMask(observedMask);
+
+ expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
+ dataMask: {
+ "NATIVE_FILTER-1": observedMask["NATIVE_FILTER-1"],
+ },
+ });
+ });
+
+ test("setDataMask sends complex dataMask with multiple filters", async () => {
+ const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
+ const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
+ const complexDataMask = {
+ "NATIVE_FILTER-1": {
+ filterState: {
+ value: ["CA", "NY"],
+ },
+ },
+ "NATIVE_FILTER-2": {
+ filterState: {
+ value: [2023, 2024],
+ },
+ },
+ };
+
+ const dashboard = await embedDashboard({
+ id: "test-id",
+ supersetDomain: "https://superset.example.com",
+ mountPoint,
+ fetchGuestToken: mockFetchGuestToken,
+ });
+
+ dashboard.setDataMask(complexDataMask);
+
+ expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
+ dataMask: complexDataMask,
+ });
+ });
+
+ test("setDataMask rejects when the embedded page does not support it", async () => {
+ const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
+ const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
+ vi.mocked(mockSwitchboard.get).mockRejectedValue(
+ new Error('Method "setDataMask" is not defined'),
+ );
+
+ const dashboard = await embedDashboard({
+ id: "test-id",
+ supersetDomain: "https://superset.example.com",
+ mountPoint,
+ fetchGuestToken: mockFetchGuestToken,
+ });
+
+ await expect(dashboard.setDataMask({})).rejects.toThrow(
+ 'Method "setDataMask" is not defined',
+ );
+ });
+});
diff --git a/superset-embedded-sdk/src/index.ts b/superset-embedded-sdk/src/index.ts
index 07aeb500b13..7c9cf9d4ec2 100644
--- a/superset-embedded-sdk/src/index.ts
+++ b/superset-embedded-sdk/src/index.ts
@@ -115,6 +115,12 @@ export type EmbeddedDashboard = {
getActiveTabs: () => Promise<string[]>;
observeDataMask: (callbackFn: ObserveDataMaskCallbackFn) => void;
getDataMask: () => Promise<Record<string, any>>;
+ /**
+ * Applies a data mask to the dashboard.
+ * Rejects if the embedded Superset page does not support `setDataMask`,
+ * so a version mismatch surfaces instead of silently doing nothing.
+ */
+ setDataMask: (dataMask: Record<string, any>) => Promise<void>;
getChartStates: () => Promise<Record<string, any>>;
getChartDataPayloads: (params?: {
chartId?: number;
@@ -355,6 +361,21 @@ export async function embedDashboard({
ourPort.get<string>("getDashboardPermalink", { anchor });
const getActiveTabs = () => ourPort.get<string[]>("getActiveTabs");
const getDataMask = () => ourPort.get<Record<string, any>>("getDataMask");
+ // `observeDataMask` hands the host a mask with the change-trigger booleans
+ // mixed in, so feeding that payload straight back into `setDataMask` is a
+ // natural thing for a host to do. Keep only the entries that look like a
+ // filter's mask, so those flags never reach the dashboard as filter ids.
+ // Sent with `get` rather than `emit` so the iframe acknowledges the call:
+ // an embedded page that predates `setDataMask` replies with an error instead
+ // of dropping the message silently.
+ const setDataMask = (dataMask: Record<string, any>) =>
+ ourPort.get<void>("setDataMask", {
+ dataMask: Object.fromEntries(
+ Object.entries(dataMask).filter(
+ ([, mask]) => typeof mask === "object" && mask !== null,
+ ),
+ ),
+ });
const getChartStates = () =>
ourPort.get<Record<string, any>>("getChartStates");
const getChartDataPayloads = (params?: { chartId?: number }) =>
@@ -396,6 +417,7 @@ export async function embedDashboard({
getActiveTabs,
observeDataMask,
getDataMask,
+ setDataMask,
getChartStates,
getChartDataPayloads,
setThemeConfig,
diff --git a/superset-frontend/src/embedded/api.test.ts b/superset-frontend/src/embedded/api.test.ts
new file mode 100644
index 00000000000..e85a3b48185
--- /dev/null
+++ b/superset-frontend/src/embedded/api.test.ts
@@ -0,0 +1,155 @@
+/**
+ * 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 { DataMaskStateWithId } from '@superset-ui/core';
+
+// Mock factories must build their own jest.fn()s: jest.mock calls are hoisted
+// above this file's declarations, so a factory closing over a const would read
+// it before initialization.
+jest.mock('@apache-superset/core/utils', () => ({
+ logging: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
+}));
+
+jest.mock('../views/store', () => ({
+ store: { dispatch: jest.fn(), getState: jest.fn(), subscribe: jest.fn() },
+}));
+
+// eslint-disable-next-line import/first
+import { embeddedApi } from './api';
+// eslint-disable-next-line import/first
+import { updateDataMask } from '../dataMask/actions';
+
+const { logging: mockLogging } = jest.requireMock(
+ '@apache-superset/core/utils',
+);
+const { store: mockStore } = jest.requireMock('../views/store');
+const mockDispatch = mockStore.dispatch;
+const mockGetState = mockStore.getState;
+
+const nativeFilterMask = { filterState: { value: ['CA'] } };
+const crossFilterMask = { filterState: { value: [2024] } };
+
+// `dashboardInfo.id` is only set once HYDRATE_DASHBOARD lands, so it doubles as
+// the "dashboard is hydrated" signal setDataMask waits for.
+function stateWithFilters(filterIds: string[]) {
+ return {
+ dashboardInfo: { id: 1 },
+ dataMask: Object.fromEntries(filterIds.map(id => [id, { id }])),
+ };
+}
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+test('setDataMask dispatches an update for each known filter', () => {
+ mockGetState.mockReturnValue(
+ stateWithFilters(['NATIVE_FILTER-1', 'NATIVE_FILTER-2']),
+ );
+
+ embeddedApi.setDataMask({
+ dataMask: {
+ 'NATIVE_FILTER-1': nativeFilterMask,
+ 'NATIVE_FILTER-2': crossFilterMask,
+ } as unknown as DataMaskStateWithId,
+ });
+
+ expect(mockDispatch).toHaveBeenCalledTimes(2);
+ expect(mockDispatch).toHaveBeenCalledWith(
+ updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
+ );
+ expect(mockDispatch).toHaveBeenCalledWith(
+ updateDataMask('NATIVE_FILTER-2', crossFilterMask),
+ );
+ expect(mockLogging.warn).not.toHaveBeenCalled();
+});
+
+test('setDataMask ignores filter ids the dashboard does not know', () => {
+ mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
+
+ embeddedApi.setDataMask({
+ dataMask: {
+ 'NATIVE_FILTER-1': nativeFilterMask,
+ 'NATIVE_FILTER-from-another-dashboard': crossFilterMask,
+ } as unknown as DataMaskStateWithId,
+ });
+
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(
+ updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
+ );
+ expect(mockLogging.warn).toHaveBeenCalledWith(
+ expect.stringContaining('unknown filter ids'),
+ 'NATIVE_FILTER-from-another-dashboard',
+ );
+});
+
+test('setDataMask ignores the change-trigger flags observeDataMask emits', () => {
+ mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
+
+ embeddedApi.setDataMask({
+ dataMask: {
+ 'NATIVE_FILTER-1': nativeFilterMask,
+ crossFiltersChanged: false,
+ nativeFiltersChanged: true,
+ } as unknown as DataMaskStateWithId,
+ });
+
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(
+ updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
+ );
+});
+
+test('setDataMask dispatches nothing when no filter id is known', () => {
+ mockGetState.mockReturnValue(stateWithFilters([]));
+
+ embeddedApi.setDataMask({
+ dataMask: {
+ 'NATIVE_FILTER-1': nativeFilterMask,
+ } as unknown as DataMaskStateWithId,
+ });
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ expect(mockLogging.warn).toHaveBeenCalled();
+});
+
+test('setDataMask queues the mask until the dashboard hydrates', () => {
+ let notifyStoreSubscribers = () => {};
+ mockStore.subscribe.mockImplementation((listener: () => void) => {
+ notifyStoreSubscribers = listener;
+ return jest.fn();
+ });
+ mockGetState.mockReturnValue({ dataMask: {} });
+
+ embeddedApi.setDataMask({
+ dataMask: {
+ 'NATIVE_FILTER-1': nativeFilterMask,
+ } as unknown as DataMaskStateWithId,
+ });
+
+ expect(mockDispatch).not.toHaveBeenCalled();
+ expect(mockLogging.warn).not.toHaveBeenCalled();
+
+ mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
+ notifyStoreSubscribers();
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
+ );
+});
diff --git a/superset-frontend/src/embedded/api.tsx b/superset-frontend/src/embedded/api.tsx
index 8c0fb711ccc..284d2fdcdfd 100644
--- a/superset-frontend/src/embedded/api.tsx
+++ b/superset-frontend/src/embedded/api.tsx
@@ -17,12 +17,15 @@
* under the License.
*/
import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
+import { logging } from '@apache-superset/core/utils';
import getBootstrapData from 'src/utils/getBootstrapData';
+import { batch } from 'react-redux';
import { store } from '../views/store';
import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils';
import { DashboardChartStates } from '../dashboard/types/chartState';
import { hasStatefulCharts } from '../dashboard/util/chartStateConverter';
import { getChartDataPayloads as getChartDataPayloadsUtil } from './utils';
+import { updateDataMask } from '../dataMask/actions';
const bootstrapData = getBootstrapData();
@@ -40,6 +43,7 @@ type EmbeddedSupersetApi = {
getChartDataPayloads: (params?: {
chartId?: number;
}) => Promise<Record<string, JsonObject>>;
+ setDataMask: ({ dataMask }: { dataMask: DataMaskStateWithId }) => void;
};
const getScrollSize = (): Size => ({
@@ -83,6 +87,58 @@ const getActiveTabs = () => store?.getState()?.dashboardState?.activeTabs || [];
const getDataMask = () => store?.getState()?.dataMask || {};
+const isDashboardHydrated = () => Boolean(store?.getState()?.dashboardInfo?.id);
+
+const applyDataMask = (dataMask: DataMaskStateWithId) => {
+ // The dashboard's own data mask holds an entry for every native filter and
+ // every cross-filter-capable chart, so it doubles as the set of filter ids
+ // this dashboard can accept. Anything else — a filter id from a different
+ // dashboard, or the change-trigger flags that `observeDataMask` emits
+ // alongside the mask — would otherwise be inserted as a bogus filter and
+ // treated as a globally scoped filter by the active-filter derivation.
+ const knownFilterIds = new Set(Object.keys(getDataMask()));
+ const entries = Object.entries(dataMask);
+ const applicable = entries.filter(([id]) => knownFilterIds.has(id));
+ const ignored = entries.filter(([id]) => !knownFilterIds.has(id));
+
+ if (ignored.length) {
+ logging.warn(
+ '[superset] setDataMask ignored unknown filter ids:',
+ ignored.map(([id]) => id).join(', '),
+ );
+ }
+
+ batch(() => {
+ applicable.forEach(([filterId, mask]) => {
+ store?.dispatch(updateDataMask(filterId, mask));
+ });
+ });
+};
+
+// A mask requested before the dashboard hydrates cannot be applied yet: the
+// store holds no filter entries to validate the ids against, and hydration
+// would replace anything dispatched in the meantime. Hold the request and
+// replay it once hydration lands.
+let queuedDataMask: DataMaskStateWithId | undefined;
+let unsubscribeFromHydration: (() => void) | undefined;
+
+const setDataMask = ({ dataMask }: { dataMask: DataMaskStateWithId }) => {
+ if (isDashboardHydrated()) {
+ applyDataMask(dataMask);
+ return;
+ }
+
+ queuedDataMask = { ...queuedDataMask, ...dataMask };
+ unsubscribeFromHydration ??= store?.subscribe(() => {
+ if (!isDashboardHydrated()) return;
+ unsubscribeFromHydration?.();
+ unsubscribeFromHydration = undefined;
+ const pending = queuedDataMask;
+ queuedDataMask = undefined;
+ if (pending) applyDataMask(pending);
+ });
+};
+
const getChartStates = () =>
store?.getState()?.dashboardState?.chartStates || {};
@@ -102,4 +158,5 @@ export const embeddedApi: EmbeddedSupersetApi = {
getDataMask,
getChartStates,
getChartDataPayloads,
+ setDataMask,
};
diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx
index cbd2c281066..7d452f82946 100644
--- a/superset-frontend/src/embedded/index.tsx
+++ b/superset-frontend/src/embedded/index.tsx
@@ -298,6 +298,7 @@ window.addEventListener('message', function embeddedPageInitializer(event) {
Switchboard.defineMethod('getActiveTabs', embeddedApi.getActiveTabs);
Switchboard.defineMethod('getDataMask', embeddedApi.getDataMask);
Switchboard.defineMethod('getChartStates', embeddedApi.getChartStates);
+ Switchboard.defineMethod('setDataMask', embeddedApi.setDataMask);
Switchboard.defineMethod(
'getChartDataPayloads',
embeddedApi.getChartDataPayloads,