mirror of
https://github.com/apache/superset.git
synced 2026-07-18 12:45:44 +00:00
Compare commits
4 Commits
msyavuz/ch
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff88f0dcdd | ||
|
|
b83e0a8400 | ||
|
|
bd544b8bcb | ||
|
|
79e09ec1b3 |
20
UPDATING.md
20
UPDATING.md
@@ -24,6 +24,26 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Removed deck.gl JavaScript tooltip/data-mutator controls and ENABLE_JAVASCRIPT_CONTROLS
|
||||
|
||||
The `ENABLE_JAVASCRIPT_CONTROLS` feature flag and the deck.gl chart controls it gated
|
||||
(`js_tooltip`, `js_onclick_href`, `js_data_mutator`, and the GeoJSON layer's label/icon
|
||||
JavaScript-mode generators) have been removed. These controls let users write arbitrary
|
||||
JavaScript, sandboxed via Node's `vm` module, to customize deck.gl tooltips, click
|
||||
behavior, and data transforms; the flag defaulted off and the feature saw negligible use.
|
||||
|
||||
The deck.gl "Extra data for JS" control (`js_columns`) has also been removed. It only
|
||||
ever existed to feed extra columns into the JavaScript controls above; deck.gl's
|
||||
built-in field-based tooltips and cross-filtering already pull in any columns they need
|
||||
via `tooltip_contents`/`cross_filter_column`, so this control had no remaining purpose.
|
||||
Any chart layer whose "Advanced" control panel section only contained this control no
|
||||
longer has an "Advanced" section.
|
||||
|
||||
Any saved charts with these fields set will simply ignore them going forward and fall back
|
||||
to deck.gl's built-in field-based tooltips (`tooltip_contents`/`tooltip_template`) and
|
||||
native click/cross-filter behavior. No migration is required; the fields are dropped
|
||||
silently on next save.
|
||||
|
||||
### Owners, dashboard roles, and RLS roles replaced by Subjects
|
||||
|
||||
Superset now uses subject-based access assignments for dashboards, charts, datasets,
|
||||
|
||||
6
docs/static/feature-flags.json
vendored
6
docs/static/feature-flags.json
vendored
@@ -405,12 +405,6 @@
|
||||
"default": true,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Enable drill-to-detail functionality in charts"
|
||||
},
|
||||
{
|
||||
"name": "ENABLE_JAVASCRIPT_CONTROLS",
|
||||
"default": false,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Allow JavaScript in chart controls. WARNING: XSS security vulnerability!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
},
|
||||
fallback: {
|
||||
tty: false,
|
||||
vm: require.resolve('vm-browserify')
|
||||
vm: false,
|
||||
}
|
||||
},
|
||||
plugins: [...config.plugins, ...filteredPlugins],
|
||||
|
||||
255
superset-frontend/package-lock.json
generated
255
superset-frontend/package-lock.json
generated
@@ -282,7 +282,6 @@
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.10",
|
||||
"webpack": "^5.108.4",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
@@ -8569,9 +8568,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8589,9 +8585,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8609,9 +8602,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8629,9 +8619,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8649,9 +8636,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8669,9 +8653,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8689,9 +8670,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8709,9 +8687,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12646,13 +12621,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/underscore": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz",
|
||||
"integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
@@ -15534,6 +15502,7 @@
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -16512,6 +16481,7 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
@@ -16526,6 +16496,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -16805,6 +16776,7 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
@@ -21394,6 +21366,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -22487,6 +22460,7 @@
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
@@ -23954,6 +23928,7 @@
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
@@ -24370,6 +24345,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -26888,6 +26864,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
@@ -30242,6 +30233,7 @@
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
|
||||
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -30370,7 +30362,6 @@
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -31870,6 +31861,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
@@ -32794,6 +32786,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -36603,6 +36596,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -38561,6 +38555,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -38598,6 +38593,7 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string.prototype.trim": {
|
||||
@@ -38674,6 +38670,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -40583,38 +40580,115 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema": {
|
||||
"version": "0.67.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.67.1.tgz",
|
||||
"integrity": "sha512-vKTZB/RoYTIBdVP7E7vrgHMCssBuhja91wQy498QIVhvfRimaOgjc98uwAXmZ7mbLUytJmOSbF11wPz+ByQeXg==",
|
||||
"version": "0.68.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.68.0.tgz",
|
||||
"integrity": "sha512-1X60xXxkZpLAr125Jt5MT9VJTTxT0xfafbjdcbtml26nvpUr2p/P4V34JEXa+ObrHc71W9ksH5pemstY6y+SEw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/node": "^18.11.9",
|
||||
"glob": "^7.1.7",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^24.10.2",
|
||||
"glob": "^13.0.6",
|
||||
"path-equal": "^1.2.5",
|
||||
"safe-stable-stringify": "^2.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~5.5.0",
|
||||
"vm2": "^3.10.0",
|
||||
"yargs": "^17.1.1"
|
||||
"safe-stable-stringify": "^2.5.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "~5.9.3",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"typescript-json-schema": "bin/typescript-json-schema"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -40625,29 +40699,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript-json-schema/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/typewise": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz",
|
||||
@@ -40724,6 +40780,7 @@
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
@@ -41460,41 +41517,6 @@
|
||||
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vm-browserify": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vm2": {
|
||||
"version": "3.11.5",
|
||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.11.5.tgz",
|
||||
"integrity": "sha512-RSrkBiwrj6FRU+QdqNs6KG0XdlvJCjpQ4GXiqmMbrhmwfu5k/XIMpAer0L8f6iuf0uJ3a4T1xJN126Q8yf0VIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-walk": "^8.3.4"
|
||||
},
|
||||
"bin": {
|
||||
"vm2": "bin/vm2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vm2/node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
@@ -42410,6 +42432,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
@@ -42603,6 +42640,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
@@ -42860,6 +42898,7 @@
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -44349,14 +44388,12 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -367,7 +367,6 @@
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.10",
|
||||
"webpack": "^5.108.4",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
@@ -425,6 +424,7 @@
|
||||
"jest-util": "^30.4.0",
|
||||
"jest-circus": "^30.4.0",
|
||||
"jest-environment-node": "^30.4.0",
|
||||
"typescript-json-schema": "^0.68.0",
|
||||
"@babel/eslint-parser": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
|
||||
@@ -47,8 +47,6 @@ export enum FeatureFlag {
|
||||
EnableAdvancedDataTypes = 'ENABLE_ADVANCED_DATA_TYPES',
|
||||
EnableExtensions = 'ENABLE_EXTENSIONS',
|
||||
EnableViewers = 'ENABLE_VIEWERS',
|
||||
/** @deprecated */
|
||||
EnableJavascriptControls = 'ENABLE_JAVASCRIPT_CONTROLS',
|
||||
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
|
||||
EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML',
|
||||
EstimateQueryCost = 'ESTIMATE_QUERY_COST',
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
"xss": "^1.0.15",
|
||||
"lodash-es": "^4.18.1"
|
||||
@@ -59,7 +58,6 @@
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -40,7 +40,6 @@ import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { getMapboxApiKey } from './utils/mapbox';
|
||||
import sandboxedEval from './utils/sandbox';
|
||||
import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
DeckGLContainerHandle,
|
||||
@@ -173,12 +172,6 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
// Add colors from categories or fixed color
|
||||
features = addColor(features, fd, selectedColorScheme);
|
||||
|
||||
// Apply user defined data mutator if defined
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
features = jsFnMutator(features);
|
||||
}
|
||||
|
||||
// Show only categories selected in the legend
|
||||
if (fd.dimension) {
|
||||
features = features.filter(d => categories[d.cat_color]?.enabled);
|
||||
|
||||
@@ -32,19 +32,12 @@ export interface DeckArcFormData extends SqlaFormData {
|
||||
start_spatial: SpatialFormData['spatial'];
|
||||
end_spatial: SpatialFormData['spatial'];
|
||||
dimension?: string;
|
||||
js_columns?: string[];
|
||||
tooltip_contents?: unknown[];
|
||||
tooltip_template?: string;
|
||||
}
|
||||
|
||||
export default function buildQuery(formData: DeckArcFormData) {
|
||||
const {
|
||||
start_spatial,
|
||||
end_spatial,
|
||||
dimension,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
const { start_spatial, end_spatial, dimension, tooltip_contents } = formData;
|
||||
|
||||
if (!start_spatial || !end_spatial) {
|
||||
throw new Error(
|
||||
@@ -66,13 +59,6 @@ export default function buildQuery(formData: DeckArcFormData) {
|
||||
columns = [...columns, dimension];
|
||||
}
|
||||
|
||||
const jsColumns = ensureIsArray(js_columns || []);
|
||||
jsColumns.forEach(col => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
let filters = addSpatialNullFilters(
|
||||
|
||||
@@ -31,10 +31,6 @@ import {
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
viewport,
|
||||
@@ -166,15 +162,6 @@ const config: ControlPanelConfig = {
|
||||
[legendFormat],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -126,10 +126,6 @@ export const ArcChartViz = ({
|
||||
stroke_width: strokeWidth,
|
||||
legend_position: 'tr',
|
||||
legend_format: null,
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
processSpatialData,
|
||||
addJsColumnsToExtraProps,
|
||||
DataRecord,
|
||||
} from '../spatialUtils';
|
||||
import { processSpatialData, DataRecord } from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
@@ -43,7 +39,6 @@ function processArcData(
|
||||
startSpatial: DeckArcFormData['start_spatial'],
|
||||
endSpatial: DeckArcFormData['end_spatial'],
|
||||
dimension?: string,
|
||||
jsColumns?: string[],
|
||||
): ArcPoint[] {
|
||||
if (!startSpatial || !endSpatial || !records.length) {
|
||||
return [];
|
||||
@@ -52,9 +47,7 @@ function processArcData(
|
||||
const startFeatures = processSpatialData(records, startSpatial);
|
||||
const endFeatures = processSpatialData(records, endSpatial);
|
||||
const excludeKeys = new Set(
|
||||
['__timestamp', dimension, ...(jsColumns || [])].filter(
|
||||
(key): key is string => key != null,
|
||||
),
|
||||
['__timestamp', dimension].filter((key): key is string => key != null),
|
||||
);
|
||||
|
||||
return records
|
||||
@@ -72,8 +65,6 @@ function processArcData(
|
||||
extraProps: {},
|
||||
};
|
||||
|
||||
arcPoint = addJsColumnsToExtraProps(arcPoint, record, jsColumns);
|
||||
|
||||
if (dimension && record[dimension] != null) {
|
||||
arcPoint.cat_color = String(record[dimension]);
|
||||
}
|
||||
@@ -92,8 +83,7 @@ function processArcData(
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { rawFormData: formData } = chartProps;
|
||||
const { start_spatial, end_spatial, dimension, js_columns } =
|
||||
formData as DeckArcFormData;
|
||||
const { start_spatial, end_spatial, dimension } = formData as DeckArcFormData;
|
||||
|
||||
const records = getRecordsFromQuery(chartProps.queriesData);
|
||||
const features = processArcData(
|
||||
@@ -101,7 +91,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
start_spatial,
|
||||
end_spatial,
|
||||
dimension,
|
||||
js_columns,
|
||||
);
|
||||
|
||||
return createBaseTransformResult(chartProps, features);
|
||||
|
||||
@@ -21,7 +21,6 @@ import { PolygonLayer } from '@deck.gl/layers';
|
||||
import { Position } from '@deck.gl/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
@@ -69,13 +68,8 @@ export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const {
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
contours: rawContours,
|
||||
cellSize = '200',
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
const { aggregation = 'SUM', contours: rawContours, cellSize = '200' } = fd;
|
||||
const data = payload.data.features;
|
||||
|
||||
// Store original data for tooltip access
|
||||
const originalDataMap = new Map();
|
||||
@@ -113,12 +107,6 @@ export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
},
|
||||
);
|
||||
|
||||
if (jsFnMutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
|
||||
// Create wrapper for tooltip content that adds nearby points
|
||||
const tooltipContentGenerator = (o: any) => {
|
||||
// Find nearby points based on hover coordinate
|
||||
|
||||
@@ -25,10 +25,6 @@ import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
autozoom,
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
@@ -114,15 +110,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -27,9 +27,7 @@ import { render } from '@testing-library/react';
|
||||
import { SqlaFormData } from '@superset-ui/core';
|
||||
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
|
||||
import DeckGLGeoJson, {
|
||||
computeGeoJsonTextOptionsFromJsOutput,
|
||||
computeGeoJsonTextOptionsFromFormData,
|
||||
computeGeoJsonIconOptionsFromJsOutput,
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
getPoints,
|
||||
getLayer,
|
||||
@@ -61,26 +59,6 @@ jest.mock('react-map-gl/maplibre', () => ({
|
||||
useControl: () => null,
|
||||
}));
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => {
|
||||
const input = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
invalidOption: true,
|
||||
};
|
||||
const expectedOutput = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
};
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
label_property_name: 'name',
|
||||
@@ -105,28 +83,6 @@ test('computeGeoJsonTextOptionsFromFormData computes text options based on form
|
||||
expect(actualOutput.getText(sampleFeature)).toBe('Test');
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => {
|
||||
const input = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
invalidOption: false,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
};
|
||||
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from '../../DeckGLContainer';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
@@ -146,52 +145,6 @@ const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
||||
};
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
export const computeGeoJsonTextOptionsFromJsOutput = (
|
||||
output: unknown,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
if (!isObject(output)) return {};
|
||||
|
||||
// Properties sourced from:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
|
||||
const options: (keyof GeoJsonLayerProps)[] = [
|
||||
'getText',
|
||||
'getTextColor',
|
||||
'getTextAngle',
|
||||
'getTextSize',
|
||||
'getTextAnchor',
|
||||
'getTextAlignmentBaseline',
|
||||
'getTextPixelOffset',
|
||||
'getTextBackgroundColor',
|
||||
'getTextBorderColor',
|
||||
'getTextBorderWidth',
|
||||
'textSizeUnits',
|
||||
'textSizeScale',
|
||||
'textSizeMinPixels',
|
||||
'textSizeMaxPixels',
|
||||
'textCharacterSet',
|
||||
'textFontFamily',
|
||||
'textFontWeight',
|
||||
'textLineHeight',
|
||||
'textMaxWidth',
|
||||
'textWordBreak',
|
||||
'textBackground',
|
||||
'textBackgroundPadding',
|
||||
'textOutlineColor',
|
||||
'textOutlineWidth',
|
||||
'textBillboard',
|
||||
'textFontSettings',
|
||||
];
|
||||
|
||||
const allEntries = Object.entries(output);
|
||||
const validEntries = allEntries.filter(([k]) =>
|
||||
options.includes(k as keyof GeoJsonLayerProps),
|
||||
);
|
||||
return Object.fromEntries(validEntries);
|
||||
};
|
||||
|
||||
export const computeGeoJsonTextOptionsFromFormData = (
|
||||
fd: SqlaFormData,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
@@ -205,36 +158,6 @@ export const computeGeoJsonTextOptionsFromFormData = (
|
||||
};
|
||||
};
|
||||
|
||||
export const computeGeoJsonIconOptionsFromJsOutput = (
|
||||
output: unknown,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
if (!isObject(output)) return {};
|
||||
|
||||
// Properties sourced from:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
|
||||
const options: (keyof GeoJsonLayerProps)[] = [
|
||||
'getIcon',
|
||||
'getIconSize',
|
||||
'getIconColor',
|
||||
'getIconAngle',
|
||||
'getIconPixelOffset',
|
||||
'iconSizeUnits',
|
||||
'iconSizeScale',
|
||||
'iconSizeMinPixels',
|
||||
'iconSizeMaxPixels',
|
||||
'iconAtlas',
|
||||
'iconMapping',
|
||||
'iconBillboard',
|
||||
'iconAlphaCutoff',
|
||||
];
|
||||
|
||||
const allEntries = Object.entries(output);
|
||||
const validEntries = allEntries.filter(([k]) =>
|
||||
options.includes(k as keyof GeoJsonLayerProps),
|
||||
);
|
||||
return Object.fromEntries(validEntries);
|
||||
};
|
||||
|
||||
export const computeGeoJsonIconOptionsFromFormData = (
|
||||
fd: SqlaFormData,
|
||||
): Partial<GeoJsonLayerProps> => ({
|
||||
@@ -288,13 +211,6 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
features = [];
|
||||
recurseGeoJson(payload.data, propOverrides);
|
||||
|
||||
let processedFeatures = features;
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
processedFeatures = jsFnMutator(features) as ProcessedFeature[];
|
||||
}
|
||||
|
||||
let pointType = 'circle';
|
||||
if (fd.enable_labels) {
|
||||
pointType = `${pointType}+text`;
|
||||
@@ -303,33 +219,17 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
pointType = `${pointType}+icon`;
|
||||
}
|
||||
|
||||
let labelOpts: Partial<GeoJsonLayerProps> = {};
|
||||
if (fd.enable_labels) {
|
||||
if (fd.enable_label_javascript_mode) {
|
||||
const generator = sandboxedEval(fd.label_javascript_config_generator);
|
||||
if (typeof generator === 'function') {
|
||||
labelOpts = computeGeoJsonTextOptionsFromJsOutput(generator());
|
||||
}
|
||||
} else {
|
||||
labelOpts = computeGeoJsonTextOptionsFromFormData(fd);
|
||||
}
|
||||
}
|
||||
const labelOpts: Partial<GeoJsonLayerProps> = fd.enable_labels
|
||||
? computeGeoJsonTextOptionsFromFormData(fd)
|
||||
: {};
|
||||
|
||||
let iconOpts: Partial<GeoJsonLayerProps> = {};
|
||||
if (fd.enable_icons) {
|
||||
if (fd.enable_icon_javascript_mode) {
|
||||
const generator = sandboxedEval(fd.icon_javascript_config_generator);
|
||||
if (typeof generator === 'function') {
|
||||
iconOpts = computeGeoJsonIconOptionsFromJsOutput(generator());
|
||||
}
|
||||
} else {
|
||||
iconOpts = computeGeoJsonIconOptionsFromFormData(fd);
|
||||
}
|
||||
}
|
||||
const iconOpts: Partial<GeoJsonLayerProps> = fd.enable_icons
|
||||
? computeGeoJsonIconOptionsFromFormData(fd)
|
||||
: {};
|
||||
|
||||
return new GeoJsonLayer({
|
||||
id: `geojson-layer-${fd.slice_id}` as const,
|
||||
data: processedFeatures,
|
||||
data: features,
|
||||
extruded: fd.extruded,
|
||||
filled: fd.filled,
|
||||
stroked: fd.stroked,
|
||||
|
||||
@@ -24,15 +24,11 @@ import {
|
||||
QueryObjectFilterClause,
|
||||
SqlaFormData,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
addJsColumnsToColumns,
|
||||
addTooltipColumnsToQuery,
|
||||
} from '../buildQueryUtils';
|
||||
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
|
||||
|
||||
export interface DeckGeoJsonFormData extends SqlaFormData {
|
||||
geojson?: string;
|
||||
filter_nulls?: boolean;
|
||||
js_columns?: string[];
|
||||
cross_filter_column?: string | null;
|
||||
tooltip_contents?: unknown[];
|
||||
}
|
||||
@@ -41,7 +37,6 @@ export default function buildQuery(formData: DeckGeoJsonFormData) {
|
||||
const {
|
||||
geojson,
|
||||
filter_nulls = true,
|
||||
js_columns,
|
||||
cross_filter_column,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
@@ -56,13 +51,6 @@ export default function buildQuery(formData: DeckGeoJsonFormData) {
|
||||
geojson,
|
||||
];
|
||||
|
||||
// Add js_columns
|
||||
const columnStrings = columns.map(col =>
|
||||
typeof col === 'string' ? col : col.label || col.sqlExpression || '',
|
||||
);
|
||||
const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
|
||||
columns = withJsColumns as QueryFormColumn[];
|
||||
|
||||
if (cross_filter_column && !columns.includes(cross_filter_column)) {
|
||||
columns.push(cross_filter_column);
|
||||
}
|
||||
|
||||
@@ -20,18 +20,12 @@ import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
legacyValidateInteger,
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
validateNumber,
|
||||
validateInteger,
|
||||
} from '@superset-ui/core';
|
||||
import { formatSelectOptions } from '../../utilities/utils';
|
||||
import {
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
fillColorPicker,
|
||||
strokeColorPicker,
|
||||
filled,
|
||||
@@ -45,29 +39,11 @@ import {
|
||||
lineWidth,
|
||||
tooltipContents,
|
||||
tooltipTemplate,
|
||||
jsFunctionControl,
|
||||
crossFilterColumn,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import { dndGeojsonColumn } from '../../utilities/sharedDndControls';
|
||||
import { BLACK_COLOR } from '../../utilities/controls';
|
||||
|
||||
const defaultLabelConfigGenerator = `() => ({
|
||||
// Check the documentation at:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
|
||||
getText: f => f.properties.name,
|
||||
getTextColor: [0, 0, 0, 255],
|
||||
getTextSize: 24,
|
||||
textSizeUnits: 'pixels',
|
||||
})`;
|
||||
|
||||
const defaultIconConfigGenerator = `() => ({
|
||||
// Check the documentation at:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
|
||||
getIcon: () => ({ url: '', height: 128, width: 128 }),
|
||||
getIconSize: 32,
|
||||
iconSizeUnits: 'pixels',
|
||||
})`;
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
@@ -110,24 +86,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'enable_label_javascript_mode',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable label JavaScript mode'),
|
||||
description: t(
|
||||
'Enables custom label configuration via JavaScript',
|
||||
),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'label_property_name',
|
||||
@@ -135,10 +93,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'TextControl',
|
||||
label: t('Label property name'),
|
||||
description: t('The feature property to use for point labels'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
(!form_data.enable_label_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_labels,
|
||||
default: 'name',
|
||||
renderTrigger: true,
|
||||
resetOnHide: false,
|
||||
@@ -152,10 +107,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'ColorPickerControl',
|
||||
label: t('Label color'),
|
||||
description: t('The color of the point labels'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
(!form_data.enable_label_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_labels,
|
||||
default: BLACK_COLOR,
|
||||
renderTrigger: true,
|
||||
resetOnHide: false,
|
||||
@@ -170,10 +122,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Label size'),
|
||||
description: t('The font size of the point labels'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
(!form_data.enable_label_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_labels,
|
||||
validators: [legacyValidateInteger],
|
||||
choices: formatSelectOptions([8, 16, 24, 32, 64, 128]),
|
||||
default: 24,
|
||||
@@ -189,10 +138,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'SelectControl',
|
||||
label: t('Label size unit'),
|
||||
description: t('The unit for label size'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
(!form_data.enable_label_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_labels,
|
||||
choices: [
|
||||
['meters', t('Meters')],
|
||||
['pixels', t('Pixels')],
|
||||
@@ -203,27 +149,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'label_javascript_config_generator',
|
||||
config: {
|
||||
...jsFunctionControl(
|
||||
t('Label JavaScript config generator'),
|
||||
t(
|
||||
'A JavaScript function that generates a label configuration object',
|
||||
),
|
||||
undefined,
|
||||
undefined,
|
||||
defaultLabelConfigGenerator,
|
||||
),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_labels &&
|
||||
!!form_data.enable_label_javascript_mode &&
|
||||
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'enable_icons',
|
||||
@@ -236,24 +161,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'enable_icon_javascript_mode',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable icon JavaScript mode'),
|
||||
description: t(
|
||||
'Enables custom icon configuration via JavaScript',
|
||||
),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_icons &&
|
||||
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'icon_url',
|
||||
@@ -265,10 +172,7 @@ const config: ControlPanelConfig = {
|
||||
'Note that the image URL must conform to the content ' +
|
||||
'security policy (CSP) in order to load correctly.',
|
||||
),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_icons &&
|
||||
(!form_data.enable_icon_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_icons,
|
||||
default: '',
|
||||
renderTrigger: true,
|
||||
resetOnHide: false,
|
||||
@@ -283,10 +187,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Icon size'),
|
||||
description: t('The size of the point icons'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_icons &&
|
||||
(!form_data.enable_icon_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_icons,
|
||||
validators: [legacyValidateInteger],
|
||||
choices: formatSelectOptions([16, 24, 32, 64, 128]),
|
||||
default: 32,
|
||||
@@ -302,10 +203,7 @@ const config: ControlPanelConfig = {
|
||||
type: 'SelectControl',
|
||||
label: t('Icon size unit'),
|
||||
description: t('The unit for icon size'),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_icons &&
|
||||
(!form_data.enable_icon_javascript_mode ||
|
||||
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||
visibility: ({ form_data }) => !!form_data.enable_icons,
|
||||
choices: [
|
||||
['meters', t('Meters')],
|
||||
['pixels', t('Pixels')],
|
||||
@@ -316,27 +214,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'icon_javascript_config_generator',
|
||||
config: {
|
||||
...jsFunctionControl(
|
||||
t('Icon JavaScript config generator'),
|
||||
t(
|
||||
'A JavaScript function that generates an icon configuration object',
|
||||
),
|
||||
undefined,
|
||||
undefined,
|
||||
defaultIconConfigGenerator,
|
||||
),
|
||||
visibility: ({ form_data }) =>
|
||||
!!form_data.enable_icons &&
|
||||
!!form_data.enable_icon_javascript_mode &&
|
||||
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[lineWidth],
|
||||
[
|
||||
{
|
||||
@@ -411,13 +288,7 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[crossFilterColumn],
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
controlSetRows: [[crossFilterColumn]],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
getColorForBreakpoints,
|
||||
getColorRange,
|
||||
} from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
|
||||
@@ -65,12 +64,7 @@ export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const colorBreakpoints = fd.color_breakpoints;
|
||||
|
||||
@@ -132,13 +126,7 @@ export const getHighlightLayer: GetLayerType<GridLayer> = function ({
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
|
||||
@@ -25,10 +25,6 @@ import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
extruded,
|
||||
gridSize,
|
||||
viewport,
|
||||
@@ -75,15 +71,6 @@ const config: ControlPanelConfig = {
|
||||
[extruded],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -92,10 +92,6 @@ export const GridChartViz = ({
|
||||
autozoom,
|
||||
grid_size: gridSize,
|
||||
extruded,
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import { isPointInBonds } from '../../utilities/utils';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { createTooltipContent } from '../../utilities/tooltipUtils';
|
||||
@@ -110,15 +109,9 @@ export const getLayer: GetLayerType<HeatmapLayer> = ({
|
||||
intensity = 1,
|
||||
radius_pixels: radiusPixels = 30,
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
linear_color_scheme: colorScheme,
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (jsFnMutator) {
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const colorScale = getSequentialSchemeRegistry()
|
||||
?.get(colorScheme)
|
||||
@@ -173,15 +166,8 @@ export const getHighlightLayer: GetLayerType<HeatmapLayer> = ({
|
||||
intensity = 1,
|
||||
radius_pixels: radiusPixels = 30,
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (jsFnMutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const dataInside = data.filter((d: JsonObject) =>
|
||||
isPointInBonds(d.position, filterState?.value),
|
||||
|
||||
@@ -32,10 +32,6 @@ import {
|
||||
deckGLFixedColor,
|
||||
deckGLLinearColorSchemeSelect,
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
@@ -148,15 +144,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
getColorForBreakpoints,
|
||||
getColorRange,
|
||||
} from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import {
|
||||
createTooltipContent,
|
||||
@@ -65,13 +64,7 @@ export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
const colorRange = getColorRange({
|
||||
@@ -133,13 +126,7 @@ export const getHighlightLayer: GetLayerType<HexagonLayer> = function ({
|
||||
filterState,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
|
||||
|
||||
|
||||
@@ -27,10 +27,6 @@ import {
|
||||
filterNulls,
|
||||
generateDeckGLColorSchemeControls,
|
||||
gridSize,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
@@ -101,15 +97,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
|
||||
@@ -93,10 +93,6 @@ export const HexChartViz = ({
|
||||
grid_size: gridSize,
|
||||
extruded,
|
||||
js_agg_function: 'sum',
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
import { PathLayer } from '@deck.gl/layers';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
|
||||
import { Point } from '../../types';
|
||||
import {
|
||||
@@ -115,11 +114,6 @@ export const getLayer: GetLayerType<PathLayer> = function ({
|
||||
});
|
||||
}
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
return new PathLayer({
|
||||
id: `path-layer-${fd.slice_id}` as const,
|
||||
getColor: (d: any) => d.color || [0, 0, 0, 255],
|
||||
@@ -163,7 +157,7 @@ export const getHighlightLayer: GetLayerType<PathLayer> = function ({
|
||||
const maxWidth = Number(fd.max_width) || 20;
|
||||
const multiplier = Number(fd.line_width_multiplier) || 1;
|
||||
const fixedColor = HIGHLIGHT_COLOR_ARRAY;
|
||||
let data = payload.data.features.map((feature: JsonObject) => {
|
||||
const data = payload.data.features.map((feature: JsonObject) => {
|
||||
const baseWidth = Number.isFinite(feature.width) ? feature.width : 1;
|
||||
let width = baseWidth * multiplier;
|
||||
|
||||
@@ -177,11 +171,6 @@ export const getHighlightLayer: GetLayerType<PathLayer> = function ({
|
||||
};
|
||||
});
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
|
||||
const filteredData = data.filter(
|
||||
(d: JsonObject) =>
|
||||
JSON.stringify(d.path).replaceAll(' ', '') === filterState?.value?.[0],
|
||||
|
||||
@@ -339,7 +339,6 @@ test('Path buildQuery should handle line_width and breakpoint_metrics together t
|
||||
value: 'SUM(distance)',
|
||||
},
|
||||
breakpoint_metric: 'AVG(speed)',
|
||||
js_columns: ['color'],
|
||||
tooltip_contents: ['name'],
|
||||
row_limit: 500,
|
||||
};
|
||||
@@ -349,7 +348,6 @@ test('Path buildQuery should handle line_width and breakpoint_metrics together t
|
||||
|
||||
expect(query.metrics).toContain('SUM(distance)');
|
||||
expect(query.metrics).toContain('AVG(speed)');
|
||||
expect(query.columns).toContain('color');
|
||||
expect(query.columns).toContain('name');
|
||||
expect(query.row_limit).toBe(500);
|
||||
});
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface DeckPathFormData extends SqlaFormData {
|
||||
line_type?: 'polyline' | 'json' | 'geohash';
|
||||
metric?: string;
|
||||
reverse_long_lat?: boolean;
|
||||
js_columns?: string[];
|
||||
tooltip_contents?: unknown[];
|
||||
tooltip_template?: string;
|
||||
line_width?:
|
||||
@@ -48,7 +47,6 @@ export default function buildQuery(formData: DeckPathFormData) {
|
||||
const {
|
||||
line_column,
|
||||
metric,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
line_width,
|
||||
dimension,
|
||||
@@ -68,7 +66,6 @@ export default function buildQuery(formData: DeckPathFormData) {
|
||||
const groupby = ensureIsArray(
|
||||
baseQueryObject.groupby || [],
|
||||
) as QueryFormColumn[];
|
||||
const jsColumns = ensureIsArray(js_columns || []);
|
||||
|
||||
if (baseQueryObject.metrics?.length || metric) {
|
||||
if (metric && !metrics.includes(metric)) {
|
||||
@@ -124,12 +121,6 @@ export default function buildQuery(formData: DeckPathFormData) {
|
||||
}
|
||||
}
|
||||
|
||||
jsColumns.forEach(col => {
|
||||
if (!columns.includes(col) && !groupby.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
|
||||
const finalColumns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
const finalGroupby = addTooltipColumnsToQuery(groupby, tooltip_contents);
|
||||
|
||||
|
||||
@@ -21,10 +21,6 @@ import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
viewport,
|
||||
lineType,
|
||||
reverseLongLat,
|
||||
@@ -153,15 +149,6 @@ const config: ControlPanelConfig = {
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -97,10 +97,6 @@ export const PathChartViz = ({
|
||||
line_width: lineWidth,
|
||||
reverse_long_lat: false,
|
||||
autozoom,
|
||||
js_columns: ['color'],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, DTTM_ALIAS, getMetricLabel } from '@superset-ui/core';
|
||||
import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
|
||||
import { DataRecord } from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
@@ -39,12 +39,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeckPathTransformPropsFormData extends DeckPathFormData {
|
||||
js_data_mutator?: string;
|
||||
js_tooltip?: string;
|
||||
js_onclick_href?: string;
|
||||
}
|
||||
|
||||
interface PathFeature {
|
||||
path: [number, number][];
|
||||
metric?: number;
|
||||
@@ -93,7 +87,6 @@ function processPathData(
|
||||
lineType: 'polyline' | 'json' | 'geohash' = 'json',
|
||||
reverseLongLat: boolean = false,
|
||||
metricLabel?: string,
|
||||
jsColumns?: string[],
|
||||
widthMetricLabel?: string,
|
||||
fixedWidthValue?: number | string | null,
|
||||
categoryColumn?: string,
|
||||
@@ -111,7 +104,6 @@ function processPathData(
|
||||
metricLabel,
|
||||
widthMetricLabel,
|
||||
categoryColumn,
|
||||
...(jsColumns || []),
|
||||
].filter(Boolean) as string[],
|
||||
);
|
||||
|
||||
@@ -157,7 +149,6 @@ function processPathData(
|
||||
feature.cat_color = String(record[categoryColumn]);
|
||||
}
|
||||
|
||||
feature = addJsColumnsToExtraProps(feature, record, jsColumns);
|
||||
feature = addPropertiesToFeature(feature, record, excludeKeys);
|
||||
return feature;
|
||||
});
|
||||
@@ -172,9 +163,8 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
line_width,
|
||||
dimension,
|
||||
reverse_long_lat = false,
|
||||
js_columns,
|
||||
breakpoint_metric,
|
||||
} = formData as DeckPathTransformPropsFormData;
|
||||
} = formData as DeckPathFormData;
|
||||
|
||||
// Check so legacy values still work
|
||||
const fixedWidthValue =
|
||||
@@ -207,7 +197,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
line_type,
|
||||
reverse_long_lat,
|
||||
metricLabel,
|
||||
js_columns,
|
||||
widthMetricLabel,
|
||||
fixedWidthValue,
|
||||
dimension,
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
} from '../../utils';
|
||||
|
||||
import { commonLayerProps, getColorForBreakpoints } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import getPointsFromPolygon from '../../utils/getPointsFromPolygon';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import {
|
||||
@@ -121,13 +120,7 @@ export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
const sc: { r: number; g: number; b: number; a: number } =
|
||||
fd.stroke_color_picker;
|
||||
const defaultBreakpointColor = fd.default_breakpoint_color;
|
||||
let data = [...payload.data.features];
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = [...payload.data.features];
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type;
|
||||
|
||||
|
||||
@@ -318,22 +318,6 @@ describe('Polygon buildQuery', () => {
|
||||
});
|
||||
|
||||
describe('Integration with other form data fields', () => {
|
||||
test('should include js_columns in query columns', () => {
|
||||
const formDataWithJsColumns = {
|
||||
...baseFormData,
|
||||
js_columns: ['custom_col1', 'custom_col2'],
|
||||
};
|
||||
|
||||
const queryContext = buildQuery(formDataWithJsColumns);
|
||||
const [query] = queryContext.queries;
|
||||
|
||||
expect(query.columns).toEqual([
|
||||
'polygon_geom',
|
||||
'custom_col1',
|
||||
'custom_col2',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should include tooltip_contents columns in query', () => {
|
||||
const formDataWithTooltips = {
|
||||
...baseFormData,
|
||||
@@ -375,20 +359,19 @@ describe('Polygon buildQuery', () => {
|
||||
expect(query.columns).toContain('sa3_name');
|
||||
});
|
||||
|
||||
test('should not duplicate cross_filter_column when it overlaps with another column', () => {
|
||||
test('should not duplicate cross_filter_column when it overlaps with line_column', () => {
|
||||
const formDataWithOverlap = {
|
||||
...baseFormData,
|
||||
js_columns: ['sa3_name', 'extra_col'],
|
||||
cross_filter_column: 'sa3_name',
|
||||
cross_filter_column: 'polygon_geom',
|
||||
};
|
||||
|
||||
const queryContext = buildQuery(formDataWithOverlap);
|
||||
const [query] = queryContext.queries;
|
||||
|
||||
const sa3Count = query.columns?.filter(
|
||||
(col: unknown) => col === 'sa3_name',
|
||||
const geomCount = query.columns?.filter(
|
||||
(col: unknown) => col === 'polygon_geom',
|
||||
).length;
|
||||
expect(sa3Count).toBe(1);
|
||||
expect(geomCount).toBe(1);
|
||||
});
|
||||
|
||||
test('should not add cross_filter_column to query columns when unset', () => {
|
||||
@@ -412,7 +395,6 @@ describe('Polygon buildQuery', () => {
|
||||
label: 'AVG(elevation)',
|
||||
},
|
||||
},
|
||||
js_columns: ['custom_prop'],
|
||||
tooltip_contents: [
|
||||
{ item_type: 'column', column_name: 'tooltip_info' },
|
||||
],
|
||||
@@ -423,7 +405,6 @@ describe('Polygon buildQuery', () => {
|
||||
const [query] = queryContext.queries;
|
||||
|
||||
expect(query.columns).toContain('polygon_geom');
|
||||
expect(query.columns).toContain('custom_prop');
|
||||
expect(query.columns).toContain('tooltip_info');
|
||||
expect(query.metrics).toContain('population');
|
||||
expect(query.metrics).toContain(complexFormData.point_radius_fixed.value);
|
||||
|
||||
@@ -46,7 +46,6 @@ export interface DeckPolygonFormData extends SqlaFormData {
|
||||
};
|
||||
reverse_long_lat?: boolean;
|
||||
filter_nulls?: boolean;
|
||||
js_columns?: string[];
|
||||
cross_filter_column?: string | null;
|
||||
tooltip_contents?: unknown[];
|
||||
tooltip_template?: string;
|
||||
@@ -58,7 +57,6 @@ export default function buildQuery(formData: DeckPolygonFormData) {
|
||||
metric,
|
||||
point_radius_fixed,
|
||||
filter_nulls = true,
|
||||
js_columns,
|
||||
cross_filter_column,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
@@ -73,13 +71,6 @@ export default function buildQuery(formData: DeckPolygonFormData) {
|
||||
line_column,
|
||||
];
|
||||
|
||||
const jsColumns = ensureIsArray(js_columns || []);
|
||||
jsColumns.forEach((col: string) => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
|
||||
if (cross_filter_column && !columns.includes(cross_filter_column)) {
|
||||
columns.push(cross_filter_column);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
crossFilterColumn,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
@@ -203,13 +199,7 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[crossFilterColumn],
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
controlSetRows: [[crossFilterColumn]],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
|
||||
@@ -137,10 +137,6 @@ export const PolygonChartViz = ({
|
||||
toggle_polygons: true,
|
||||
legend_position: 'tr',
|
||||
legend_format: null,
|
||||
js_columns: ['population', 'area'],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, getMetricLabel, QueryFormMetric } from '@superset-ui/core';
|
||||
import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
|
||||
import { DataRecord } from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
@@ -104,7 +104,6 @@ function processPolygonData(
|
||||
metric,
|
||||
point_radius_fixed,
|
||||
reverse_long_lat,
|
||||
js_columns,
|
||||
} = formData;
|
||||
|
||||
if (!line_column || !records.length) {
|
||||
@@ -139,7 +138,7 @@ function processPolygonData(
|
||||
}
|
||||
}
|
||||
|
||||
const excludeKeys = new Set([line_column, ...(js_columns || [])]);
|
||||
const excludeKeys = new Set([line_column]);
|
||||
|
||||
return records.flatMap(record => {
|
||||
let feature: PolygonFeature = {
|
||||
@@ -147,7 +146,6 @@ function processPolygonData(
|
||||
metrics: {},
|
||||
};
|
||||
|
||||
feature = addJsColumnsToExtraProps(feature, record, js_columns);
|
||||
const updatedFeature = addPropertiesToFeature(
|
||||
feature as unknown as Record<string, unknown>,
|
||||
record,
|
||||
|
||||
@@ -195,23 +195,6 @@ test('Scatter buildQuery should handle tooltip_contents', () => {
|
||||
expect(query.columns).toContain('description');
|
||||
});
|
||||
|
||||
test('Scatter buildQuery should handle js_columns', () => {
|
||||
const formData: DeckScatterFormData = {
|
||||
...baseFormData,
|
||||
js_columns: ['custom_col1', 'custom_col2'],
|
||||
point_radius_fixed: {
|
||||
type: 'fix',
|
||||
value: '1000',
|
||||
},
|
||||
};
|
||||
|
||||
const queryContext = buildQuery(formData);
|
||||
const [query] = queryContext.queries;
|
||||
|
||||
expect(query.columns).toContain('custom_col1');
|
||||
expect(query.columns).toContain('custom_col2');
|
||||
});
|
||||
|
||||
test('Scatter buildQuery should handle adhoc SQL metric for point_radius_fixed', () => {
|
||||
const adhocMetric = {
|
||||
label: 'count(*) * 1.1',
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
QueryFormMetric,
|
||||
QueryFormOrderBy,
|
||||
SqlaFormData,
|
||||
QueryFormColumn,
|
||||
QueryObject,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
@@ -31,10 +30,7 @@ import {
|
||||
addSpatialNullFilters,
|
||||
SpatialFormData,
|
||||
} from '../spatialUtils';
|
||||
import {
|
||||
addJsColumnsToColumns,
|
||||
addTooltipColumnsToQuery,
|
||||
} from '../buildQueryUtils';
|
||||
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
|
||||
import { isMetricValue } from '../utils/metricUtils';
|
||||
|
||||
export interface DeckScatterFormData
|
||||
@@ -55,13 +51,7 @@ export interface DeckScatterFormData
|
||||
}
|
||||
|
||||
export default function buildQuery(formData: DeckScatterFormData) {
|
||||
const {
|
||||
spatial,
|
||||
point_radius_fixed,
|
||||
dimension,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
const { spatial, point_radius_fixed, dimension, tooltip_contents } = formData;
|
||||
|
||||
if (!spatial) {
|
||||
throw new Error('Spatial configuration is required for Scatter charts');
|
||||
@@ -76,12 +66,6 @@ export default function buildQuery(formData: DeckScatterFormData) {
|
||||
columns.push(dimension);
|
||||
}
|
||||
|
||||
const columnStrings = columns.map(col =>
|
||||
typeof col === 'string' ? col : col.label || col.sqlExpression || '',
|
||||
);
|
||||
const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
|
||||
|
||||
columns = withJsColumns as QueryFormColumn[];
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
// Only add metric if point_radius_fixed is a metric type
|
||||
|
||||
@@ -23,10 +23,6 @@ import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
viewport,
|
||||
@@ -144,15 +140,6 @@ const config: ControlPanelConfig = {
|
||||
...generateDeckGLColorSchemeControls({}),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -106,10 +106,6 @@ export const ScatterChartViz = ({
|
||||
dimension: null,
|
||||
color_scheme: 'bnbColors',
|
||||
label_colors: {},
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
granularity: null,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -43,7 +43,6 @@ function processScatterData(
|
||||
spatial: DeckScatterFormData['spatial'],
|
||||
radiusMetricLabel?: string,
|
||||
categoryColumn?: string,
|
||||
jsColumns?: string[],
|
||||
fixedRadiusValue?: number | string | null,
|
||||
): ScatterPoint[] {
|
||||
if (!spatial || !records.length) {
|
||||
@@ -65,7 +64,6 @@ function processScatterData(
|
||||
: []),
|
||||
radiusMetricLabel,
|
||||
categoryColumn,
|
||||
...(jsColumns || []),
|
||||
]);
|
||||
|
||||
return spatialFeatures.map(feature => {
|
||||
@@ -105,7 +103,7 @@ function processScatterData(
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { rawFormData: formData } = chartProps;
|
||||
const { spatial, point_radius_fixed, dimension, js_columns } =
|
||||
const { spatial, point_radius_fixed, dimension } =
|
||||
formData as DeckScatterFormData;
|
||||
|
||||
// Check if this is a fixed value or metric
|
||||
@@ -121,7 +119,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
spatial,
|
||||
radiusMetricLabel,
|
||||
dimension,
|
||||
js_columns,
|
||||
fixedRadiusValue,
|
||||
);
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
ColorSchemeType,
|
||||
isPointInBonds,
|
||||
} from '../../utilities/utils';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps, getColorRange } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
@@ -117,12 +116,7 @@ export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
let data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const data = payload.data.features;
|
||||
|
||||
const colorSchemeType = fd.color_scheme_type as ColorSchemeType & 'default';
|
||||
const colorRange = getColorRange({
|
||||
@@ -225,14 +219,8 @@ const getHighlightLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
filterState,
|
||||
payload,
|
||||
}) {
|
||||
const fd = formData;
|
||||
let data = payload.data.features;
|
||||
const data = payload.data.features;
|
||||
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutator(data);
|
||||
}
|
||||
const dataInside = data.filter((d: JsonObject) =>
|
||||
isPointInBonds(d.position, filterState?.value),
|
||||
);
|
||||
|
||||
@@ -26,10 +26,6 @@ import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
gridSize,
|
||||
viewport,
|
||||
spatial,
|
||||
@@ -94,15 +90,6 @@ const config: ControlPanelConfig = {
|
||||
[deckGLCategoricalColorSchemeSelect],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -84,10 +84,6 @@ export const ScreengridChartViz = ({
|
||||
autozoom,
|
||||
grid_size: gridSize,
|
||||
color_picker: { a: 1, b: 0, g: 255, r: 14 },
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23,26 +23,6 @@ import {
|
||||
getColumnLabel,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export function addJsColumnsToColumns(
|
||||
columns: string[],
|
||||
jsColumns?: string[],
|
||||
existingColumns?: string[],
|
||||
): string[] {
|
||||
if (!jsColumns?.length) return columns;
|
||||
|
||||
const allExisting = new Set([...columns, ...(existingColumns || [])]);
|
||||
const result = [...columns];
|
||||
|
||||
jsColumns.forEach(col => {
|
||||
if (!allExisting.has(col)) {
|
||||
result.push(col);
|
||||
allExisting.add(col);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function addNullFilters(
|
||||
filters: QueryObjectFilterClause[],
|
||||
columnNames: string[],
|
||||
|
||||
@@ -103,11 +103,8 @@ describe('commonLayerProps', () => {
|
||||
) as any;
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
test('returns correct props when js_tooltip is provided', () => {
|
||||
const formData = {
|
||||
...partialformData,
|
||||
js_tooltip: 'tooltip => tooltip.content',
|
||||
} as QueryFormData;
|
||||
test('returns correct props with a tooltip content generator', () => {
|
||||
const formData = { ...partialformData } as QueryFormData;
|
||||
const props = commonLayerProps({
|
||||
formData,
|
||||
setTooltip: mockSetTooltip,
|
||||
@@ -118,7 +115,7 @@ describe('commonLayerProps', () => {
|
||||
});
|
||||
|
||||
test('calls onHover and sets tooltip', () => {
|
||||
const formData = { ...partialformData, js_tooltip: null } as QueryFormData;
|
||||
const formData = { ...partialformData } as QueryFormData;
|
||||
const props = commonLayerProps({
|
||||
formData,
|
||||
setTooltip: mockSetTooltip,
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
import { Layer, PickingInfo, Color } from '@deck.gl/core';
|
||||
import { ScaleLinear } from 'd3-scale';
|
||||
import { ColorBreakpointType } from '../types';
|
||||
import sandboxedEval from '../utils/sandbox';
|
||||
import { TooltipProps } from '../components/Tooltip';
|
||||
import { getCrossFilterDataMask } from '../utils/crossFiltersDataMask';
|
||||
import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../utilities/utils';
|
||||
@@ -68,11 +67,7 @@ export function commonLayerProps({
|
||||
}) {
|
||||
const fd = formData;
|
||||
let onHover;
|
||||
let tooltipContentGenerator = setTooltipContent;
|
||||
if (fd.js_tooltip) {
|
||||
tooltipContentGenerator = sandboxedEval(fd.js_tooltip);
|
||||
}
|
||||
if (tooltipContentGenerator) {
|
||||
if (setTooltipContent) {
|
||||
let currentTooltipContent: ReactNode = null;
|
||||
|
||||
const isCustomTooltip = (content: ReactNode): boolean =>
|
||||
@@ -81,7 +76,7 @@ export function commonLayerProps({
|
||||
|
||||
onHover = (o: JsonObject) => {
|
||||
if (o.picked) {
|
||||
currentTooltipContent = tooltipContentGenerator(o);
|
||||
currentTooltipContent = setTooltipContent(o);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -102,13 +97,7 @@ export function commonLayerProps({
|
||||
}
|
||||
|
||||
let onClick;
|
||||
if (fd.js_onclick_href) {
|
||||
onClick = (o: any) => {
|
||||
const href = sandboxedEval(fd.js_onclick_href)(o);
|
||||
window.open(href);
|
||||
return true;
|
||||
};
|
||||
} else if (fd.table_filter && onSelect !== undefined) {
|
||||
if (fd.table_filter && onSelect !== undefined) {
|
||||
onClick = (o: any) => {
|
||||
onSelect(o.object[fd.line_column]);
|
||||
return true;
|
||||
|
||||
@@ -199,7 +199,6 @@ describe('spatialUtils', () => {
|
||||
latCol: 'latitude',
|
||||
},
|
||||
size: 'count',
|
||||
js_columns: ['extra_col'],
|
||||
} as SpatialFormData;
|
||||
|
||||
buildSpatialQuery(formData);
|
||||
@@ -220,20 +219,21 @@ describe('spatialUtils', () => {
|
||||
latCol: 'latitude',
|
||||
};
|
||||
const metricLabel = 'count';
|
||||
const jsColumns = ['extra'];
|
||||
|
||||
const result = processSpatialData(records, spatial, metricLabel, jsColumns);
|
||||
const result = processSpatialData(records, spatial, metricLabel);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
position: [-122.4, 37.8],
|
||||
weight: 10,
|
||||
extraProps: { extra: 'test1' },
|
||||
extraProps: {},
|
||||
extra: 'test1',
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
position: [-122.5, 37.9],
|
||||
weight: 20,
|
||||
extraProps: { extra: 'test2' },
|
||||
extraProps: {},
|
||||
extra: 'test2',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -398,22 +398,21 @@ describe('spatialUtils', () => {
|
||||
lonCol: 'longitude',
|
||||
latCol: 'latitude',
|
||||
};
|
||||
const jsColumns = ['extra_col'];
|
||||
|
||||
const result = processSpatialData(records, spatial, 'count', jsColumns);
|
||||
const result = processSpatialData(records, spatial, 'count');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
position: [-122.4, 37.8],
|
||||
weight: 10,
|
||||
extraProps: { extra_col: 'extra_value' },
|
||||
extraProps: {},
|
||||
category: 'A',
|
||||
description: 'Test location',
|
||||
extra_col: 'extra_value',
|
||||
});
|
||||
|
||||
expect(result[0]).not.toHaveProperty('longitude');
|
||||
expect(result[0]).not.toHaveProperty('latitude');
|
||||
expect(result[0]).not.toHaveProperty('count');
|
||||
expect(result[0]).not.toHaveProperty('extra_col');
|
||||
});
|
||||
|
||||
test('transformSpatialProps transforms chart props correctly', () => {
|
||||
@@ -452,7 +451,6 @@ describe('spatialUtils', () => {
|
||||
latCol: 'latitude',
|
||||
},
|
||||
size: 'count',
|
||||
js_columns: [],
|
||||
viewport: {
|
||||
zoom: 10,
|
||||
latitude: 37.8,
|
||||
@@ -471,7 +469,6 @@ describe('spatialUtils', () => {
|
||||
latCol: 'latitude',
|
||||
},
|
||||
size: 'count',
|
||||
js_columns: [],
|
||||
viewport: {
|
||||
zoom: 10,
|
||||
latitude: 37.8,
|
||||
|
||||
@@ -53,9 +53,7 @@ export interface SpatialFormData extends QueryFormData {
|
||||
spatial: SpatialConfiguration;
|
||||
size?: string;
|
||||
grid_size?: number;
|
||||
js_data_mutator?: string;
|
||||
js_agg_function?: string;
|
||||
js_columns?: string[];
|
||||
color_scheme?: string;
|
||||
color_scheme_type?: string;
|
||||
color_breakpoints?: number[];
|
||||
@@ -119,7 +117,7 @@ export function addSpatialNullFilters(
|
||||
}
|
||||
|
||||
export function buildSpatialQuery(formData: SpatialFormData) {
|
||||
const { spatial, size: metric, js_columns, tooltip_contents } = formData;
|
||||
const { spatial, size: metric, tooltip_contents } = formData;
|
||||
|
||||
if (!spatial) {
|
||||
throw new Error(`Spatial configuration is required for this chart`);
|
||||
@@ -130,14 +128,6 @@ export function buildSpatialQuery(formData: SpatialFormData) {
|
||||
let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
|
||||
const metrics = metric ? [metric] : [];
|
||||
|
||||
if (js_columns?.length) {
|
||||
js_columns.forEach(col => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
const filters = addSpatialNullFilters(
|
||||
@@ -207,29 +197,10 @@ function reverseGeohashDecode(geohashCode: string): [number, number] | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function addJsColumnsToExtraProps<
|
||||
T extends { extraProps?: Record<string, unknown> },
|
||||
>(feature: T, record: DataRecord, jsColumns?: string[]): T {
|
||||
if (!jsColumns?.length) {
|
||||
return feature;
|
||||
}
|
||||
|
||||
const extraProps: Record<string, unknown> = { ...feature.extraProps };
|
||||
|
||||
jsColumns.forEach(col => {
|
||||
if (record[col] !== undefined) {
|
||||
extraProps[col] = record[col];
|
||||
}
|
||||
});
|
||||
|
||||
return { ...feature, extraProps };
|
||||
}
|
||||
|
||||
export function processSpatialData(
|
||||
records: DataRecord[],
|
||||
spatial: SpatialConfiguration,
|
||||
metricLabel?: string,
|
||||
jsColumns?: string[],
|
||||
): SpatialPoint[] {
|
||||
if (!spatial || !records.length) {
|
||||
return [];
|
||||
@@ -237,7 +208,6 @@ export function processSpatialData(
|
||||
|
||||
const features: SpatialPoint[] = [];
|
||||
const spatialColumns = getSpatialColumns(spatial);
|
||||
const jsColumnsSet = jsColumns ? new Set(jsColumns) : null;
|
||||
const spatialColumnsSet = new Set(spatialColumns);
|
||||
|
||||
for (const record of records) {
|
||||
@@ -286,13 +256,12 @@ export function processSpatialData(
|
||||
}
|
||||
}
|
||||
|
||||
let spatialPoint: SpatialPoint = {
|
||||
const spatialPoint: SpatialPoint = {
|
||||
position,
|
||||
weight,
|
||||
extraProps: {},
|
||||
};
|
||||
|
||||
spatialPoint = addJsColumnsToExtraProps(spatialPoint, record, jsColumns);
|
||||
Object.keys(record).forEach(key => {
|
||||
if (spatialColumnsSet.has(key)) {
|
||||
return;
|
||||
@@ -302,10 +271,6 @@ export function processSpatialData(
|
||||
return;
|
||||
}
|
||||
|
||||
if (jsColumnsSet?.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
spatialPoint[key] = record[key];
|
||||
});
|
||||
|
||||
@@ -345,17 +310,12 @@ export function transformSpatialProps(chartProps: ChartProps) {
|
||||
setDataMask = NOOP,
|
||||
} = hooks;
|
||||
|
||||
const { spatial, size: metric, js_columns } = formData as SpatialFormData;
|
||||
const { spatial, size: metric } = formData as SpatialFormData;
|
||||
const metricLabel = metric ? getMetricLabel(metric) : undefined;
|
||||
|
||||
const queryData = queriesData[0];
|
||||
const records = queryData?.data || [];
|
||||
const features = processSpatialData(
|
||||
records,
|
||||
spatial,
|
||||
metricLabel,
|
||||
js_columns,
|
||||
);
|
||||
const features = processSpatialData(records, spatial, metricLabel);
|
||||
|
||||
return {
|
||||
datasource,
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
validateNonEmpty,
|
||||
getCategoricalSchemeRegistry,
|
||||
getSequentialSchemeRegistry,
|
||||
@@ -151,49 +149,6 @@ const DEFAULT_VIEWPORT = {
|
||||
pitch: 0,
|
||||
};
|
||||
|
||||
const sandboxUrl =
|
||||
'https://github.com/apache/superset/' +
|
||||
'blob/master/superset-frontend/plugins/preset-chart-deckgl/src/utils/sandbox.ts';
|
||||
const jsFunctionInfo = (
|
||||
<div>
|
||||
{t(
|
||||
'For more information about objects are in context in the scope of this function, refer to the',
|
||||
)}
|
||||
<a href={sandboxUrl}>{t(" source code of Superset's sandboxed parser")}.</a>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
|
||||
export function jsFunctionControl(
|
||||
label: string,
|
||||
description: string,
|
||||
extraDescr = null,
|
||||
height = 100,
|
||||
defaultText = '',
|
||||
) {
|
||||
return {
|
||||
type: 'TextAreaControl',
|
||||
language: 'javascript',
|
||||
label,
|
||||
description,
|
||||
height,
|
||||
default: defaultText,
|
||||
aboveEditorSection: (
|
||||
<div>
|
||||
<p>{description}</p>
|
||||
<p>{jsFunctionInfo}</p>
|
||||
{extraDescr}
|
||||
</div>
|
||||
),
|
||||
warning: !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)
|
||||
? t(
|
||||
'This functionality is disabled in your environment for security reasons.',
|
||||
)
|
||||
: null,
|
||||
readOnly: !isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||
};
|
||||
}
|
||||
|
||||
export const filterNulls = {
|
||||
name: 'filter_nulls',
|
||||
config: {
|
||||
@@ -228,18 +183,6 @@ export const dimension: CustomControlItem = {
|
||||
},
|
||||
};
|
||||
|
||||
export const jsColumns = {
|
||||
name: 'js_columns',
|
||||
config: {
|
||||
...sharedControls.groupby,
|
||||
label: t('Extra data for JS'),
|
||||
default: [],
|
||||
description: t(
|
||||
'List of extra columns made available in JavaScript functions',
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const crossFilterColumn: CustomControlItem = {
|
||||
name: 'cross_filter_column',
|
||||
config: {
|
||||
@@ -255,36 +198,6 @@ export const crossFilterColumn: CustomControlItem = {
|
||||
},
|
||||
};
|
||||
|
||||
export const jsDataMutator = {
|
||||
name: 'js_data_mutator',
|
||||
config: jsFunctionControl(
|
||||
t('JavaScript data interceptor'),
|
||||
t(
|
||||
'Define a javascript function that receives the data array used in the visualization ' +
|
||||
'and is expected to return a modified version of that array. This can be used ' +
|
||||
'to alter properties of the data, filter, or enrich the array.',
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
export const jsTooltip = {
|
||||
name: 'js_tooltip',
|
||||
config: jsFunctionControl(
|
||||
t('JavaScript tooltip generator'),
|
||||
t(
|
||||
'Define a function that receives the input and outputs the content for a tooltip',
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
export const jsOnclickHref = {
|
||||
name: 'js_onclick_href',
|
||||
config: jsFunctionControl(
|
||||
t('JavaScript onClick href'),
|
||||
t('Define a function that returns a URL to navigate to when user clicks'),
|
||||
),
|
||||
};
|
||||
|
||||
export const legendFormat = {
|
||||
name: 'legend_format',
|
||||
config: {
|
||||
|
||||
@@ -447,52 +447,6 @@ describe('getCrossFilterDataMask', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('deck_polygon with cross_filter_column reads from extraProps fallback', () => {
|
||||
const polygonFormData = {
|
||||
...formData,
|
||||
line_column: 'geojson',
|
||||
cross_filter_column: 'region_id',
|
||||
};
|
||||
|
||||
const polygonPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
polygon: [
|
||||
[-122.42, 37.8],
|
||||
[-122.42, 37.81],
|
||||
[-122.41, 37.81],
|
||||
[-122.41, 37.8],
|
||||
[-122.42, 37.8],
|
||||
],
|
||||
extraProps: { region_id: 42 },
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: polygonFormData,
|
||||
data: polygonPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
expect(dataMask).toStrictEqual({
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'region_id',
|
||||
op: '==',
|
||||
val: 42,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [42],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('deck_polygon throws when cross_filter_column value missing on feature', () => {
|
||||
const polygonFormData = {
|
||||
...formData,
|
||||
|
||||
@@ -414,14 +414,12 @@ const getLineColumnFilters = ({
|
||||
if (!path) throw new Error('Position of picked data is required');
|
||||
|
||||
// Preferred path: emit on a dimension column the user selected. The value
|
||||
// can land either directly on the picked feature (groupby/excluded keys are
|
||||
// spread by addPropertiesToFeature) or under extraProps when it overlaps
|
||||
// with js_columns (addJsColumnsToExtraProps).
|
||||
// lands directly on the picked feature (groupby/excluded keys are spread
|
||||
// by addPropertiesToFeature).
|
||||
if (formData.cross_filter_column) {
|
||||
const col = formData.cross_filter_column;
|
||||
const obj = data.object ?? {};
|
||||
const extraProps = (obj.extraProps ?? {}) as Record<string, unknown>;
|
||||
const dimensionVal = (obj[col] ?? extraProps[col]) as
|
||||
const dimensionVal = obj[col] as
|
||||
string | number | boolean | null | undefined;
|
||||
|
||||
if (dimensionVal == null) {
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// A safe alternative to JS's eval
|
||||
import vm, { Context, RunningScriptOptions } from 'vm';
|
||||
import _ from 'underscore';
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
import * as d3array from 'd3-array';
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
import * as colors from './colors';
|
||||
|
||||
// Objects exposed here should be treated like a public API
|
||||
// if `underscore` had backwards incompatible changes in a future release, we'd
|
||||
// have to be careful about bumping the library as those changes could break user charts
|
||||
const GLOBAL_CONTEXT = {
|
||||
console,
|
||||
_,
|
||||
colors,
|
||||
d3array,
|
||||
};
|
||||
|
||||
type GlobalContext = {
|
||||
console: Console;
|
||||
_: _.UnderscoreStatic;
|
||||
colors: typeof colors;
|
||||
d3array: typeof d3array;
|
||||
};
|
||||
|
||||
// Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js
|
||||
export default function sandboxedEval(
|
||||
code: string,
|
||||
context?: Context,
|
||||
opts?: RunningScriptOptions | string,
|
||||
) {
|
||||
const sandbox: Context = {};
|
||||
const resultKey = `SAFE_EVAL_${Math.floor(Math.random() * 1000000)}`;
|
||||
sandbox[resultKey] = {};
|
||||
const codeToEval = `${resultKey}=${code}`;
|
||||
const sandboxContext: GlobalContext = { ...GLOBAL_CONTEXT, ...context };
|
||||
Object.keys(sandboxContext).forEach(key => {
|
||||
sandbox[key] = sandboxContext[key as keyof GlobalContext];
|
||||
});
|
||||
try {
|
||||
vm.runInNewContext(codeToEval, sandbox, opts);
|
||||
|
||||
return sandbox[resultKey];
|
||||
} catch (error) {
|
||||
return () => error;
|
||||
}
|
||||
}
|
||||
@@ -472,7 +472,7 @@ const config = {
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.yml'],
|
||||
fallback: {
|
||||
fs: false,
|
||||
vm: require.resolve('vm-browserify'),
|
||||
vm: false,
|
||||
path: false,
|
||||
stream: require.resolve('stream-browserify'),
|
||||
...(isDevMode ? { buffer: require.resolve('buffer/') } : {}), // Fix legacy-plugin-chart-paired-t-test broken Story
|
||||
|
||||
@@ -936,9 +936,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
|
||||
# Enable drill-to-detail functionality in charts
|
||||
# @lifecycle: deprecated
|
||||
"DRILL_TO_DETAIL": True,
|
||||
# Allow JavaScript in chart controls. WARNING: XSS security vulnerability!
|
||||
# @lifecycle: deprecated
|
||||
"ENABLE_JAVASCRIPT_CONTROLS": False,
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
|
||||
@@ -26,12 +26,6 @@ params:
|
||||
g: 122
|
||||
r: 0
|
||||
datasource: 12__table
|
||||
js_columns:
|
||||
- color
|
||||
js_data_mutator: "data => data.map(d => ({\n ...d,\n color: colors.hexToRGB(d.extraProps.color)\n\
|
||||
}));"
|
||||
js_onclick_href: ''
|
||||
js_tooltip: ''
|
||||
line_column: path_json
|
||||
line_type: json
|
||||
line_width: 150
|
||||
|
||||
@@ -29,10 +29,6 @@ params:
|
||||
r: 3
|
||||
filled: true
|
||||
granularity_sqla: null
|
||||
js_columns: []
|
||||
js_data_mutator: ''
|
||||
js_onclick_href: ''
|
||||
js_tooltip: ''
|
||||
legend_format: .1s
|
||||
legend_position: tr
|
||||
line_column: contour
|
||||
|
||||
@@ -386,10 +386,6 @@ def resolve_deck_gl_columns(form_data: dict[str, Any]) -> list[str]:
|
||||
for field in ("line_column", "geojson", "dimension"):
|
||||
_add(form_data.get(field))
|
||||
|
||||
for col in form_data.get("js_columns") or []:
|
||||
if isinstance(col, str):
|
||||
_add(col)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ FRONTEND_CONF_KEYS = (
|
||||
"SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT",
|
||||
"SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE",
|
||||
"SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS",
|
||||
"ENABLE_JAVASCRIPT_CONTROLS",
|
||||
"DEFAULT_SQLLAB_LIMIT",
|
||||
"DEFAULT_VIZ_TYPE",
|
||||
"SQL_MAX_ROW",
|
||||
|
||||
@@ -46,7 +46,7 @@ from superset.exceptions import (
|
||||
SupersetException,
|
||||
SupersetSecurityException,
|
||||
)
|
||||
from superset.extensions import cache_manager, feature_flag_manager, security_manager
|
||||
from superset.extensions import cache_manager, security_manager
|
||||
from superset.legacy import update_time_range
|
||||
from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
@@ -65,23 +65,6 @@ from superset.viz import BaseViz
|
||||
logger = logging.getLogger(__name__)
|
||||
stats_logger = app.config["STATS_LOGGER"]
|
||||
|
||||
# Form-data keys whose values are executed as JavaScript at render time by the
|
||||
# deck.gl charts (via the frontend ``sandboxedEval`` helper). These are stripped
|
||||
# from incoming form_data unless the ``ENABLE_JAVASCRIPT_CONTROLS`` feature flag
|
||||
# is enabled. Keep this list in sync with every ``sandboxedEval(fd.<key>)`` call
|
||||
# site in the deck.gl plugins.
|
||||
JS_CONTROL_FORM_DATA_KEYS: list[str] = [
|
||||
"js_tooltip",
|
||||
"js_onclick_href",
|
||||
"js_data_mutator",
|
||||
"label_javascript_config_generator",
|
||||
"icon_javascript_config_generator",
|
||||
]
|
||||
|
||||
REJECTED_FORM_DATA_KEYS: list[str] = []
|
||||
if not feature_flag_manager.is_feature_enabled("ENABLE_JAVASCRIPT_CONTROLS"):
|
||||
REJECTED_FORM_DATA_KEYS = list(JS_CONTROL_FORM_DATA_KEYS)
|
||||
|
||||
|
||||
def redirect_to_login(next_target: str | None = None) -> FlaskResponse:
|
||||
"""Return a redirect response to the login view, preserving target URL.
|
||||
@@ -396,8 +379,6 @@ def get_form_data(
|
||||
json_data = form_data["queries"][0] if "queries" in form_data else {}
|
||||
form_data.update(json_data)
|
||||
|
||||
form_data = {k: v for k, v in form_data.items() if k not in REJECTED_FORM_DATA_KEYS}
|
||||
|
||||
# When a slice_id is present, load from DB and override
|
||||
# the form_data from the DB with the other form_data provided
|
||||
slice_id = form_data.get("slice_id") or slice_id
|
||||
|
||||
@@ -234,10 +234,6 @@ class TestImportChartsCommand(SupersetTestCase):
|
||||
"annotation_layers": [],
|
||||
"color_picker": {"a": 1, "b": 135, "g": 122, "r": 0},
|
||||
"datasource": dataset.uid if dataset else None,
|
||||
"js_columns": ["color"],
|
||||
"js_data_mutator": "data => data.map(d => ({\\n ...d,\\n color: colors.hexToRGB(d.extraProps.color)\\n}));", # noqa: E501
|
||||
"js_onclick_href": "",
|
||||
"js_tooltip": "",
|
||||
"line_column": "path_json",
|
||||
"line_type": "json",
|
||||
"line_width": 150,
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
"datasource": "12__table",
|
||||
"filters": [],
|
||||
"having": "",
|
||||
"js_columns": [
|
||||
"color"
|
||||
],
|
||||
"js_datapoint_mutator": "d => {\n return {\n ...d,\n color: colors.hexToRGB(d.extraProps.color),\n }\n}",
|
||||
"js_onclick_href": "",
|
||||
"js_tooltip": "",
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"reverse_long_lat": false,
|
||||
"row_limit": 5000,
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
"datasource": "12__table",
|
||||
"filters": [],
|
||||
"having": "",
|
||||
"js_columns": [
|
||||
"color"
|
||||
],
|
||||
"js_datapoint_mutator": "d => {\n return {\n ...d,\n color: colors.hexToRGB(d.extraProps.color),\n }\n}",
|
||||
"js_onclick_href": "",
|
||||
"js_tooltip": "",
|
||||
"line_column": "path_json",
|
||||
"line_type": "json",
|
||||
"line_width": 150,
|
||||
|
||||
@@ -587,10 +587,6 @@ chart_config: dict[str, Any] = {
|
||||
"params": {
|
||||
"color_picker": {"a": 1, "b": 135, "g": 122, "r": 0},
|
||||
"datasource": "12__table",
|
||||
"js_columns": ["color"],
|
||||
"js_data_mutator": r"data => data.map(d => ({\n ...d,\n color: colors.hexToRGB(d.extraProps.color)\n}));", # noqa: E501
|
||||
"js_onclick_href": "",
|
||||
"js_tooltip": "",
|
||||
"line_column": "path_json",
|
||||
"line_type": "json",
|
||||
"line_width": 150,
|
||||
|
||||
@@ -914,7 +914,10 @@ class TestBaseDeckGLViz(SupersetTestCase):
|
||||
assert result == []
|
||||
|
||||
def test_get_js_columns(self):
|
||||
form_data = load_fixture("deck_path_form_data.json")
|
||||
form_data = {
|
||||
**load_fixture("deck_path_form_data.json"),
|
||||
"js_columns": ["color"],
|
||||
}
|
||||
datasource = self.get_datasource_mock()
|
||||
mock_d = {"a": "dummy1", "b": "dummy2", "c": "dummy3"}
|
||||
test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
|
||||
|
||||
@@ -344,18 +344,15 @@ def test_resolve_deck_gl_columns_geojson():
|
||||
assert resolve_deck_gl_columns(form_data) == ["geom_col"]
|
||||
|
||||
|
||||
def test_resolve_deck_gl_columns_with_dimension_and_js_columns():
|
||||
def test_resolve_deck_gl_columns_with_dimension():
|
||||
form_data = {
|
||||
"spatial": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
|
||||
"dimension": "category",
|
||||
"js_columns": ["name", "value"],
|
||||
}
|
||||
cols = resolve_deck_gl_columns(form_data)
|
||||
assert "lon" in cols
|
||||
assert "lat" in cols
|
||||
assert "category" in cols
|
||||
assert "name" in cols
|
||||
assert "value" in cols
|
||||
|
||||
|
||||
def test_resolve_deck_gl_columns_deduplicates():
|
||||
@@ -371,13 +368,6 @@ def test_resolve_deck_gl_columns_empty():
|
||||
assert resolve_deck_gl_columns({}) == []
|
||||
|
||||
|
||||
def test_resolve_deck_gl_columns_ignores_non_string_js_columns():
|
||||
form_data = {
|
||||
"js_columns": [42, None, "valid_col"],
|
||||
}
|
||||
assert resolve_deck_gl_columns(form_data) == ["valid_col"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_query_dicts_from_form_data — Deck.gl branch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Tests for superset.views.utils module"""
|
||||
|
||||
from superset.views.utils import (
|
||||
get_form_data,
|
||||
JS_CONTROL_FORM_DATA_KEYS,
|
||||
REJECTED_FORM_DATA_KEYS,
|
||||
)
|
||||
|
||||
|
||||
def test_rejected_form_data_keys_cover_all_js_control_keys() -> None:
|
||||
"""
|
||||
With ENABLE_JAVASCRIPT_CONTROLS disabled (the default), every form_data key
|
||||
that is later executed as JavaScript by the deck.gl charts must be rejected.
|
||||
|
||||
This guards against a new ``sandboxedEval(fd.<key>)`` call site being added
|
||||
without also adding its key to the strip list.
|
||||
"""
|
||||
# The test app keeps ENABLE_JAVASCRIPT_CONTROLS at its default (off).
|
||||
assert set(JS_CONTROL_FORM_DATA_KEYS) <= set(REJECTED_FORM_DATA_KEYS)
|
||||
|
||||
|
||||
def test_get_form_data_strips_js_control_keys() -> None:
|
||||
"""get_form_data drops all JS-executed keys when the flag is disabled."""
|
||||
initial_form_data = dict.fromkeys(JS_CONTROL_FORM_DATA_KEYS, "data => data")
|
||||
initial_form_data["viz_type"] = "deck_geojson"
|
||||
|
||||
form_data, _ = get_form_data(initial_form_data=initial_form_data)
|
||||
|
||||
for key in JS_CONTROL_FORM_DATA_KEYS:
|
||||
assert key not in form_data
|
||||
# Non-JS keys are preserved.
|
||||
assert form_data["viz_type"] == "deck_geojson"
|
||||
Reference in New Issue
Block a user