This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e29777526 fix(Dashboard): Sync/Async Dashboard Screenshot Generation 
and Default Cache (#30755)
3e29777526 is described below

commit 3e297775265eff51ac700826282e3924fe57ea84
Author: Geido <[email protected]>
AuthorDate: Fri Nov 1 11:39:43 2024 +0200

    fix(Dashboard): Sync/Async Dashboard Screenshot Generation and Default 
Cache (#30755)
    
    Co-authored-by: Michael S. Molina <[email protected]>
    Co-authored-by: Michael S. Molina 
<[email protected]>
---
 .github/workflows/dependency-review.yml            |   2 +-
 superset-frontend/package-lock.json                | 332 ++++++++++++++++++++-
 superset-frontend/package.json                     |   1 +
 .../superset-ui-core/src/utils/featureFlags.ts     |   2 +
 .../DownloadMenuItems/DownloadAsImage.test.tsx     |  17 +-
 .../menu/DownloadMenuItems/DownloadAsImage.tsx     |   4 +-
 ...loadAsImage.test.tsx => DownloadAsPdf.test.tsx} |  35 ++-
 .../{DownloadAsImage.tsx => DownloadAsPdf.tsx}     |  20 +-
 .../DownloadMenuItems/DownloadScreenshot.test.tsx  |   5 +-
 .../menu/DownloadMenuItems/DownloadScreenshot.tsx  | 111 ++++---
 .../components/menu/DownloadMenuItems/index.tsx    |  54 +++-
 superset-frontend/src/types/dom-to-pdf.d.ts        |  36 +++
 superset-frontend/src/utils/downloadAsPdf.ts       |  74 +++++
 superset/config.py                                 |   8 +
 superset/dashboards/api.py                         |  10 +-
 tests/integration_tests/dashboards/api_tests.py    |  55 +++-
 16 files changed, 663 insertions(+), 103 deletions(-)

diff --git a/.github/workflows/dependency-review.yml 
b/.github/workflows/dependency-review.yml
index 773e735834..11ed2d7720 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -32,4 +32,4 @@ jobs:
           #   license: https://applitools.com/legal/open-source-terms-of-use/
           # pkg:npm/[email protected]
           #   selecting BSD-3-Clause licensing terms for node-forge to ensure 
compatibility with Apache
-          allow-dependencies-licenses: pkg:npm/[email protected], 
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/[email protected]
+          allow-dependencies-licenses: pkg:npm/[email protected], 
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/[email protected], pkg:npm/rgbcolor
diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index 83acfcd534..a859168b7c 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -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://[email protected]/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://[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"
     },
+    "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
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 736c294de0..b373eb041b 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -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",
diff --git 
a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts 
b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index 8801706c55..be28944a91 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -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 = {
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
index 7e9d9226df..8401ece73c 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
@@ -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);
   });
 });
 
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
index 0cb3f1fbb4..505a9b8184 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
@@ -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);
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
similarity index 64%
copy from 
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
copy to 
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
index 7e9d9226df..56916f4b64 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
@@ -20,17 +20,24 @@ 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 downloadAsImage from 'src/utils/downloadAsImage';
-import DownloadAsImage from './DownloadAsImage';
+import downloadAsPdf from 'src/utils/downloadAsPdf';
+import DownloadAsPdf from './DownloadAsPdf';
 
-jest.mock('src/utils/downloadAsImage', () => ({
+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 = () => ({
-  addDangerToast: jest.fn(),
-  text: 'Download as Image',
+  text: 'Export as PDF',
   dashboardTitle: 'Test Dashboard',
   logEvent: jest.fn(),
 });
@@ -38,29 +45,29 @@ const createProps = () => ({
 const renderComponent = () => {
   render(
     <Menu>
-      <DownloadAsImage {...createProps()} />
+      <DownloadAsPdf {...createProps()} />
     </Menu>,
+    { useRedux: true },
   );
 };
 
-test('Should call download image on click', async () => {
-  const props = createProps();
+test('Should call download pdf on click', async () => {
   renderComponent();
   await waitFor(() => {
-    expect(downloadAsImage).toHaveBeenCalledTimes(0);
-    expect(props.addDangerToast).toHaveBeenCalledTimes(0);
+    expect(downloadAsPdf).toHaveBeenCalledTimes(0);
+    expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
   });
 
-  userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
+  userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));
 
   await waitFor(() => {
-    expect(downloadAsImage).toHaveBeenCalledTimes(1);
-    expect(props.addDangerToast).toHaveBeenCalledTimes(0);
+    expect(downloadAsPdf).toHaveBeenCalledTimes(1);
+    expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
   });
 });
 
 test('Component is rendered with role="button"', async () => {
   renderComponent();
-  const button = screen.getByRole('button', { name: 'Download as Image' });
+  const button = screen.getByRole('button', { name: 'Export as PDF' });
   expect(button).toBeInTheDocument();
 });
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
similarity index 69%
copy from 
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
copy to 
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
index 0cb3f1fbb4..a07a2e232c 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
@@ -19,35 +19,35 @@
 import { SyntheticEvent } from 'react';
 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 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 DownloadAsImage({
+export default function DownloadAsPdf({
   text,
   logEvent,
   dashboardTitle,
-  addDangerToast,
   ...rest
 }: {
   text: string;
-  addDangerToast: Function;
   dashboardTitle: string;
   logEvent?: Function;
 }) {
   const SCREENSHOT_NODE_SELECTOR = '.dashboard';
-  const onDownloadImage = async (e: SyntheticEvent) => {
+  const { addDangerToast } = useToasts();
+  const onDownloadPdf = async (e: SyntheticEvent) => {
     try {
-      downloadAsImage(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
+      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_IMAGE);
+    logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF);
   };
 
   return (
-    <Menu.Item key="download-image" {...rest}>
-      <div onClick={onDownloadImage} role="button" tabIndex={0}>
+    <Menu.Item key="download-pdf" {...rest}>
+      <div onClick={onDownloadPdf} role="button" tabIndex={0}>
         {text}
       </div>
     </Menu.Item>
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
index e1851e6199..9c8922f621 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
@@ -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.',
       );
     });
   });
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
index 85f3e1d2c4..17ec6ee8d8 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
@@ -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}>
diff --git 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
index e0ec06d9b2..875537fb8e 100644
--- 
a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
+++ 
b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
@@ -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>
   );
 };
diff --git a/superset-frontend/src/types/dom-to-pdf.d.ts 
b/superset-frontend/src/types/dom-to-pdf.d.ts
new file mode 100644
index 0000000000..061e80d96c
--- /dev/null
+++ b/superset-frontend/src/types/dom-to-pdf.d.ts
@@ -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;
+}
diff --git a/superset-frontend/src/utils/downloadAsPdf.ts 
b/superset-frontend/src/utils/downloadAsPdf.ts
new file mode 100644
index 0000000000..bb769d1eb1
--- /dev/null
+++ b/superset-frontend/src/utils/downloadAsPdf.ts
@@ -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);
+      });
+  };
+}
diff --git a/superset/config.py b/superset/config.py
index d19e30a5a5..354e60c571 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -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,
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 733cc555a4..a752091cc1 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -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,
             )
diff --git a/tests/integration_tests/dashboards/api_tests.py 
b/tests/integration_tests/dashboards/api_tests.py
index c3bbf97536..e02b5a116a 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -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

Reply via email to