Compare commits

...

1 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
a39e3f10be feat: sandbox plugin 2026-05-11 10:32:04 +03:00
35 changed files with 2244 additions and 7 deletions

View File

@@ -66,6 +66,7 @@
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
"@superset-ui/plugin-chart-sandpack": "file:./plugins/plugin-chart-sandpack",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
@@ -2713,6 +2714,206 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
"integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz",
"integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.42.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.42.1.tgz",
"integrity": "sha512-ToN3oFc0nsxNUYVF5P0ztLgbC4UPPjPtA9aKYhkOKQaZASpOUo6ISXyQLP66ctVwlDc+j6Jv0uK5IFALkiXztg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@codesandbox/nodebox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@codesandbox/nodebox/-/nodebox-0.1.8.tgz",
"integrity": "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==",
"license": "SEE LICENSE IN ./LICENSE",
"dependencies": {
"outvariant": "^1.4.0",
"strict-event-emitter": "^0.4.3"
}
},
"node_modules/@codesandbox/sandpack-client": {
"version": "2.19.8",
"resolved": "https://registry.npmjs.org/@codesandbox/sandpack-client/-/sandpack-client-2.19.8.tgz",
"integrity": "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==",
"license": "Apache-2.0",
"dependencies": {
"@codesandbox/nodebox": "0.1.8",
"buffer": "^6.0.3",
"dequal": "^2.0.2",
"mime-db": "^1.52.0",
"outvariant": "1.4.0",
"static-browser-server": "1.0.3"
}
},
"node_modules/@codesandbox/sandpack-client/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/@codesandbox/sandpack-react": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/@codesandbox/sandpack-react/-/sandpack-react-2.20.0.tgz",
"integrity": "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ==",
"license": "Apache-2.0",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.1.3",
"@codemirror/lang-css": "^6.0.1",
"@codemirror/lang-html": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.3.2",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.1",
"@codesandbox/sandpack-client": "^2.19.8",
"@lezer/highlight": "^1.1.3",
"@react-hook/intersection-observer": "^3.1.1",
"@stitches/core": "^1.2.6",
"anser": "^2.1.1",
"clean-set": "^1.1.2",
"dequal": "^2.0.2",
"escape-carriage": "^1.3.1",
"lz-string": "^1.4.4",
"react-devtools-inline": "4.4.0",
"react-is": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@codesandbox/sandpack-react/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -6850,6 +7051,63 @@
"dev": true,
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz",
"integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@loaders.gl/3d-tiles": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@loaders.gl/3d-tiles/-/3d-tiles-4.3.4.tgz",
@@ -7444,6 +7702,12 @@
"pbf": "bin/pbf"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@math.gl/core": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz",
@@ -8642,6 +8906,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@open-draft/deferred-promise": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
"license": "MIT"
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.62.0.tgz",
@@ -9450,6 +9720,28 @@
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
"license": "MIT"
},
"node_modules/@react-hook/intersection-observer": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-hook/intersection-observer/-/intersection-observer-3.1.2.tgz",
"integrity": "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==",
"license": "MIT",
"dependencies": {
"@react-hook/passive-layout-effect": "^1.2.0",
"intersection-observer": "^0.10.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-hook/passive-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
"integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-spring/animated": {
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
@@ -10032,6 +10324,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@stitches/core": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
"license": "MIT"
},
"node_modules/@storybook/addon-actions": {
"version": "8.6.18",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.18.tgz",
@@ -12197,6 +12495,10 @@
"resolved": "plugins/plugin-chart-point-cluster-map",
"link": true
},
"node_modules/@superset-ui/plugin-chart-sandpack": {
"resolved": "plugins/plugin-chart-sandpack",
"link": true
},
"node_modules/@superset-ui/plugin-chart-table": {
"resolved": "plugins/plugin-chart-table",
"link": true
@@ -16393,7 +16695,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz",
"integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==",
"dev": true,
"license": "MIT"
},
"node_modules/ansi-colors": {
@@ -18470,6 +18771,12 @@
"node": ">=0.10.0"
}
},
"node_modules/clean-set": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/clean-set/-/clean-set-1.1.2.tgz",
"integrity": "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==",
"license": "MIT"
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -20114,6 +20421,12 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
@@ -20650,6 +20963,19 @@
"node": ">=0.8"
}
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/d3": {
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
@@ -21717,7 +22043,6 @@
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -22294,6 +22619,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -22301,6 +22642,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -22416,6 +22781,12 @@
"node": ">=6"
}
},
"node_modules/escape-carriage": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
"integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
"license": "MIT"
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -23242,6 +23613,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -23363,6 +23749,16 @@
"node": ">= 0.6"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -23545,6 +23941,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -27102,6 +27507,13 @@
"node": ">=10.13.0"
}
},
"node_modules/intersection-observer": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz",
"integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==",
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
"license": "W3C-20150513"
},
"node_modules/interweave": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.1.tgz",
@@ -36272,7 +36684,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -36759,6 +37170,12 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/ngeohash": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.3.tgz",
@@ -38148,6 +38565,12 @@
"node": ">=0.10.0"
}
},
"node_modules/outvariant": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz",
"integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
"license": "MIT"
},
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -41145,6 +41568,15 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/react-devtools-inline": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz",
"integrity": "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==",
"license": "MIT",
"dependencies": {
"es6-symbol": "^3"
}
},
"node_modules/react-diff-viewer-continued": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-4.2.2.tgz",
@@ -44424,6 +44856,18 @@
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
"license": "MIT"
},
"node_modules/static-browser-server": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz",
"integrity": "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==",
"license": "Apache-2.0",
"dependencies": {
"@open-draft/deferred-promise": "^2.1.0",
"dotenv": "^16.0.3",
"mime-db": "^1.52.0",
"outvariant": "^1.3.0"
}
},
"node_modules/static-eval": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz",
@@ -44711,6 +45155,12 @@
"bare-events": "^2.2.0"
}
},
"node_modules/strict-event-emitter": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz",
"integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -45000,6 +45450,12 @@
"webpack": "^5.27.0"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/style-to-object": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz",
@@ -46615,6 +47071,12 @@
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -47844,6 +48306,12 @@
"node": ">=0.4.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -50796,6 +51264,27 @@
}
}
},
"plugins/plugin-chart-sandpack": {
"name": "@superset-ui/plugin-chart-sandpack",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"@codesandbox/sandpack-react": "^2.20.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24",
"jest": "^30.3.0"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"plugins/plugin-chart-table": {
"name": "@superset-ui/plugin-chart-table",
"version": "0.20.3",
@@ -50893,7 +51382,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "^9.3.2",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",

View File

@@ -147,6 +147,7 @@
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-sandpack": "file:./plugins/plugin-chart-sandpack",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",

View File

@@ -50,6 +50,7 @@ export enum VizType {
PivotTable = 'pivot_table_v2',
Radar = 'radar',
Rose = 'rose',
Sandpack = 'sandpack',
Sankey = 'sankey_v2',
Scatter = 'echarts_timeseries_scatter',
SmoothLine = 'echarts_timeseries_smooth',

View File

@@ -0,0 +1,46 @@
{
"name": "@superset-ui/plugin-chart-sandpack",
"version": "0.1.0",
"description": "Superset Chart - Render arbitrary Sandpack-powered apps against query data",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "https://github.com/apache/superset.git",
"directory": "superset-frontend/plugins/plugin-chart-sandpack"
},
"keywords": [
"superset",
"sandpack"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache/superset/issues"
},
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-sandpack#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@codesandbox/sandpack-react": "^2.20.0"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@apache-superset/core": "*",
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24",
"jest": "^30.3.0"
}
}

View File

@@ -0,0 +1,69 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@apache-superset/core/theme';
import { useMemo } from 'react';
import { SandpackViewer } from './components/Sandpack/SandpackViewer';
import { DEFAULT_APP_CODE, DEFAULT_DEPENDENCIES } from './consts';
import { SandpackProps, SandpackStylesProps } from './types';
const Styles = styled.div<SandpackStylesProps>`
height: ${({ height }) => height}px;
width: ${({ width }) => width}px;
overflow: hidden;
`;
function safeParseDependencies(raw?: string): Record<string, string> {
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, string>;
}
} catch {
// fall through
}
return {};
}
export default function SandpackChart(props: SandpackProps) {
const { data, height, width, formData } = props;
const appCode = formData.appCode || DEFAULT_APP_CODE;
const template = formData.template || 'react';
const layout = formData.layout || 'split';
const showNavigator = formData.showNavigator ?? false;
const dependencies = useMemo(
() => safeParseDependencies(formData.dependencies || DEFAULT_DEPENDENCIES),
[formData.dependencies],
);
return (
<Styles height={height} width={width}>
<SandpackViewer
appCode={appCode}
data={data}
dependencies={dependencies}
template={template}
layout={layout}
showNavigator={showNavigator}
height={height}
/>
</Styles>
);
}

