fix(Dashboard): Sync/Async Dashboard Screenshot Generation and Default Cache (#30755)

Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
Geido 2024-11-01 11:39:43 +02:00 committed by GitHub
parent 2518190b2d
commit 3e29777526
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 760 additions and 79 deletions

View File

@ -32,4 +32,4 @@ jobs:
# license: https://applitools.com/legal/open-source-terms-of-use/
# pkg:npm/node-forge@1.3.1
# selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor

View File

@ -71,6 +71,7 @@
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.2.0",
"dom-to-pdf": "^0.3.2",
"emotion-rgba": "0.0.12",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
@ -13879,6 +13880,13 @@
"version": "6.9.7",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"license": "MIT"
@ -17358,7 +17366,6 @@
},
"node_modules/atob": {
"version": "2.1.2",
"dev": true,
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
@ -18076,6 +18083,16 @@
"version": "1.0.0",
"license": "MIT"
},
"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==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"funding": [
@ -18543,6 +18560,18 @@
"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==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buf-compare": {
"version": "1.0.1",
"license": "MIT",
@ -19036,6 +19065,33 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"license": "MIT",
"optional": true,
"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/canvg/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/capture-exit": {
"version": "2.0.0",
"dev": true,
@ -20619,6 +20675,16 @@
"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==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "6.8.1",
"dev": true,
@ -22609,10 +22675,25 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/dom-to-image": {
"version": "2.6.0",
"resolved": "git+ssh://git@github.com/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3",
"license": "MIT"
},
"node_modules/dom-to-image-more": {
"version": "3.2.0",
"license": "MIT"
},
"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==",
"license": "MIT",
"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"
},
@ -22640,6 +22721,13 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true
},
"node_modules/domutils": {
"version": "3.1.0",
"dev": true,
@ -25929,6 +26017,12 @@
"version": "6.0.0",
"license": "MIT"
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/figures": {
"version": "3.2.0",
"dev": true,
@ -28659,6 +28753,20 @@
"node": ">=6"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"dev": true,
@ -33251,6 +33359,24 @@
"node": "*"
}
},
"node_modules/jspdf": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.5.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jsprim": {
"version": "1.4.2",
"dev": true,
@ -43191,7 +43317,7 @@
},
"node_modules/performance-now": {
"version": "2.1.0",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/periscopic": {
@ -44746,7 +44872,7 @@
},
"node_modules/raf": {
"version": "3.4.1",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"performance-now": "^2.1.0"
@ -48352,6 +48478,16 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"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==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": {
"version": "6.0.1",
"license": "ISC",
@ -50008,6 +50144,16 @@
"node": ">=8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/static-eval": {
"version": "2.1.0",
"license": "MIT",
@ -50563,6 +50709,16 @@
"dev": true,
"license": "MIT"
},
"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==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/svgo": {
"version": "3.2.0",
"dev": true,
@ -51024,6 +51180,16 @@
"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==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"license": "MIT"
@ -52416,6 +52582,16 @@
"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==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "3.4.0",
"dev": true,
@ -58112,7 +58288,9 @@
}
},
"plugins/legacy-preset-chart-nvd3/node_modules/dompurify": {
"version": "3.1.0",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
},
"plugins/plugin-chart-echarts": {
@ -68699,7 +68877,9 @@
},
"dependencies": {
"dompurify": {
"version": "3.1.0"
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ=="
}
}
},
@ -69831,6 +70011,12 @@
"@types/qs": {
"version": "6.9.7"
},
"@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"optional": true
},
"@types/range-parser": {
"version": "1.2.4"
},
@ -72271,8 +72457,7 @@
"peer": true
},
"atob": {
"version": "2.1.2",
"dev": true
"version": "2.1.2"
},
"atomic-sleep": {
"version": "1.0.0",
@ -72766,6 +72951,12 @@
"base16": {
"version": "1.0.0"
},
"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==",
"optional": true
},
"base64-js": {
"version": "1.5.1"
},
@ -73091,6 +73282,11 @@
"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"
},
@ -73396,6 +73592,30 @@
"caniuse-lite": {
"version": "1.0.30001639"
},
"canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.38.1",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"optional": true
}
}
},
"capture-exit": {
"version": "2.0.0",
"dev": true,
@ -74471,6 +74691,15 @@
"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==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
},
"css-loader": {
"version": "6.8.1",
"dev": true,
@ -75800,9 +76029,22 @@
"entities": "^4.2.0"
}
},
"dom-to-image": {
"version": "git+ssh://git@github.com/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"
},
"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"
},
@ -75816,6 +76058,12 @@
"domelementtype": "^2.3.0"
}
},
"dompurify": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz",
"integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==",
"optional": true
},
"domutils": {
"version": "3.1.0",
"dev": true,
@ -77982,6 +78230,11 @@
"fetch-retry": {
"version": "6.0.0"
},
"fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"figures": {
"version": "3.2.0",
"dev": true,
@ -79740,6 +79993,16 @@
}
}
},
"html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"requires": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
}
},
"htmlparser2": {
"version": "8.0.2",
"dev": true,
@ -82719,6 +82982,21 @@
"through": ">=2.2.7 <3"
}
},
"jspdf": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"requires": {
"@babel/runtime": "^7.23.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"canvg": "^3.0.6",
"core-js": "^3.38.1",
"dompurify": "^2.5.4",
"fflate": "^0.8.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"jsprim": {
"version": "1.4.2",
"dev": true,
@ -88598,7 +88876,7 @@
},
"performance-now": {
"version": "2.1.0",
"dev": true
"devOptional": true
},
"periscopic": {
"version": "3.1.0",
@ -89546,7 +89824,7 @@
},
"raf": {
"version": "3.4.1",
"dev": true,
"devOptional": true,
"requires": {
"performance-now": "^2.1.0"
}
@ -91831,6 +92109,12 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"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==",
"optional": true
},
"rimraf": {
"version": "6.0.1",
"requires": {
@ -92964,6 +93248,12 @@
}
}
},
"stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"optional": true
},
"static-eval": {
"version": "2.1.0",
"requires": {
@ -93308,6 +93598,12 @@
"version": "2.0.4",
"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==",
"optional": true
},
"svgo": {
"version": "3.2.0",
"dev": true,
@ -93610,6 +93906,15 @@
"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==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
},
"text-table": {
"version": "0.2.0"
},
@ -94483,6 +94788,15 @@
"version": "1.0.1",
"dev": true
},
"utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"requires": {
"base64-arraybuffer": "^1.0.2"
}
},
"uuid": {
"version": "3.4.0",
"dev": true

View File

@ -137,6 +137,7 @@
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.2.0",
"dom-to-pdf": "^0.3.2",
"emotion-rgba": "0.0.12",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",

View File

@ -60,6 +60,8 @@ export enum FeatureFlag {
UseAnalagousColors = 'USE_ANALAGOUS_COLORS',
ForceSqlLabRunAsync = 'SQLLAB_FORCE_RUN_ASYNC',
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT',
}
export type ScheduleQueriesProps = {

View File

@ -23,13 +23,20 @@ import { Menu } from 'src/components/Menu';
import downloadAsImage from 'src/utils/downloadAsImage';
import DownloadAsImage from './DownloadAsImage';
const mockAddDangerToast = jest.fn();
jest.mock('src/utils/downloadAsImage', () => ({
__esModule: true,
default: jest.fn(() => (_e: SyntheticEvent) => {}),
}));
jest.mock('src/components/MessageToasts/withToasts', () => ({
useToasts: () => ({
addDangerToast: mockAddDangerToast,
}),
}));
const createProps = () => ({
addDangerToast: jest.fn(),
text: 'Download as Image',
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
@ -40,22 +47,24 @@ const renderComponent = () => {
<Menu>
<DownloadAsImage {...createProps()} />
</Menu>,
{
useRedux: true,
},
);
};
test('Should call download image on click', async () => {
const props = createProps();
renderComponent();
await waitFor(() => {
expect(downloadAsImage).toHaveBeenCalledTimes(0);
expect(props.addDangerToast).toHaveBeenCalledTimes(0);
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
await waitFor(() => {
expect(downloadAsImage).toHaveBeenCalledTimes(1);
expect(props.addDangerToast).toHaveBeenCalledTimes(0);
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
});

View File

@ -21,20 +21,20 @@ import { logging, t } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
import downloadAsImage from 'src/utils/downloadAsImage';
import { useToasts } from 'src/components/MessageToasts/withToasts';
export default function DownloadAsImage({
text,
logEvent,
dashboardTitle,
addDangerToast,
...rest
}: {
text: string;
addDangerToast: Function;
dashboardTitle: string;
logEvent?: Function;
}) {
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
const { addDangerToast } = useToasts();
const onDownloadImage = async (e: SyntheticEvent) => {
try {
downloadAsImage(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);

View File

@ -0,0 +1,73 @@
/**
* 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 { SyntheticEvent } 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 downloadAsPdf from 'src/utils/downloadAsPdf';
import DownloadAsPdf from './DownloadAsPdf';
const mockAddDangerToast = jest.fn();
jest.mock('src/utils/downloadAsPdf', () => ({
__esModule: true,
default: jest.fn(() => (_e: SyntheticEvent) => {}),
}));
jest.mock('src/components/MessageToasts/withToasts', () => ({
useToasts: () => ({
addDangerToast: mockAddDangerToast,
}),
}));
const createProps = () => ({
text: 'Export as PDF',
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
});
const renderComponent = () => {
render(
<Menu>
<DownloadAsPdf {...createProps()} />
</Menu>,
{ useRedux: true },
);
};
test('Should call download pdf on click', async () => {
renderComponent();
await waitFor(() => {
expect(downloadAsPdf).toHaveBeenCalledTimes(0);
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));
await waitFor(() => {
expect(downloadAsPdf).toHaveBeenCalledTimes(1);
expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
});
});
test('Component is rendered with role="button"', async () => {
renderComponent();
const button = screen.getByRole('button', { name: 'Export as PDF' });
expect(button).toBeInTheDocument();
});

View File

@ -0,0 +1,55 @@
/**
* 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 { SyntheticEvent } from 'react';
import { logging, t } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import downloadAsPdf from 'src/utils/downloadAsPdf';
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF } from 'src/logger/LogUtils';
import { useToasts } from 'src/components/MessageToasts/withToasts';
export default function DownloadAsPdf({
text,
logEvent,
dashboardTitle,
...rest
}: {
text: string;
dashboardTitle: string;
logEvent?: Function;
}) {
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
const { addDangerToast } = useToasts();
const onDownloadPdf = async (e: SyntheticEvent) => {
try {
downloadAsPdf(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
} catch (error) {
logging.error(error);
addDangerToast(t('Sorry, something went wrong. Try again later.'));
}
logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF);
};
return (
<Menu.Item key="download-pdf" {...rest}>
<div onClick={onDownloadPdf} role="button" tabIndex={0}>
{text}
</div>
</Menu.Item>
);
}

View File

@ -130,6 +130,9 @@ describe('DownloadScreenshot component', () => {
await waitFor(() => {
expect(mockAddInfoToast).toHaveBeenCalledWith(
'The screenshot is being generated. Please, do not leave the page.',
{
noDuplicate: true,
},
);
});
});
@ -202,7 +205,7 @@ describe('DownloadScreenshot component', () => {
// Wait for the successful image retrieval message
await waitFor(() => {
expect(mockAddSuccessToast).toHaveBeenCalledWith(
'The screenshot is now being downloaded.',
'The screenshot has been downloaded.',
);
});
});

View File

@ -33,6 +33,7 @@ import { useSelector } from 'react-redux';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { last } from 'lodash';
import { getDashboardUrlParams } from 'src/utils/urlUtils';
import { useCallback, useEffect, useRef } from 'react';
import { DownloadScreenshotFormat } from './types';
const RETRY_INTERVAL = 3000;
@ -53,21 +54,66 @@ export default function DownloadScreenshot({
const activeTabs = useSelector(
(state: RootState) => state.dashboardState.activeTabs || undefined,
);
const anchor = useSelector(
(state: RootState) =>
last(state.dashboardState.directPathToChild) || undefined,
);
const dataMask = useSelector(
(state: RootState) => state.dataMask || undefined,
);
const { addDangerToast, addSuccessToast, addInfoToast } = useToasts();
const currentIntervalIds = useRef<NodeJS.Timeout[]>([]);
const printLoadingToast = () =>
addInfoToast(
t('The screenshot is being generated. Please, do not leave the page.'),
{
noDuplicate: true,
},
);
const printFailureToast = useCallback(
() =>
addDangerToast(
t('The screenshot could not be downloaded. Please, try again later.'),
),
[addDangerToast],
);
const printSuccessToast = useCallback(
() => addSuccessToast(t('The screenshot has been downloaded.')),
[addSuccessToast],
);
const stopIntervals = useCallback(
(message?: 'success' | 'failure') => {
currentIntervalIds.current.forEach(clearInterval);
if (message === 'failure') {
printFailureToast();
}
if (message === 'success') {
printSuccessToast();
}
},
[printFailureToast, printSuccessToast],
);
const onDownloadScreenshot = () => {
let retries = 0;
const toastIntervalId = setInterval(
() => printLoadingToast(),
RETRY_INTERVAL,
);
currentIntervalIds.current = [
...(currentIntervalIds.current || []),
toastIntervalId,
];
printLoadingToast();
// this function checks if the image is ready
const checkImageReady = (cacheKey: string) =>
SupersetClient.get({
@ -85,6 +131,7 @@ export default function DownloadScreenshot({
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
stopIntervals('success');
})
.catch(err => {
if ((err as SupersetApiError).status === 404) {
@ -92,34 +139,15 @@ export default function DownloadScreenshot({
}
});
// this is the functions that handles the retries
const fetchImageWithRetry = (cacheKey: string) => {
checkImageReady(cacheKey)
.then(() => {
addSuccessToast(t('The screenshot is now being downloaded.'));
})
.catch(error => {
// we check how many retries have been made
if (retries < MAX_RETRIES) {
retries += 1;
addInfoToast(
t(
'The screenshot is being generated. Please, do not leave the page.',
),
{
noDuplicate: true,
},
);
setTimeout(() => fetchImageWithRetry(cacheKey), RETRY_INTERVAL);
} else {
addDangerToast(
t(
'The screenshot could not be downloaded. Please, try again later.',
),
);
logging.error(error);
}
});
if (retries >= MAX_RETRIES) {
stopIntervals('failure');
logging.error('Max retries reached');
return;
}
checkImageReady(cacheKey).catch(() => {
retries += 1;
});
};
SupersetClient.post({
@ -136,18 +164,15 @@ export default function DownloadScreenshot({
if (!cacheKey) {
throw new Error('No image URL in response');
}
addInfoToast(
t(
'The screenshot is being generated. Please, do not leave the page.',
),
);
const retryIntervalId = setInterval(() => {
fetchImageWithRetry(cacheKey);
}, RETRY_INTERVAL);
currentIntervalIds.current.push(retryIntervalId);
fetchImageWithRetry(cacheKey);
})
.catch(error => {
logging.error(error);
addDangerToast(
t('The screenshot could not be downloaded. Please, try again later.'),
);
stopIntervals('failure');
})
.finally(() => {
logEvent?.(
@ -158,6 +183,16 @@ export default function DownloadScreenshot({
});
};
useEffect(
() => () => {
if (currentIntervalIds.current.length > 0) {
stopIntervals();
}
currentIntervalIds.current = [];
},
[stopIntervals],
);
return (
<Menu.Item key={format} {...rest}>
<div onClick={onDownloadScreenshot} role="button" tabIndex={0}>

View File

@ -17,8 +17,11 @@
* under the License.
*/
import { Menu } from 'src/components/Menu';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import DownloadScreenshot from './DownloadScreenshot';
import { DownloadScreenshotFormat } from './types';
import DownloadAsPdf from './DownloadAsPdf';
import DownloadAsImage from './DownloadAsImage';
export interface DownloadMenuItemProps {
pdfMenuItemTitle: string;
@ -34,25 +37,48 @@ const DownloadMenuItems = (props: DownloadMenuItemProps) => {
imageMenuItemTitle,
logEvent,
dashboardId,
dashboardTitle,
...rest
} = props;
const isWebDriverScreenshotEnabled =
isFeatureEnabled(FeatureFlag.EnableDashboardScreenshotEndpoints) &&
isFeatureEnabled(FeatureFlag.EnableDashboardDownloadWebDriverScreenshot);
return (
<Menu selectable={false}>
<DownloadScreenshot
text={pdfMenuItemTitle}
dashboardId={dashboardId}
logEvent={logEvent}
format={DownloadScreenshotFormat.PDF}
{...rest}
/>
<DownloadScreenshot
text={imageMenuItemTitle}
dashboardId={dashboardId}
logEvent={logEvent}
format={DownloadScreenshotFormat.PNG}
{...rest}
/>
{isWebDriverScreenshotEnabled ? (
<>
<DownloadScreenshot
text={pdfMenuItemTitle}
dashboardId={dashboardId}
logEvent={logEvent}
format={DownloadScreenshotFormat.PDF}
{...rest}
/>
<DownloadScreenshot
text={imageMenuItemTitle}
dashboardId={dashboardId}
logEvent={logEvent}
format={DownloadScreenshotFormat.PNG}
{...rest}
/>
</>
) : (
<>
<DownloadAsPdf
text={pdfMenuItemTitle}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
{...rest}
/>
<DownloadAsImage
text={imageMenuItemTitle}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
{...rest}
/>
</>
)}
</Menu>
);
};

View File

@ -0,0 +1,36 @@
/**
* 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 'dom-to-pdf' {
interface Image {
type: string;
quality: number;
}
interface Options {
margin: number;
filename: string;
image: Image;
html2canvas: object;
excludeClassNames?: string[];
}
function domToPdf(elementToPrint: Element, options?: Options): Promise<any>;
export default domToPdf;
}

View File

@ -0,0 +1,74 @@
/**
* 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 { SyntheticEvent } from 'react';
import domToPdf from 'dom-to-pdf';
import { kebabCase } from 'lodash';
import { logging, t } from '@superset-ui/core';
import { addWarningToast } from 'src/components/MessageToasts/actions';
/**
* generate a consistent file stem from a description and date
*
* @param description title or description of content of file
* @param date date when file was generated
*/
const generateFileStem = (description: string, date = new Date()) =>
`${kebabCase(description)}-${date.toISOString().replace(/[: ]/g, '-')}`;
/**
* Create an event handler for turning an element into an image
*
* @param selector css selector of the parent element which should be turned into image
* @param description name or a short description of what is being printed.
* Value will be normalized, and a date as well as a file extension will be added.
* @param isExactSelector if false, searches for the closest ancestor that matches selector.
* @returns event handler
*/
export default function downloadAsPdf(
selector: string,
description: string,
isExactSelector = false,
) {
return (event: SyntheticEvent) => {
const elementToPrint = isExactSelector
? document.querySelector(selector)
: event.currentTarget.closest(selector);
if (!elementToPrint) {
return addWarningToast(
t('PDF download failed, please refresh and try again.'),
);
}
const options = {
margin: 10,
filename: `${generateFileStem(description)}.pdf`,
image: { type: 'jpeg', quality: 1 },
html2canvas: { scale: 2 },
excludeClassNames: ['header-controls'],
};
return domToPdf(elementToPrint, options)
.then(() => {
// nothing to be done
})
.catch((e: Error) => {
logging.error('PDF generation failed', e);
});
};
}

View File

@ -478,6 +478,14 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
"PRESTO_EXPAND_DATA": False,
# Exposes API endpoint to compute thumbnails
"THUMBNAILS": False,
# Enable the endpoints to cache and retrieve dashboard screenshots via webdriver.
# Requires configuring Celery and a cache using THUMBNAIL_CACHE_CONFIG.
"ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS": False,
# Generate screenshots (PDF or JPG) of dashboards using the web driver.
# When disabled, screenshots are generated on the fly by the browser.
# This feature flag is used by the download feature in the dashboard view.
# It is dependent on ENABLE_DASHBOARD_SCREENSHOT_ENDPOINT being enabled.
"ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT": False,
"SHARE_QUERIES_VIA_KV_STORE": False,
"TAGGING_SYSTEM": False,
"SQLLAB_BACKEND_PERSISTENCE": True,

View File

@ -156,12 +156,18 @@ def with_dashboard(
class DashboardRestApi(BaseSupersetModelRestApi):
datamodel = SQLAInterface(Dashboard)
@before_request(only=["thumbnail"])
@before_request(only=["thumbnail", "cache_dashboard_screenshot", "screenshot"])
def ensure_thumbnails_enabled(self) -> Optional[Response]:
if not is_feature_enabled("THUMBNAILS"):
return self.response_404()
return None
@before_request(only=["cache_dashboard_screenshot", "screenshot"])
def ensure_screenshots_enabled(self) -> Optional[Response]:
if not is_feature_enabled("ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS"):
return self.response_404()
return None
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
RouteMethod.EXPORT,
RouteMethod.IMPORT,
@ -1133,7 +1139,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
dashboard_id=dashboard.id,
dashboard_url=dashboard_url,
cache_key=cache_key,
force=True,
force=False,
thumb_size=thumb_size,
window_size=window_size,
)

View File

@ -3025,7 +3025,9 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
return self.client.get(uri)
@pytest.mark.usefixtures("create_dashboard_with_tag")
def test_cache_dashboard_screenshot_success(self):
@patch("superset.dashboards.api.is_feature_enabled")
def test_cache_dashboard_screenshot_success(self, is_feature_enabled):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
dashboard = (
db.session.query(Dashboard)
@ -3036,7 +3038,9 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
assert response.status_code == 202
@pytest.mark.usefixtures("create_dashboard_with_tag")
def test_cache_dashboard_screenshot_dashboard_validation(self):
@patch("superset.dashboards.api.is_feature_enabled")
def test_cache_dashboard_screenshot_dashboard_validation(self, is_feature_enabled):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
dashboard = (
db.session.query(Dashboard)
@ -3052,7 +3056,9 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
response = self._cache_screenshot(dashboard.id, invalid_payload)
assert response.status_code == 400
def test_cache_dashboard_screenshot_dashboard_not_found(self):
@patch("superset.dashboards.api.is_feature_enabled")
def test_cache_dashboard_screenshot_dashboard_not_found(self, is_feature_enabled):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
non_existent_id = 999
response = self._cache_screenshot(non_existent_id)
@ -3061,10 +3067,14 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
@pytest.mark.usefixtures("create_dashboard_with_tag")
@patch("superset.dashboards.api.cache_dashboard_screenshot")
@patch("superset.dashboards.api.DashboardScreenshot.get_from_cache_key")
def test_screenshot_success_png(self, mock_get_cache, mock_cache_task):
@patch("superset.dashboards.api.is_feature_enabled")
def test_screenshot_success_png(
self, is_feature_enabled, mock_get_cache, mock_cache_task
):
"""
Validate screenshot returns png
"""
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
mock_cache_task.return_value = None
mock_get_cache.return_value = BytesIO(b"fake image data")
@ -3087,12 +3097,14 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
@patch("superset.dashboards.api.cache_dashboard_screenshot")
@patch("superset.dashboards.api.build_pdf_from_screenshots")
@patch("superset.dashboards.api.DashboardScreenshot.get_from_cache_key")
@patch("superset.dashboards.api.is_feature_enabled")
def test_screenshot_success_pdf(
self, mock_get_from_cache, mock_build_pdf, mock_cache_task
self, is_feature_enabled, mock_get_from_cache, mock_build_pdf, mock_cache_task
):
"""
Validate screenshot can return pdf.
"""
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
mock_cache_task.return_value = None
mock_get_from_cache.return_value = BytesIO(b"fake image data")
@ -3115,7 +3127,11 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
@pytest.mark.usefixtures("create_dashboard_with_tag")
@patch("superset.dashboards.api.cache_dashboard_screenshot")
@patch("superset.dashboards.api.DashboardScreenshot.get_from_cache_key")
def test_screenshot_not_in_cache(self, mock_get_cache, mock_cache_task):
@patch("superset.dashboards.api.is_feature_enabled")
def test_screenshot_not_in_cache(
self, is_feature_enabled, mock_get_cache, mock_cache_task
):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
mock_cache_task.return_value = None
mock_get_cache.return_value = None
@ -3132,7 +3148,9 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
response = self._get_screenshot(dashboard.id, cache_key, "pdf")
assert response.status_code == 404
def test_screenshot_dashboard_not_found(self):
@patch("superset.dashboards.api.is_feature_enabled")
def test_screenshot_dashboard_not_found(self, is_feature_enabled):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
non_existent_id = 999
response = self._get_screenshot(non_existent_id, "some_cache_key", "png")
@ -3141,7 +3159,11 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
@pytest.mark.usefixtures("create_dashboard_with_tag")
@patch("superset.dashboards.api.cache_dashboard_screenshot")
@patch("superset.dashboards.api.DashboardScreenshot.get_from_cache_key")
def test_screenshot_invalid_download_format(self, mock_get_cache, mock_cache_task):
@patch("superset.dashboards.api.is_feature_enabled")
def test_screenshot_invalid_download_format(
self, is_feature_enabled, mock_get_cache, mock_cache_task
):
is_feature_enabled.return_value = True
self.login(ADMIN_USERNAME)
mock_cache_task.return_value = None
mock_get_cache.return_value = BytesIO(b"fake png data")
@ -3158,3 +3180,20 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
response = self._get_screenshot(dashboard.id, cache_key, "invalid")
assert response.status_code == 404
@pytest.mark.usefixtures("create_dashboard_with_tag")
@patch("superset.dashboards.api.is_feature_enabled")
def test_cache_dashboard_screenshot_feature_disabled(self, is_feature_enabled):
is_feature_enabled.return_value = False
self.login(ADMIN_USERNAME)
dashboard = (
db.session.query(Dashboard)
.filter(Dashboard.dashboard_title == "dash with tag")
.first()
)
assert dashboard is not None
response = self._cache_screenshot(dashboard.id)
assert response.status_code == 404