This is an automated email from the ASF dual-hosted git repository. diegopucci pushed a commit to branch geido/refactor/dashboard-screenshot in repository https://gitbox.apache.org/repos/asf/superset.git
commit 909ba5771eb22ace0234e44b6ce98e49441c8c3d Author: Diego Pucci <[email protected]> AuthorDate: Mon Jun 17 17:38:49 2024 +0200 Fetch dashboard screenshot --- superset-frontend/package-lock.json | 282 +-------------------- superset-frontend/package.json | 1 - .../Header/HeaderActionsDropdown/index.jsx | 2 + .../DownloadMenuItems/DownloadMenuItems.test.tsx | 6 +- .../DownloadMenuItems/DownloadScreenshot.test.tsx | 225 ++++++++++++++++ .../menu/DownloadMenuItems/DownloadScreenshot.tsx | 133 ++++++++++ .../components/menu/DownloadMenuItems/index.tsx | 25 +- .../{DownloadMenuItems.test.tsx => types.ts} | 27 +- 8 files changed, 398 insertions(+), 303 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index f4159d5764..99ca85f830 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -66,7 +66,6 @@ "core-js": "^3.37.1", "d3-scale": "^2.1.2", "dom-to-image-more": "^3.2.0", - "dom-to-pdf": "^0.3.1", "emotion-rgba": "0.0.12", "fast-glob": "^3.2.7", "fontsource-fira-code": "^4.0.0", @@ -23047,11 +23046,6 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, - "node_modules/@types/raf": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz", - "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==" - }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", @@ -27166,14 +27160,6 @@ "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -27628,17 +27614,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buf-compare": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", @@ -27942,24 +27917,6 @@ } ] }, - "node_modules/canvg": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", - "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@types/raf": "^3.4.0", - "core-js": "^3.8.3", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.7", - "rgbcolor": "^1.0.1", - "stackblur-canvas": "^2.0.0", - "svg-pathdata": "^6.0.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -30885,14 +30842,6 @@ "isobject": "^3.0.1" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/css-loader": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", @@ -33078,25 +33027,11 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-to-image": { - "version": "2.6.0", - "resolved": "git+ssh://[email protected]/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3", - "license": "MIT" - }, "node_modules/dom-to-image-more": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.2.0.tgz", "integrity": "sha512-2bGQTB6m17MBseVhIjShwZqqqCyVS9GgTykWqvVXMqr56fSgHhXnEvZfZkaSuHJYW3ICZQ3sZwAu+UY5tfsF9Q==" }, - "node_modules/dom-to-pdf": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz", - "integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==", - "dependencies": { - "dom-to-image": "git+https://github.com/dmapper/dom-to-image.git", - "jspdf": "^2.5.1" - } - }, "node_modules/dom-walk": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", @@ -33127,12 +33062,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz", - "integrity": "sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==", - "optional": true - }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -36116,11 +36045,6 @@ "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==" }, - "node_modules/fflate": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", - "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -39147,18 +39071,6 @@ "node": ">=6" } }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -46705,23 +46617,6 @@ "node": "*" } }, - "node_modules/jspdf": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", - "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", - "dependencies": { - "@babel/runtime": "^7.14.0", - "atob": "^2.1.2", - "btoa": "^1.2.1", - "canvg": "^3.0.6", - "fflate": "^0.4.8", - "html2canvas": "^1.0.0-rc.5" - }, - "optionalDependencies": { - "core-js": "^3.6.0", - "dompurify": "^2.2.0" - } - }, "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -60475,14 +60370,6 @@ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, - "node_modules/rgbcolor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", - "engines": { - "node": ">= 0.8.15" - } - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -62012,14 +61899,6 @@ "node": ">=8" } }, - "node_modules/stackblur-canvas": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", - "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==", - "engines": { - "node": ">=0.1.14" - } - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -62720,14 +62599,6 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, - "node_modules/svg-pathdata": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", - "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/svgo": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", @@ -63243,14 +63114,6 @@ "node": ">=0.10" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -64732,14 +64595,6 @@ "node": ">= 0.4.0" } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -67695,6 +67550,11 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" }, + "packages/superset-ui-core/node_modules/@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "packages/superset-ui-core/node_modules/@types/fetch-mock": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/@types/fetch-mock/-/fetch-mock-7.3.8.tgz", @@ -67705,11 +67565,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, - "packages/superset-ui-core/node_modules/@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" - }, "packages/superset-ui-core/node_modules/@types/math-expression-evaluator": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/math-expression-evaluator/-/math-expression-evaluator-1.3.3.tgz", @@ -86352,6 +86207,11 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" }, + "@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + }, "@types/fetch-mock": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/@types/fetch-mock/-/fetch-mock-7.3.8.tgz", @@ -86362,11 +86222,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, - "@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" - }, "@types/math-expression-evaluator": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/math-expression-evaluator/-/math-expression-evaluator-1.3.3.tgz", @@ -90127,11 +89982,6 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, - "@types/raf": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz", - "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==" - }, "@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", @@ -93427,11 +93277,6 @@ "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==" }, - "base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -93794,11 +93639,6 @@ "node-int64": "^0.4.0" } }, - "btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" - }, "buf-compare": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", @@ -94014,21 +93854,6 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==" }, - "canvg": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", - "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", - "requires": { - "@babel/runtime": "^7.12.5", - "@types/raf": "^3.4.0", - "core-js": "^3.8.3", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.7", - "rgbcolor": "^1.0.1", - "stackblur-canvas": "^2.0.0", - "svg-pathdata": "^6.0.3" - } - }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -96333,14 +96158,6 @@ "isobject": "^3.0.1" } }, - "css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "requires": { - "utrie": "^1.0.2" - } - }, "css-loader": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", @@ -98025,24 +97842,11 @@ "entities": "^4.2.0" } }, - "dom-to-image": { - "version": "git+ssh://[email protected]/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3", - "from": "dom-to-image@git+https://github.com/dmapper/dom-to-image.git" - }, "dom-to-image-more": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.2.0.tgz", "integrity": "sha512-2bGQTB6m17MBseVhIjShwZqqqCyVS9GgTykWqvVXMqr56fSgHhXnEvZfZkaSuHJYW3ICZQ3sZwAu+UY5tfsF9Q==" }, - "dom-to-pdf": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz", - "integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==", - "requires": { - "dom-to-image": "git+https://github.com/dmapper/dom-to-image.git", - "jspdf": "^2.5.1" - } - }, "dom-walk": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", @@ -98061,12 +97865,6 @@ "domelementtype": "^2.3.0" } }, - "dompurify": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz", - "integrity": "sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==", - "optional": true - }, "domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -100381,11 +100179,6 @@ "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==" }, - "fflate": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", - "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -102656,15 +102449,6 @@ } } }, - "html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "requires": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - } - }, "htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -108437,21 +108221,6 @@ "through": ">=2.2.7 <3" } }, - "jspdf": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", - "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", - "requires": { - "@babel/runtime": "^7.14.0", - "atob": "^2.1.2", - "btoa": "^1.2.1", - "canvg": "^3.0.6", - "core-js": "^3.6.0", - "dompurify": "^2.2.0", - "fflate": "^0.4.8", - "html2canvas": "^1.0.0-rc.5" - } - }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -118847,11 +118616,6 @@ "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, - "rgbcolor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==" - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -120072,11 +119836,6 @@ } } }, - "stackblur-canvas": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", - "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==" - }, "stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -120606,11 +120365,6 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, - "svg-pathdata": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", - "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==" - }, "svgo": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", @@ -120990,14 +120744,6 @@ "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, - "text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "requires": { - "utrie": "^1.0.2" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -122099,14 +121845,6 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, - "utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "requires": { - "base64-arraybuffer": "^1.0.2" - } - }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 3d488537fb..c4c448992f 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -131,7 +131,6 @@ "core-js": "^3.37.1", "d3-scale": "^2.1.2", "dom-to-image-more": "^3.2.0", - "dom-to-pdf": "^0.3.1", "emotion-rgba": "0.0.12", "fast-glob": "^3.2.7", "fontsource-fira-code": "^4.0.0", diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index 8008558ded..2d675a5fdc 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -268,6 +268,8 @@ class HeaderActionsDropdown extends PureComponent { imageMenuItemTitle={t('Download as Image')} dashboardTitle={dashboardTitle} addDangerToast={addDangerToast} + addSuccessToast={addSuccessToast} + dashboardId={dashboardId} /> </Menu.SubMenu> {userCanShare && ( diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx index a4e058572e..a2176e555f 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx @@ -25,10 +25,14 @@ const createProps = () => ({ imageMenuItemTitle: 'Download as Image', dashboardTitle: 'Test Dashboard', logEvent: jest.fn(), + addSuccessToast: jest.fn(), + dashboardId: '123', }); const renderComponent = () => { - render(<DownloadMenuItems {...createProps()} />); + render(<DownloadMenuItems {...createProps()} />, { + useRedux: true, + }); }; test('Should render menu items', () => { diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx new file mode 100644 index 0000000000..612f975153 --- /dev/null +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx @@ -0,0 +1,225 @@ +/** + * 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 React from 'react'; +import { render, screen, waitFor } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import { Menu } from 'src/components/Menu'; +import fetchMock from 'fetch-mock'; +import { logging } from '@superset-ui/core'; +import { DownloadScreenshotFormat } from './types'; +import DownloadScreenshot from './DownloadScreenshot'; + +const mockAddDangerToast = jest.fn(); +const mockLogEvent = jest.fn(); +const mockAddSuccessToast = jest.fn(); +const mockAddInfoToast = jest.fn(); + +jest.spyOn(logging, 'error').mockImplementation(() => {}); + +const defaultProps = () => ({ + text: 'Download', + dashboardId: '123', + format: DownloadScreenshotFormat.PDF, + addDangerToast: mockAddDangerToast, + addSuccessToast: mockAddSuccessToast, + addInfoToast: mockAddInfoToast, + logEvent: mockLogEvent, +}); + +const renderComponent = () => { + render( + <Menu> + <DownloadScreenshot {...defaultProps()} /> + </Menu>, + ); +}; + +describe('DownloadScreenshot component', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + fetchMock.restore(); + }); + + test('renders correctly with the given text', () => { + renderComponent(); + expect(screen.getByText('Download')).toBeInTheDocument(); + }); + + test('button renders with role="button"', async () => { + renderComponent(); + const button = screen.getByRole('button', { name: 'Download' }); + expect(button).toBeInTheDocument(); + }); + + test('displays error message when API call fails', async () => { + const props = defaultProps(); + + fetchMock.post( + `glob:*/api/v1/dashboard/${props.dashboardId}/cache_screenshot`, + { + status: 400, + body: {}, + }, + ); + + renderComponent(); + + userEvent.click(screen.getByRole('button', { name: 'Download' })); + + await waitFor(() => { + expect(defaultProps().addDangerToast).toHaveBeenCalledWith( + 'The screenshot could not be downloaded. Please, try again later.', + ); + }); + }); + + test('displays success message when API call succeeds', async () => { + const props = defaultProps(); + fetchMock.post( + `glob:*/api/v1/dashboard/${props.dashboardId}/cache_screenshot`, + { + status: 200, + body: { + image_url: 'mocked_image_url', + }, + }, + ); + + fetchMock.get('glob:*/mocked_image_url?download_format=pdf', { + status: 200, + body: {}, + }); + + renderComponent(); + + userEvent.click(screen.getByRole('button', { name: 'Download' })); + + await waitFor(() => { + expect(props.addInfoToast).toHaveBeenCalledWith( + 'The screenshot is being generated. Please, do not leave the page.', + ); + }); + }); + + test('throws error when no image URL is provided', async () => { + const props = defaultProps(); + fetchMock.post( + `glob:*/api/v1/dashboard/${props.dashboardId}/cache_screenshot`, + { + status: 200, + body: { + image_url: '', + }, + }, + ); + + renderComponent(); + + // Simulate the user clicking the download button + userEvent.click(screen.getByRole('button', { name: 'Download' })); + + await waitFor(() => { + expect(props.addDangerToast).toHaveBeenCalledWith( + 'The screenshot could not be downloaded. Please, try again later.', + ); + }); + }); + + test('retries fetching image when retrieval fails', async () => { + jest.useFakeTimers(); + + const props = defaultProps(); + const maxRetries = 20; + const imageUrl = 'glob:*/mocked_image_url?download_format=pdf'; + fetchMock.post( + `glob:*/api/v1/dashboard/${props.dashboardId}/cache_screenshot`, + { + status: 200, + body: { + image_url: 'mocked_image_url', + }, + }, + ); + + fetchMock.get(imageUrl, 404); + + // Render the component + renderComponent(); + + // Simulate the user clicking the download button + userEvent.click(screen.getByRole('button', { name: 'Download' })); + + // Assert that the correct number of retries are made + for (let i = 0; i <= maxRetries; i += 1) { + jest.advanceTimersByTime(1000); + } + await waitFor(() => { + expect(fetchMock.calls(imageUrl).length).toBe(maxRetries + 1); + }); + + // Assert that the error message is displayed + expect(props.addDangerToast).toHaveBeenCalledWith( + 'The screenshot could not be downloaded. Please, try again later.', + ); + }); + + test('displays success message when image retrieval succeeds', async () => { + const props = defaultProps(); + const imageUrl = 'glob:*/mocked_image_url?download_format=pdf'; + fetchMock.post( + `glob:*/api/v1/dashboard/${props.dashboardId}/cache_screenshot`, + { + status: 200, + body: { + image_url: 'mocked_image_url', + }, + }, + ); + + fetchMock.get(imageUrl, { + status: 200, + headers: { + 'Content-Type': 'image/png', + }, + body: new Blob([], { type: 'image/png' }), + }); + + global.URL.createObjectURL = jest.fn(() => 'mockedObjectURL'); + global.URL.revokeObjectURL = jest.fn(); + + // Render the component + renderComponent(); + + // Simulate the user clicking the download button + userEvent.click(screen.getByRole('button', { name: 'Download' })); + + await waitFor(() => { + expect(fetchMock.calls(imageUrl).length).toBe(1); + }); + + // Wait for the successful image retrieval message + await waitFor(() => { + expect(props.addSuccessToast).toHaveBeenCalledWith( + 'The screenshot is now ready to be downloaded.', + ); + }); + }); +}); diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx new file mode 100644 index 0000000000..641a3376f3 --- /dev/null +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx @@ -0,0 +1,133 @@ +/** + * 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 React, { SyntheticEvent } from 'react'; +import { logging, t, SupersetClient } from '@superset-ui/core'; +import { Menu } from 'src/components/Menu'; +import { + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE, + LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, +} from 'src/logger/LogUtils'; +import { DownloadScreenshotFormat } from './types'; + +// parameters for adjustments +const RETRY_INTERVAL = 3000; // 3 seconds +const MAX_RETRIES = 20; // 60 seconds / 3 seconds = 20 retries + +export default function DownloadScreenshot({ + text, + logEvent, + dashboardId, + addDangerToast, + format, + addSuccessToast, + addInfoToast, + ...rest +}: { + text: string; + addDangerToast: Function; + dashboardId: string; + logEvent?: Function; + format: string; + addSuccessToast: Function; + addInfoToast: Function; +}) { + const onDownloadScreenshot = (e: SyntheticEvent) => { + let retries = 0; + + // this function checks if the image is ready + const checkImageReady = (imageUrl: string) => + fetch(`${imageUrl}?download_format=${format}`) + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `screenshot.${format}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }) + .catch(() => { + throw new Error('Image not ready'); + }); + + // this is the functions that handles the retries + const fetchImageWithRetry = (imageUrl: string) => { + checkImageReady(imageUrl) + .then(() => { + addSuccessToast(t('The screenshot is now ready to be downloaded.')); + }) + .catch(error => { + // we check how many retries have been made + if (retries < MAX_RETRIES) { + retries += 1; + setTimeout(() => fetchImageWithRetry(imageUrl), RETRY_INTERVAL); + } else { + logging.error(error); + addDangerToast( + t( + 'The screenshot could not be downloaded. Please, try again later.', + ), + ); + } + }); + }; + + SupersetClient.post({ + endpoint: `/api/v1/dashboard/${dashboardId}/cache_screenshot`, + }) + .then(({ json }) => { + const imageUrl = json?.image_url; + if (imageUrl) { + addInfoToast( + 'The screenshot is being generated. Please, do not leave the page.', + ); + } else { + throw new Error('No image URL in response'); + } + fetchImageWithRetry(imageUrl); + }) + .catch(error => { + logging.error(error); + addDangerToast( + t('The screenshot could not be downloaded. Please, try again later.'), + ); + }) + .finally(() => { + logEvent?.( + format === DownloadScreenshotFormat.PNG + ? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE + : LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF, + ); + }); + }; + + const item_key = + format === DownloadScreenshotFormat.PDF ? 'download-pdf' : 'download-image'; + + return ( + <Menu.Item key={item_key} {...rest}> + <div onClick={onDownloadScreenshot} role="button" tabIndex={0}> + {text} + </div> + </Menu.Item> + ); +} diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx index 290d18232f..709c3c0653 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx @@ -17,15 +17,18 @@ * under the License. */ import { Menu } from 'src/components/Menu'; -import DownloadAsImage from './DownloadAsImage'; -import DownloadAsPdf from './DownloadAsPdf'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import DownloadScreenshot from './DownloadScreenshot'; +import { DownloadScreenshotFormat } from './types'; export interface DownloadMenuItemProps { pdfMenuItemTitle: string; imageMenuItemTitle: string; addDangerToast: Function; + addSuccessToast: Function; dashboardTitle: string; logEvent?: Function; + dashboardId: string; } const DownloadMenuItems = (props: DownloadMenuItemProps) => { @@ -33,25 +36,35 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => { pdfMenuItemTitle, imageMenuItemTitle, addDangerToast, + addSuccessToast, dashboardTitle, logEvent, + dashboardId, ...rest } = props; + const { addInfoToast } = useToasts(); + return ( <Menu selectable={false}> - <DownloadAsPdf + <DownloadScreenshot text={pdfMenuItemTitle} + dashboardId={dashboardId} addDangerToast={addDangerToast} - dashboardTitle={dashboardTitle} + addSuccessToast={addSuccessToast} logEvent={logEvent} + addInfoToast={addInfoToast} + format={DownloadScreenshotFormat.PDF} {...rest} /> - <DownloadAsImage + <DownloadScreenshot text={imageMenuItemTitle} + dashboardId={dashboardId} addDangerToast={addDangerToast} - dashboardTitle={dashboardTitle} + addSuccessToast={addSuccessToast} + addInfoToast={addInfoToast} logEvent={logEvent} + format={DownloadScreenshotFormat.PNG} {...rest} /> </Menu> diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/types.ts similarity index 55% copy from superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx copy to superset-frontend/src/dashboard/components/menu/DownloadMenuItems/types.ts index a4e058572e..35129532e2 100644 --- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/types.ts @@ -16,27 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen } from 'spec/helpers/testing-library'; -import DownloadMenuItems from '.'; -const createProps = () => ({ - addDangerToast: jest.fn(), - pdfMenuItemTitle: 'Export to PDF', - imageMenuItemTitle: 'Download as Image', - dashboardTitle: 'Test Dashboard', - logEvent: jest.fn(), -}); - -const renderComponent = () => { - render(<DownloadMenuItems {...createProps()} />); -}; - -test('Should render menu items', () => { - renderComponent(); - expect( - screen.getByRole('menuitem', { name: 'Export to PDF' }), - ).toBeInTheDocument(); - expect( - screen.getByRole('menuitem', { name: 'Download as Image' }), - ).toBeInTheDocument(); -}); +export enum DownloadScreenshotFormat { + PDF = 'pdf', + PNG = 'png', +}