View File

@@ -0,0 +1,25 @@
/**
* 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.
*/
export {
CodeEditor,
type CodeEditorProps,
type CodeEditorMode,
type CodeEditorTheme,
} from '@superset-ui/core/components';

View File

@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode } from 'react';
interface ControlHeaderProps {
children: ReactNode;
}
export const ControlHeader = ({
children,
}: ControlHeaderProps): JSX.Element => (
<div className="ControlHeader">
<div className="pull-left">{children}</div>
</div>
);

View File

@@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { debounce } from 'lodash';
import { Constants } from '@superset-ui/core/components';
export const debounceFunc = debounce(
(func: (val: string) => void, source: string) => func(source),
Constants.SLOW_DEBOUNCE,
);
export const DEFAULT_APP_CODE = `import data from './data.json';
export default function App() {
return (
<div style={{ fontFamily: 'system-ui', padding: 16 }}>
<h2>Rows: {data.length}</h2>
<ul>
{data.slice(0, 20).map((row, i) => (
<li key={i}>{JSON.stringify(row)}</li>
))}
</ul>
</div>
);
}
`;
export const DEFAULT_DEPENDENCIES = `{
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

View File

@@ -0,0 +1,20 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/prefer-default-export
export { default as SandpackChartPlugin } from './plugin';

View File

@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
buildQueryContext,
normalizeOrderBy,
QueryFormData,
} from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
orderby: normalizeOrderBy(baseQueryObject).orderby,
},
]);
}

View File

@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ControlPanelConfig,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import { appCodeControlSetItem } from './controls/appCode';
import { dependenciesControlSetItem } from './controls/dependencies';
import {
layoutControlSetItem,
showNavigatorControlSetItem,
templateControlSetItem,
} from './controls/options';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['groupby'],
['metrics'],
['adhoc_filters'],
['row_limit'],
],
},
{
label: t('App'),
expanded: true,
controlSetRows: [
[templateControlSetItem],
[appCodeControlSetItem],
[dependenciesControlSetItem],
],
},
{
label: t('Display'),
expanded: false,
controlSetRows: [[layoutControlSetItem], [showNavigatorControlSetItem]],
},
],
formDataOverrides: formData => ({
...formData,
groupby: getStandardizedControls().popAllColumns(),
metrics: getStandardizedControls().popAllMetrics(),
}),
};
export default config;

View File

@@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ControlSetItem,
CustomControlConfig,
sharedControls,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/theme';
import { InfoTooltip } from '@superset-ui/core/components';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
import { debounceFunc, DEFAULT_APP_CODE } from '../../consts';
interface AppCodeControlProps {
value: string;
}
const AppCodeControl = (props: CustomControlConfig<AppCodeControlProps>) => {
const theme = useTheme();
const val = String(
props?.value ? props?.value : props?.default ? props?.default : '',
);
return (
<div>
<ControlHeader>
<div>
{typeof props.label === 'function' ? null : props.label}
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={t(
'Query results are exposed to the app as `./data.json`. ' +
'Import it from your entry file to render the data.',
)}
/>
</div>
</ControlHeader>
<CodeEditor
theme="dark"
mode="javascript"
value={val}
onChange={source => {
debounceFunc(props.onChange, source || '');
}}
/>
</div>
);
};
export const appCodeControlSetItem: ControlSetItem = {
name: 'appCode',
config: {
...sharedControls.entity,
type: AppCodeControl,
label: t('App Code'),
description: t(
'Source code for the Sandpack app entry file. The query result is ' +
'available as `./data.json`.',
),
default: DEFAULT_APP_CODE,
isInt: false,
renderTrigger: true,
valueKey: null,
validators: [validateNonEmpty],
mapStateToProps: ({ form_data }) => ({
value: form_data?.appCode ?? form_data?.app_code,
}),
},
};

View File

@@ -0,0 +1,69 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ControlSetItem,
CustomControlConfig,
sharedControls,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
import { debounceFunc, DEFAULT_DEPENDENCIES } from '../../consts';
interface DependenciesControlProps {
value: string;
}
const DependenciesControl = (
props: CustomControlConfig<DependenciesControlProps>,
) => (
<div>
<ControlHeader>
<div>{typeof props.label === 'function' ? null : props.label}</div>
</ControlHeader>
<CodeEditor
theme="dark"
mode="json"
value={props.value || ''}
onChange={source => {
debounceFunc(props.onChange, source || '');
}}
/>
</div>
);
export const dependenciesControlSetItem: ControlSetItem = {
name: 'dependencies',
config: {
...sharedControls.entity,
type: DependenciesControl,
label: t('NPM Dependencies'),
description: t(
'JSON object of npm packages and versions made available to the app.',
),
default: DEFAULT_DEPENDENCIES,
isInt: false,
renderTrigger: true,
valueKey: null,
validators: [],
mapStateToProps: ({ form_data }) => ({
value: form_data?.dependencies,
}),
},
};

View File

@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ControlSetItem } from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
export const templateControlSetItem: ControlSetItem = {
name: 'template',
config: {
type: 'SelectControl',
label: t('Sandpack Template'),
description: t('Sandpack runtime template used to bundle the app.'),
default: 'react',
renderTrigger: true,
clearable: false,
choices: [
['react', t('React (JS)')],
['react-ts', t('React (TypeScript)')],
['vanilla', t('Vanilla JS')],
['vanilla-ts', t('Vanilla TypeScript')],
],
},
};
export const layoutControlSetItem: ControlSetItem = {
name: 'layout',
config: {
type: 'SelectControl',
label: t('Layout'),
description: t('Which Sandpack panes to show inside the chart frame.'),
default: 'preview',
renderTrigger: true,
clearable: false,
choices: [
['preview', t('Preview only')],
['split', t('Editor + preview')],
['editor', t('Editor only')],
],
},
};
export const showNavigatorControlSetItem: ControlSetItem = {
name: 'showNavigator',
config: {
type: 'CheckboxControl',
label: t('Show navigator'),
description: t('Display the Sandpack URL/navigator bar above the preview.'),
default: false,
renderTrigger: true,
},
};

View File

@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import thumbnail from '../images/thumbnail.png';
import thumbnailDark from '../images/thumbnail-dark.png';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
export default class SandpackChartPlugin extends ChartPlugin {
constructor() {
const metadata = new ChartMetadata({
description: t(
'Render a Sandpack-powered React or JS app against the query result. ' +
'The dataset is exposed to the app as `./data.json`.',
),
name: t('Sandpack App'),
tags: [t('Advanced'), t('Custom'), t('Experimental')],
thumbnail,
thumbnailDark,
behaviors: [Behavior.InteractiveChart],
});
super({
buildQuery,
controlPanel,
loadChart: () => import('../SandpackChart'),
metadata,
transformProps,
});
}
}

View File

@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps, TimeseriesDataRecord } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queriesData } = chartProps;
const data = queriesData[0].data as TimeseriesDataRecord[];
return {
width,
height,
data,
formData,
};
}

View File

@@ -0,0 +1,54 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
QueryFormData,
QueryFormMetric,
TimeseriesDataRecord,
} from '@superset-ui/core';
export type SandpackTemplate = 'react' | 'react-ts' | 'vanilla' | 'vanilla-ts';
export type SandpackLayout = 'preview' | 'split' | 'editor';
export interface SandpackStylesProps {
height: number;
width: number;
}
interface SandpackCustomizeProps {
appCode?: string;
dependencies?: string;
template?: SandpackTemplate;
layout?: SandpackLayout;
showNavigator?: boolean;
}
export type SandpackQueryFormData = QueryFormData &
SandpackStylesProps &
SandpackCustomizeProps & {
metrics?: QueryFormMetric[] | null;
groupby?: QueryFormMetric[] | null;
all_columns?: QueryFormMetric[] | null;
};
export type SandpackProps = SandpackStylesProps &
SandpackCustomizeProps & {
data: TimeseriesDataRecord[];
formData: SandpackQueryFormData;
};

View File

@@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SandpackChartPlugin } from '../src';
describe('@superset-ui/plugin-chart-sandpack', () => {
test('exists', () => {
expect(SandpackChartPlugin).toBeDefined();
});
});

View File

@@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "../..",
"outDir": "lib",
"rootDir": "src",
"declarationDir": "lib"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
declare module '*.png' {
const value: any;
export default value;
}
declare module '*.jpg';

View File

@@ -85,6 +85,7 @@ import {
} from 'src/chartCustomizations/components';
import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table';
import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
import { SandpackChartPlugin } from '@superset-ui/plugin-chart-sandpack';
import { ChartCustomizationPlugins, FilterPlugins } from 'src/constants';
import AgGridTableChartPlugin from '@superset-ui/plugin-chart-ag-grid-table';
import TimeTableChartPlugin from '../TimeTable';
@@ -195,6 +196,7 @@ export default class MainPreset extends Preset {
new EchartsTreeChartPlugin().configure({ key: VizType.Tree }),
new EchartsSunburstChartPlugin().configure({ key: VizType.Sunburst }),
new HandlebarsChartPlugin().configure({ key: VizType.Handlebars }),
new SandpackChartPlugin().configure({ key: VizType.Sandpack }),
new EchartsBubbleChartPlugin().configure({ key: VizType.Bubble }),
new CartodiagramPlugin({
defaultLayers: [

View File

@@ -95,6 +95,7 @@
{ "path": "./plugins/plugin-chart-echarts" },
{ "path": "./plugins/plugin-chart-point-cluster-map" },
{ "path": "./plugins/plugin-chart-handlebars" },
{ "path": "./plugins/plugin-chart-sandpack" },
{ "path": "./plugins/plugin-chart-pivot-table" },
{ "path": "./plugins/plugin-chart-table" },
{ "path": "./plugins/plugin-chart-word-cloud" }

View File

@@ -96,6 +96,8 @@ Available Resources:
Available Prompts:
- quickstart: Interactive guide for getting started with the MCP service
- create_chart_guided: Step-by-step chart creation wizard
- create_sandpack_app: Guidance for authoring chart_type="sandpack" apps
(./data.json contract, template/dependencies, common patterns)
IMPORTANT - Using Saved Metrics vs Columns:
When get_dataset_info returns a dataset, it includes both 'columns' and 'metrics'.
@@ -217,6 +219,16 @@ Chart Types You Can CREATE with generate_chart/generate_explore_link:
Requires handlebars_template with Handlebars HTML template string.
Supports query_mode="aggregate" (with metrics/groupby) or "raw" (with columns).
Data available as {{{{data}}}} array; helpers: dateFormat, formatNumber, stringify.
- chart_type="sandpack": In-browser Sandpack app rendered against the query result.
Requires app_code (entry-file source). Inside the sandbox the dataset is exposed
as ./data.json — import it with `import data from './data.json';`.
Pick template: 'react' (default, JS, entry App.js with default export),
'react-ts' (TS, App.tsx), 'vanilla' (index.js), or 'vanilla-ts' (index.ts).
Add npm packages via dependencies={{...}} (e.g. {{"recharts": "^2.12.0"}}).
Defaults to query_mode="raw" with columns; switch to "aggregate" with metrics
for grouped data. Use this when the user wants a custom interactive UI that
the built-in chart types can't express. Call get_chart_type_schema(chart_type
="sandpack") for the full schema and runnable examples.
Time grain for temporal x-axis (time_grain parameter):
- PT1H (hourly), P1D (daily), P1W (weekly), P1M (monthly), P1Y (yearly)

View File

@@ -37,6 +37,7 @@ from superset.mcp_service.chart.schemas import (
MixedTimeseriesChartConfig,
PieChartConfig,
PivotTableChartConfig,
SandpackChartConfig,
TableChartConfig,
XYChartConfig,
)
@@ -315,6 +316,7 @@ def map_config_to_form_data(
| PivotTableChartConfig
| MixedTimeseriesChartConfig
| HandlebarsChartConfig
| SandpackChartConfig
| BigNumberChartConfig,
dataset_id: int | str | None = None,
) -> Dict[str, Any]:
@@ -331,6 +333,8 @@ def map_config_to_form_data(
return map_mixed_timeseries_config(config, dataset_id=dataset_id)
elif isinstance(config, HandlebarsChartConfig):
return map_handlebars_config(config)
elif isinstance(config, SandpackChartConfig):
return map_sandpack_config(config)
elif isinstance(config, BigNumberChartConfig):
if config.show_trendline and config.temporal_column:
if not is_column_truly_temporal(config.temporal_column, dataset_id):
@@ -824,6 +828,41 @@ def map_handlebars_config(config: HandlebarsChartConfig) -> Dict[str, Any]:
return form_data
def map_sandpack_config(config: SandpackChartConfig) -> Dict[str, Any]:
"""Map sandpack chart config to Superset form_data.
Mirrors the camelCase keys the frontend control panel writes:
``appCode``, ``dependencies`` (JSON-stringified), ``template``, ``layout``,
``showNavigator``.
"""
form_data: Dict[str, Any] = {
"viz_type": "sandpack",
"appCode": config.app_code,
"template": config.template,
"layout": config.layout,
"showNavigator": config.show_navigator,
"row_limit": config.row_limit,
}
if config.dependencies:
form_data["dependencies"] = json.dumps(config.dependencies)
if config.query_mode == "raw":
form_data["query_mode"] = "raw"
if config.columns:
form_data["all_columns"] = [col.name for col in config.columns]
else:
form_data["query_mode"] = "aggregate"
if config.groupby:
form_data["groupby"] = [col.name for col in config.groupby]
if config.metrics:
form_data["metrics"] = [create_metric_object(col) for col in config.metrics]
_add_adhoc_filters(form_data, config.filters)
return form_data
def map_pivot_table_config(config: PivotTableChartConfig) -> Dict[str, Any]:
"""Map pivot table config to Superset form_data."""
if not config.rows:
@@ -1111,6 +1150,17 @@ def _handlebars_chart_what(config: HandlebarsChartConfig) -> str:
return "Handlebars Chart"
def _sandpack_chart_what(config: SandpackChartConfig) -> str:
"""Build the 'what' portion for a sandpack chart name."""
if config.query_mode == "raw" and config.columns:
cols = ", ".join(col.name for col in config.columns[:3])
return f"Sandpack ({cols})"
if config.metrics:
metrics = ", ".join(col.name for col in config.metrics[:3])
return f"Sandpack ({metrics})"
return "Sandpack App"
def _big_number_chart_what(config: BigNumberChartConfig) -> str:
"""Build the 'what' portion for a big number chart name.
@@ -1135,6 +1185,7 @@ def generate_chart_name(
| PivotTableChartConfig
| MixedTimeseriesChartConfig
| HandlebarsChartConfig
| SandpackChartConfig
| BigNumberChartConfig,
dataset_name: str | None = None,
) -> str:
@@ -1169,6 +1220,9 @@ def generate_chart_name(
elif isinstance(config, HandlebarsChartConfig):
what = _handlebars_chart_what(config)
context = _summarize_filters(getattr(config, "filters", None))
elif isinstance(config, SandpackChartConfig):
what = _sandpack_chart_what(config)
context = _summarize_filters(getattr(config, "filters", None))
elif isinstance(config, BigNumberChartConfig):
what = _big_number_chart_what(config)
context = _summarize_filters(getattr(config, "filters", None))
@@ -1203,6 +1257,8 @@ def _resolve_viz_type(config: Any) -> str:
return "mixed_timeseries"
elif chart_type == "handlebars":
return "handlebars"
elif chart_type == "sandpack":
return "sandpack"
elif chart_type == "big_number":
show_trendline = getattr(config, "show_trendline", False)
temporal_column = getattr(config, "temporal_column", None)
@@ -1292,6 +1348,10 @@ def analyze_chart_semantics(chart: Any | None, config: Any) -> ChartSemantics:
"Renders data using a custom Handlebars HTML template for "
"fully flexible layouts like KPI cards, leaderboards, and reports"
),
"sandpack": (
"Runs an in-browser Sandpack app (React/JS) bundled at view time, "
"with the query result available inside the sandbox as ./data.json"
),
"big_number": (
"Displays a key metric with a trendline showing "
"how the value changes over time"

View File

@@ -19,3 +19,4 @@
# Import to register prompts when module is imported
from . import create_chart_guided # noqa: F401
from . import create_sandpack_app # noqa: F401

View File

@@ -0,0 +1,152 @@
# 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.
"""
Guided prompt for authoring chart_type="sandpack" apps.
"""
from superset_core.mcp.decorators import prompt
@prompt("create_sandpack_app")
async def create_sandpack_app_prompt(
template: str = "react",
) -> str:
"""Guidance for writing a Sandpack chart app.
Args:
template: Sandpack template — react, react-ts, vanilla, vanilla-ts.
"""
valid_templates = ("react", "react-ts", "vanilla", "vanilla-ts")
template = template if template in valid_templates else "react"
entry_file = {
"react": "App.js (default export of a React component)",
"react-ts": "App.tsx (default export of a React component)",
"vanilla": "index.js (top-level script — write to document.body)",
"vanilla-ts": "index.ts (top-level script — write to document.body)",
}[template]
return f"""**Authoring a Sandpack Chart**
Sandpack charts let you ship a small, self-contained app rendered against the
query result. The chart frame mounts the Sandpack bundler at view time.
## The contract — read this first
1. **Data is exposed as `./data.json`** inside the sandbox. The shape mirrors
what the query returns (one row per array element, snake_case-style keys).
2. **`app_code` is the entry file**. For `{template}` that file is **{entry_file}**.
3. **Pick `query_mode` based on what your app needs**:
- `query_mode="raw"` (default for sandpack) → row-level data via `columns`.
- `query_mode="aggregate"` → grouped data via `metrics`/`groupby`.
4. **Add npm packages with `dependencies`** (a `{{name: semver}}` object) only
when you actually `import` them. React/ReactDOM ship with the react templates.
## Workflow
### Step 1 — Pick a dataset
Call `list_datasets` and `get_dataset_info(dataset_id)`. Note the column names
exactly as Superset returns them; `./data.json` will use the same keys.
### Step 2 — Decide raw vs. aggregate
- "Show me a list / a table / row-level details" → `query_mode="raw"`,
set `columns: [...]`.
- "Show me a chart of grouped numbers" → `query_mode="aggregate"`,
set `metrics: [{{"name": "...", "aggregate": "SUM"}}]` and optionally `groupby`.
In aggregate mode, metric keys in `./data.json` look like `"SUM(revenue)"`.
### Step 3 — Write `app_code`
Always start with:
```js
import data from './data.json';
```
Then default-export a React component (for react/react-ts) or write to
`document.body` (vanilla). Keep it one file when you can.
### Step 4 — Submit via `generate_chart` or `generate_explore_link`
```json
{{
"dataset_id": <id>,
"config": {{
"chart_type": "sandpack",
"template": "{template}",
"query_mode": "raw",
"columns": [{{"name": "<column>"}}],
"app_code": "...",
"dependencies": {{"recharts": "^2.12.0"}}
}}
}}
```
## Worked examples
### Minimal list (raw mode, react)
```js
import data from './data.json';
export default function App() {{
return (
<ul>
{{data.map((row, i) => (
<li key={{i}}>{{JSON.stringify(row)}}</li>
))}}
</ul>
);
}}
```
### Grouped bar chart (aggregate mode, react + recharts)
- `metrics: [{{"name": "revenue", "aggregate": "SUM"}}]`, `groupby: [{{"name": "region"}}]`
- `dependencies: {{"recharts": "^2.12.0"}}`
```js
import data from './data.json';
import {{ BarChart, Bar, XAxis, YAxis, Tooltip }} from 'recharts';
export default function App() {{
return (
<BarChart width={{500}} height={{300}} data={{data}}>
<XAxis dataKey="region" />
<YAxis />
<Tooltip />
<Bar dataKey="SUM(revenue)" />
</BarChart>
);
}}
```
## Common pitfalls
- **Wrong key in aggregate mode**: in aggregate mode keys are `"SUM(col)"`,
`"COUNT(col)"`, etc. — not the bare column name. Inspect a row first.
- **Forgot `import data from './data.json'`**: the bundle still loads, but the
app has no data. Always import the JSON.
- **Used a package without declaring it**: every imported package needs an entry
in `dependencies` (except react/react-dom on react templates).
- **Multi-file apps**: only `app_code` is editable from MCP today. If you need
multiple files, inline them into one entry file or open the chart in Explore
to edit further.
- **Heavy bundles**: Sandpack rebundles on every prop change. Avoid pulling in
charting kitchen-sink libraries when a small one will do.
## Verification
After calling `generate_chart` or `generate_explore_link`, open the URL it
returns in a browser. If the preview shows a Sandpack error overlay, fix the
`app_code` (syntax, missing dependency, wrong data key) and re-call the tool —
the chart's `form_data` is rewritten on each call.
"""

View File

@@ -1054,6 +1054,137 @@ class HandlebarsChartConfig(UnknownFieldCheckMixin):
return self
class SandpackChartConfig(UnknownFieldCheckMixin):
model_config = ConfigDict(extra="ignore")
chart_type: Literal["sandpack"] = Field(
...,
description=(
"Chart type discriminator - MUST be 'sandpack' for in-browser apps "
"rendered with the Sandpack bundler. The chart frame mounts a "
"Sandpack runtime; the dataset is exposed inside the sandbox as "
"`./data.json`. Use this for fully custom interactive UIs (small "
"React apps, dashboards-in-a-cell, bespoke widgets) when the "
"built-in chart types are too restrictive."
),
)
app_code: str = Field(
...,
description=(
"Source code for the entry file of the Sandpack app. Import the "
"query result with `import data from './data.json';` (default-export "
"the React component for `react`/`react-ts` templates). Keep it a "
"single file when possible — Sandpack rebundles on every edit."
),
min_length=1,
max_length=100000,
validation_alias=AliasChoices("app_code", "appCode"),
)
template: Literal["react", "react-ts", "vanilla", "vanilla-ts"] = Field(
"react",
description=(
"Sandpack runtime template. 'react' = JS React (entry: App.js, "
"default export). 'react-ts' = TS React (entry: App.tsx). "
"'vanilla' / 'vanilla-ts' = plain JS/TS (entry: index.js / "
"index.ts). Pick the one matching the syntax used in app_code."
),
)
dependencies: Dict[str, str] | None = Field(
None,
description=(
"Optional npm dependencies as {name: semver} pairs. React/ReactDOM "
"ship with the react templates; only add what your app additionally "
"imports (e.g. {'recharts': '^2.12.0'})."
),
)
layout: Literal["preview", "split", "editor"] = Field(
"preview",
description=(
"Which Sandpack panes are visible in the chart frame. 'preview' = "
"rendered output only (recommended for finished charts), 'split' = "
"editor + preview side-by-side, 'editor' = code only."
),
)
show_navigator: bool = Field(
False,
description="Show the Sandpack URL bar above the preview pane.",
validation_alias=AliasChoices("show_navigator", "showNavigator"),
)
query_mode: Literal["aggregate", "raw"] = Field(
"raw",
description=(
"Query mode: 'raw' returns individual rows (default for sandpack — "
"most apps want row-level data), 'aggregate' groups with metrics."
),
)
columns: list[ColumnRef] | None = Field(
None,
description=(
"Columns to fetch in raw mode (query_mode='raw'). These become the "
"fields available on each object in `./data.json`."
),
)
groupby: list[ColumnRef] | None = Field(
None,
description=(
"Group-by columns in aggregate mode (query_mode='aggregate'). "
"Become the dimensions for aggregation."
),
validation_alias=AliasChoices("groupby", "group_by"),
)
metrics: list[ColumnRef] | None = Field(
None,
description=(
"Metrics to aggregate in aggregate mode. Each must have an "
"aggregate function (SUM, COUNT, AVG, ...)."
),
)
filters: list[FilterConfig] | None = Field(
None, description="Filters to apply"
)
row_limit: int = Field(
1000,
description="Maximum number of rows passed to the app",
ge=1,
le=50000,
)
@model_validator(mode="after")
def validate_query_fields(self) -> "SandpackChartConfig":
"""Enforce that the right query fields are present for the chosen mode."""
if self.query_mode == "raw":
if not self.columns:
raise ValueError(
"Sandpack chart in 'raw' query mode requires 'columns'. "
"Specify which columns to expose in ./data.json."
)
if self.metrics:
raise ValueError(
"Sandpack chart in 'raw' query mode does not use 'metrics'. "
"Remove 'metrics' or switch to 'aggregate' query mode."
)
if self.groupby:
raise ValueError(
"Sandpack chart in 'raw' query mode does not use 'groupby'. "
"Remove 'groupby' or switch to 'aggregate' query mode."
)
else:
if not self.metrics:
raise ValueError(
"Sandpack chart in 'aggregate' query mode requires 'metrics'. "
"Specify at least one metric with an aggregate function."
)
missing_agg = [m.name for m in self.metrics if not m.is_metric]
if missing_agg:
raise ValueError(
f"Sandpack chart in 'aggregate' query mode requires an "
f"aggregate function on every metric. Missing aggregate for: "
f"{', '.join(missing_agg)}. "
f"Use one of: SUM, COUNT, AVG, MIN, MAX, COUNT_DISTINCT, etc."
)
return self
class BigNumberChartConfig(UnknownFieldCheckMixin):
model_config = ConfigDict(extra="ignore")
@@ -1343,13 +1474,14 @@ ChartConfig = Annotated[
| PivotTableChartConfig
| MixedTimeseriesChartConfig
| HandlebarsChartConfig
| SandpackChartConfig
| BigNumberChartConfig,
Field(
discriminator="chart_type",
description=(
"Chart configuration - specify chart_type as 'xy', 'table', "
"'pie', 'pivot_table', 'mixed_timeseries', 'handlebars', "
"or 'big_number'"
"'sandpack', or 'big_number'"
),
),
]

View File

@@ -33,6 +33,7 @@ from superset.mcp_service.chart.schemas import (
MixedTimeseriesChartConfig,
PieChartConfig,
PivotTableChartConfig,
SandpackChartConfig,
TableChartConfig,
XYChartConfig,
)
@@ -47,6 +48,7 @@ _CHART_TYPE_ADAPTERS: Dict[str, TypeAdapter[Any]] = {
"pivot_table": TypeAdapter(PivotTableChartConfig),
"mixed_timeseries": TypeAdapter(MixedTimeseriesChartConfig),
"handlebars": TypeAdapter(HandlebarsChartConfig),
"sandpack": TypeAdapter(SandpackChartConfig),
"big_number": TypeAdapter(BigNumberChartConfig),
}
@@ -110,6 +112,49 @@ _CHART_EXAMPLES: Dict[str, list[Dict[str, Any]]] = {
"handlebars_template": "{{#each data}}<p>{{customer_name}}</p>{{/each}}",
},
],
"sandpack": [
{
"chart_type": "sandpack",
"template": "react",
"query_mode": "raw",
"columns": [{"name": "customer_name"}, {"name": "revenue"}],
"app_code": (
"import data from './data.json';\n\n"
"export default function App() {\n"
" return (\n"
" <ul>\n"
" {data.map((row, i) => (\n"
" <li key={i}>{row.customer_name}: {row.revenue}</li>\n"
" ))}\n"
" </ul>\n"
" );\n"
"}\n"
),
},
{
"chart_type": "sandpack",
"template": "react",
"query_mode": "aggregate",
"groupby": [{"name": "region"}],
"metrics": [{"name": "revenue", "aggregate": "SUM"}],
"dependencies": {"recharts": "^2.12.0"},
"app_code": (
"import data from './data.json';\n"
"import { BarChart, Bar, XAxis, YAxis, Tooltip } "
"from 'recharts';\n\n"
"export default function App() {\n"
" return (\n"
" <BarChart width={400} height={300} data={data}>\n"
" <XAxis dataKey=\"region\" />\n"
" <YAxis />\n"
" <Tooltip />\n"
" <Bar dataKey=\"SUM(revenue)\" />\n"
" </BarChart>\n"
" );\n"
"}\n"
),
},
],
"big_number": [
{
"chart_type": "big_number",
@@ -179,7 +224,7 @@ def get_chart_type_schema(
for a chart configuration before calling generate_chart or update_chart.
Valid chart_type values: xy, table, pie, pivot_table,
mixed_timeseries, handlebars, big_number.
mixed_timeseries, handlebars, sandpack, big_number.
Returns the JSON Schema for the requested chart type, optionally
with working examples.

View File

@@ -28,6 +28,7 @@ from superset.mcp_service.chart.schemas import (
BigNumberChartConfig,
ColumnRef,
HandlebarsChartConfig,
SandpackChartConfig,
MixedTimeseriesChartConfig,
PieChartConfig,
PivotTableChartConfig,
@@ -302,6 +303,13 @@ class DatasetValidator:
refs.extend(config.groupby)
if config.metrics:
refs.extend(config.metrics)
elif isinstance(config, SandpackChartConfig):
if config.columns:
refs.extend(config.columns)
if config.groupby:
refs.extend(config.groupby)
if config.metrics:
refs.extend(config.metrics)
elif isinstance(config, BigNumberChartConfig):
refs.append(config.metric)
if config.temporal_column:

View File

@@ -134,6 +134,8 @@ class SchemaValidator:
"Add 'chart_type': 'pivot_table' for interactive pivot tables",
"Add 'chart_type': 'mixed_timeseries' for dual-series time charts",
"Add 'chart_type': 'handlebars' for custom HTML template charts",
"Add 'chart_type': 'sandpack' for in-browser Sandpack apps "
"(React/JS) rendered against the query result",
"Add 'chart_type': 'big_number' for big number display",
"Example: 'config': {'chart_type': 'xy', ...}",
],
@@ -155,6 +157,7 @@ class SchemaValidator:
"pivot_table": SchemaValidator._pre_validate_pivot_table_config,
"mixed_timeseries": SchemaValidator._pre_validate_mixed_timeseries_config,
"handlebars": SchemaValidator._pre_validate_handlebars_config,
"sandpack": SchemaValidator._pre_validate_sandpack_config,
"big_number": SchemaValidator._pre_validate_big_number_config,
}
@@ -172,6 +175,7 @@ class SchemaValidator:
"Use 'chart_type': 'pivot_table' for interactive pivot tables",
"Use 'chart_type': 'mixed_timeseries' for dual-series time charts",
"Use 'chart_type': 'handlebars' for custom HTML template charts",
"Use 'chart_type': 'sandpack' for Sandpack-rendered React/JS apps",
"Use 'chart_type': 'big_number' for big number display",
"Check spelling and ensure lowercase",
],
@@ -358,6 +362,130 @@ class SchemaValidator:
return True, None
@staticmethod
def _pre_validate_sandpack_config(
config: Dict[str, Any],
) -> Tuple[bool, ChartGenerationError | None]:
"""Pre-validate sandpack chart configuration."""
# Accept both snake_case and the camelCase form_data alias.
app_code = config.get("app_code") or config.get("appCode")
if app_code is None:
return False, ChartGenerationError(
error_type="missing_app_code",
message="Sandpack chart missing required field: app_code",
details="Sandpack charts require an 'app_code' string with the "
"source code of the entry file. Inside the sandbox the "
"dataset is exposed as './data.json'.",
suggestions=[
"Add 'app_code' with the source for your entry file",
"React entry: import data from './data.json'; "
"export default function App() { ... }",
"Vanilla entry: import data from './data.json'; "
"document.body.innerText = JSON.stringify(data);",
],
error_code="MISSING_APP_CODE",
)
if not isinstance(app_code, str) or not app_code.strip():
return False, ChartGenerationError(
error_type="invalid_app_code",
message="Sandpack app_code must be a non-empty string",
details="The 'app_code' field must contain the source code for "
"the entry file as a non-empty string.",
suggestions=[
"Ensure app_code is a non-empty string",
"Example: \"import data from './data.json';\\n"
"export default function App(){return <pre>{JSON."
'stringify(data)}</pre>}"',
],
error_code="INVALID_APP_CODE",
)
template = config.get("template", "react")
valid_templates = ("react", "react-ts", "vanilla", "vanilla-ts")
if template not in valid_templates:
return False, ChartGenerationError(
error_type="invalid_template",
message=f"Invalid Sandpack template: '{template}'",
details=f"template must be one of: {', '.join(valid_templates)}",
suggestions=[
"Use 'react' (default) for JS React apps",
"Use 'react-ts' for TypeScript React",
"Use 'vanilla' or 'vanilla-ts' for plain JS/TS",
],
error_code="INVALID_SANDPACK_TEMPLATE",
)
dependencies = config.get("dependencies")
if dependencies is not None and not isinstance(dependencies, dict):
return False, ChartGenerationError(
error_type="invalid_dependencies",
message="Sandpack dependencies must be an object",
details="The 'dependencies' field must be an object mapping "
"package names to semver ranges, or omitted entirely.",
suggestions=[
"Pass an object: {'recharts': '^2.12.0'}",
"Or omit 'dependencies' to use only template defaults",
],
error_code="INVALID_SANDPACK_DEPENDENCIES",
)
layout = config.get("layout", "preview")
if layout not in ("preview", "split", "editor"):
return False, ChartGenerationError(
error_type="invalid_layout",
message=f"Invalid Sandpack layout: '{layout}'",
details="layout must be 'preview', 'split', or 'editor'",
suggestions=[
"Use 'preview' for finished charts (default)",
"Use 'split' for editor + preview side-by-side",
"Use 'editor' to show only the code",
],
error_code="INVALID_SANDPACK_LAYOUT",
)
query_mode = config.get("query_mode", "raw")
if query_mode not in ("aggregate", "raw"):
return False, ChartGenerationError(
error_type="invalid_query_mode",
message="Invalid query_mode for sandpack chart",
details="query_mode must be either 'aggregate' or 'raw'",
suggestions=[
"Use 'raw' for row-level data (default for sandpack)",
"Use 'aggregate' with 'metrics' for grouped data",
],
error_code="INVALID_QUERY_MODE",
)
if query_mode == "raw" and not config.get("columns"):
return False, ChartGenerationError(
error_type="missing_raw_columns",
message="Sandpack chart in 'raw' mode requires 'columns'",
details="When query_mode is 'raw', you must specify which "
"columns to include in ./data.json",
suggestions=[
"Add 'columns': [{'name': 'column_name'}]",
"Or use query_mode='aggregate' with 'metrics' "
"and optional 'groupby'",
],
error_code="MISSING_RAW_COLUMNS",
)
if query_mode == "aggregate" and not config.get("metrics"):
return False, ChartGenerationError(
error_type="missing_aggregate_metrics",
message="Sandpack chart in 'aggregate' mode requires 'metrics'",
details="When query_mode is 'aggregate', specify at least one "
"metric with an aggregate function",
suggestions=[
"Add 'metrics': [{'name': 'col', 'aggregate': 'SUM'}]",
"Or use query_mode='raw' with 'columns' for individual rows",
],
error_code="MISSING_AGGREGATE_METRICS",
)
return True, None
@staticmethod
def _pre_validate_big_number_config(
config: Dict[str, Any],
@@ -591,6 +719,26 @@ class SchemaValidator:
],
error_code="HANDLEBARS_VALIDATION_ERROR",
)
elif chart_type == "sandpack":
return ChartGenerationError(
error_type="sandpack_validation_error",
message="Sandpack chart configuration validation failed",
details="The sandpack chart configuration is missing "
"required fields or has invalid structure",
suggestions=[
"Ensure 'app_code' is a non-empty string",
"Pick a template: 'react' (default), 'react-ts', "
"'vanilla', or 'vanilla-ts'",
"For raw mode (default): set 'columns'",
"For aggregate mode: set 'metrics' (and optionally "
"'groupby') and switch 'query_mode' to 'aggregate'",
"Example: {'chart_type': 'sandpack', 'app_code': "
"\"import data from './data.json'; export default "
"() => <pre>{JSON.stringify(data)}</pre>\", "
"'columns': [{'name': 'product'}]}",
],
error_code="SANDPACK_VALIDATION_ERROR",
)
elif chart_type == "big_number":
return ChartGenerationError(
error_type="big_number_validation_error",

View File

@@ -0,0 +1,423 @@
# 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 Sandpack chart type support in MCP service."""
import pytest
from superset.mcp_service.chart.chart_utils import (
_resolve_viz_type,
analyze_chart_capabilities,
analyze_chart_semantics,
generate_chart_name,
map_config_to_form_data,
map_sandpack_config,
)
from superset.mcp_service.chart.schemas import (
ColumnRef,
FilterConfig,
SandpackChartConfig,
)
from superset.mcp_service.chart.validation.schema_validator import SchemaValidator
from superset.utils import json
DEFAULT_APP = "import data from './data.json';\nexport default () => null;\n"
class TestSandpackChartConfig:
def test_minimal_raw_config(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
assert config.chart_type == "sandpack"
assert config.template == "react"
assert config.layout == "preview"
assert config.query_mode == "raw"
assert config.row_limit == 1000
def test_minimal_aggregate_config(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="aggregate",
metrics=[ColumnRef(name="sales", aggregate="SUM")],
)
assert config.query_mode == "aggregate"
def test_app_code_alias(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
appCode=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
assert config.app_code == DEFAULT_APP
def test_show_navigator_alias(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
showNavigator=True,
)
assert config.show_navigator is True
def test_app_code_min_length(self) -> None:
with pytest.raises(ValueError, match="at least 1 character"):
SandpackChartConfig(
chart_type="sandpack",
app_code="",
columns=[ColumnRef(name="product")],
)
def test_raw_mode_requires_columns(self) -> None:
with pytest.raises(ValueError, match="requires 'columns'"):
SandpackChartConfig(chart_type="sandpack", app_code=DEFAULT_APP)
def test_aggregate_mode_requires_metrics(self) -> None:
with pytest.raises(ValueError, match="requires 'metrics'"):
SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="aggregate",
)
def test_aggregate_mode_requires_aggregate_function(self) -> None:
with pytest.raises(ValueError, match="Missing aggregate for"):
SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="aggregate",
metrics=[ColumnRef(name="sales")],
)
def test_raw_mode_rejects_metrics(self) -> None:
with pytest.raises(ValueError, match="does not use 'metrics'"):
SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="raw",
columns=[ColumnRef(name="product")],
metrics=[ColumnRef(name="sales", aggregate="SUM")],
)
def test_extra_fields_forbidden(self) -> None:
with pytest.raises(ValueError, match="Unknown field"):
SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
bogus_field="x",
)
def test_invalid_template_rejected_by_pydantic(self) -> None:
with pytest.raises(ValueError):
SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
template="vue", # not a valid Literal value
columns=[ColumnRef(name="product")],
)
class TestMapSandpackConfig:
def test_raw_mode_basic(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product"), ColumnRef(name="price")],
)
result = map_sandpack_config(config)
assert result["viz_type"] == "sandpack"
assert result["appCode"] == DEFAULT_APP
assert result["template"] == "react"
assert result["layout"] == "preview"
assert result["showNavigator"] is False
assert result["query_mode"] == "raw"
assert result["all_columns"] == ["product", "price"]
assert "metrics" not in result
assert "groupby" not in result
def test_aggregate_mode_with_groupby(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="aggregate",
groupby=[ColumnRef(name="region")],
metrics=[ColumnRef(name="sales", aggregate="SUM")],
)
result = map_sandpack_config(config)
assert result["query_mode"] == "aggregate"
assert result["groupby"] == ["region"]
assert result["metrics"][0]["aggregate"] == "SUM"
def test_dependencies_serialized_to_json_string(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
dependencies={"recharts": "^2.12.0"},
)
result = map_sandpack_config(config)
assert isinstance(result["dependencies"], str)
assert json.loads(result["dependencies"]) == {"recharts": "^2.12.0"}
def test_no_dependencies_omits_key(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
assert "dependencies" not in map_sandpack_config(config)
def test_with_filters(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
filters=[FilterConfig(column="status", op="=", value="active")],
)
result = map_sandpack_config(config)
assert "adhoc_filters" in result
assert result["adhoc_filters"][0]["subject"] == "status"
assert result["adhoc_filters"][0]["comparator"] == "active"
def test_layout_and_navigator_pass_through(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
layout="split",
show_navigator=True,
)
result = map_sandpack_config(config)
assert result["layout"] == "split"
assert result["showNavigator"] is True
class TestMapConfigToFormDataSandpack:
def test_dispatches_sandpack_config(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
result = map_config_to_form_data(config)
assert result["viz_type"] == "sandpack"
class TestGenerateChartNameSandpack:
def test_raw_mode_uses_columns(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product"), ColumnRef(name="price")],
)
name = generate_chart_name(config)
assert "Sandpack" in name
assert "product" in name
def test_aggregate_mode_uses_metrics(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
query_mode="aggregate",
metrics=[ColumnRef(name="sales", aggregate="SUM")],
)
name = generate_chart_name(config)
assert "Sandpack" in name
assert "sales" in name
class TestResolveVizTypeSandpack:
def test_sandpack(self) -> None:
from unittest.mock import MagicMock
config = MagicMock(chart_type="sandpack")
assert _resolve_viz_type(config) == "sandpack"
class TestAnalyzeChartCapabilitiesSandpack:
def test_sandpack_capabilities(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
caps = analyze_chart_capabilities(None, config)
assert caps.supports_export is True
assert "url" in caps.optimal_formats
class TestAnalyzeChartSemanticsSandpack:
def test_sandpack_semantics(self) -> None:
config = SandpackChartConfig(
chart_type="sandpack",
app_code=DEFAULT_APP,
columns=[ColumnRef(name="product")],
)
semantics = analyze_chart_semantics(None, config)
assert semantics.primary_insight is not None
class TestSchemaValidatorSandpack:
def test_minimal_raw_accepted(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
"columns": [{"name": "product"}],
},
}
is_valid, request, error = SchemaValidator.validate_request(data)
assert is_valid is True
assert request is not None
assert error is None
def test_camelcase_app_code_alias(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"appCode": DEFAULT_APP,
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is True
assert error is None
def test_missing_app_code(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "MISSING_APP_CODE"
def test_empty_app_code(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": " ",
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "INVALID_APP_CODE"
def test_invalid_template(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
"template": "svelte",
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "INVALID_SANDPACK_TEMPLATE"
def test_dependencies_must_be_object(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
"dependencies": ["recharts"],
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "INVALID_SANDPACK_DEPENDENCIES"
def test_invalid_layout(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
"layout": "fullscreen",
"columns": [{"name": "product"}],
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "INVALID_SANDPACK_LAYOUT"
def test_raw_mode_missing_columns(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "MISSING_RAW_COLUMNS"
def test_aggregate_mode_missing_metrics(self) -> None:
data = {
"dataset_id": 1,
"config": {
"chart_type": "sandpack",
"app_code": DEFAULT_APP,
"query_mode": "aggregate",
},
}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert error.error_code == "MISSING_AGGREGATE_METRICS"
def test_invalid_chart_type_suggestions_mention_sandpack(self) -> None:
data = {"dataset_id": 1, "config": {"chart_type": "invalid"}}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
assert "sandpack" in (error.details or "")
def test_missing_chart_type_suggestions_mention_sandpack(self) -> None:
data = {"dataset_id": 1, "config": {}}
is_valid, _, error = SchemaValidator.validate_request(data)
assert is_valid is False
assert error is not None
suggestions_text = " ".join(error.suggestions or [])
assert "sandpack" in suggestions_text

View File

@@ -94,9 +94,10 @@ class TestGetChartTypeSchema:
assert example["chart_type"] == "pie"
def test_valid_chart_types_constant(self) -> None:
assert len(VALID_CHART_TYPES) == 7
assert len(VALID_CHART_TYPES) == 8
assert "xy" in VALID_CHART_TYPES
assert "table" in VALID_CHART_TYPES
assert "sandpack" in VALID_CHART_TYPES
def test_all_chart_types_have_examples(self) -> None:
for chart_type in VALID_CHART_TYPES: