Skip to content

Instantly share code, notes, and snippets.

@svet-b
Last active October 22, 2024 21:15
Show Gist options
  • Save svet-b/1ad0656cd3ce0e1a633e16eb20f66425 to your computer and use it in GitHub Desktop.
Save svet-b/1ad0656cd3ce0e1a633e16eb20f66425 to your computer and use it in GitHub Desktop.
PDF export of Grafana dashboard using puppeteer
Display the source blob
Display the rendered blob
Raw

Automated PDF export of Grafana dashboard using Puppeteer

Prerequisites

General:

  • Grafana server with dashboards that are to be exported, and datasources in "Server" (proxy) mode.
  • User account on Grafana server that has Viewer access to the required dashboards
  • This has been tested on Ubuntu 16.04 and a Mac

Packages:

  • NodeJS, and the puppeteer package (npm install puppeteer), which is used to run headless Chrome
  • In Linux, Puppeteer has the following library/tool dependencies (primarily related to libx11 - see this post). I found that I didn't need extra packages on a Mac.
sudo apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

Scripts:

  • The grafana_pdf.js file attached here, which carries out the PDF conversion using Puppeteer

Process

Environment: Set the Grafana server URL, username, and password, and the output filename as environment variables.

export GF_DASH_URL="http://localhost:3000/d/x3g4Wx5ik/new-dashboard?kiosk"
export GF_USER=pdf_export
export GF_PASSWORD=StrongPassword1
export OUTPUT_PDF=output.pdf

Now export to PDF by calling the NodeJS script with the corresponding arguments:

node grafana_pdf.js $GF_DASH_URL $GF_USER:$GF_PASSWORD $OUTPUT_PDF

Notes and caveats

  • The focus here is on single-page output. Getting "tall" dashboards to paginate nicely is an altogether separate endeavor.
  • In its present form, the script adjusts the PDF and aspect ratio to fit the dashboard, with no regard for fitting on an actual page. It's also possible to get the output to be Letter or A4 sized - see comments in the code on how to achieve that; if the intent is to print, you'll probably also want to add a margin (TODO: Add a switch in the code)
  • When you have a single pdf_export user that is a member of multiple organizations, if you try to exporting dashboards belonging to different organizations one after the other, you will occasionally get a "login" screen instead of a dashboard during the org switch. When that happens, I found that simply retrying does the trick.

Example output

Attached below are two example output PDFs (bigdashboard_output.pdf and output_energy.pdf). The former is based on https://play.grafana.org/d/000000003/big-dashboard, and the latter is from our own energy monitoring project (www.ammp.io).

'use strict';
const puppeteer = require('puppeteer');
// URL to load should be passed as first parameter
const url = process.argv[2];
// Username and password (with colon separator) should be second parameter
const auth_string = process.argv[3];
// Output file name should be third parameter
const outfile = process.argv[4];
// TODO: Output an error message if number of arguments is not right or arguments are invalid
// Set the browser width in pixels. The paper size will be calculated on the basus of 96dpi,
// so 1200 corresponds to 12.5".
const width_px = 1200;
// Note that to get an actual paper size, e.g. Letter, you will want to *not* simply set the pixel
// size here, since that would lead to a "mobile-sized" screen (816px), and mess up the rendering.
// Instead, set e.g. double the size here (1632px), and call page.pdf() with format: 'Letter' and
// scale = 0.5.
// Generate authorization header for basic auth
const auth_header = 'Basic ' + new Buffer.from(auth_string).toString('base64');
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Set basic auth headers
await page.setExtraHTTPHeaders({'Authorization': auth_header});
// Increase timeout from the default of 30 seconds to 120 seconds, to allow for slow-loading panels
await page.setDefaultNavigationTimeout(120000);
// Increasing the deviceScaleFactor gets a higher-resolution image. The width should be set to
// the same value as in page.pdf() below. The height is not important
await page.setViewport({
width: width_px,
height: 800,
deviceScaleFactor: 2,
isMobile: false
})
// Wait until all network connections are closed (and none are opened withing 0.5s).
// In some cases it may be appropriate to change this to {waitUntil: 'networkidle2'},
// which stops when there are only 2 or fewer connections remaining.
await page.goto(url, {waitUntil: 'networkidle0'});
// Hide all panel description (top-left "i") pop-up handles and, all panel resize handles
// Annoyingly, it seems you can't concatenate the two object collections into one
await page.evaluate(() => {
let infoCorners = document.getElementsByClassName('panel-info-corner');
for (el of infoCorners) { el.hidden = true; };
let resizeHandles = document.getElementsByClassName('react-resizable-handle');
for (el of resizeHandles) { el.hidden = true; };
});
// Get the height of the main canvas, and add a margin
var height_px = await page.evaluate(() => {
return document.getElementsByClassName('react-grid-layout')[0].getBoundingClientRect().bottom;
}) + 20;
await page.pdf({
path: outfile,
width: width_px + 'px',
height: height_px + 'px',
// format: 'Letter', <-- see note above for generating "paper-sized" outputs
scale: 1,
displayHeaderFooter: false,
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
});
await browser.close();
})();
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@svet-b
Copy link
Author

svet-b commented Jun 20, 2019

Hi @Optimusbumble - I'm afraid I didn't get a chance to play around with pagination properly. Indeed, I assume it's just a matter of passing the right set of parameters to puppeteer. Would love to hear more if you ever work it out!

@MarkoBursic
Copy link

MarkoBursic commented Jul 16, 2019

Tried on Raspberry Pi 3B:

node grafana_pdf.js $GF_DASH_URL $GF_USER:$GF_PASSWORD $OUTPUT_PDF
(node:2764) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!
/home/pi/node_modules/puppeteer/.local-chromium/linux-672088/chrome-linux/chrome: 2: /home/pi/node_modules/puppeteer/.local-chromium/linux-672088/chrome-linux/chrome: Syntax error: Unterminated quoted string
TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

at onClose (/home/pi/node_modules/puppeteer/lib/Launcher.js:340:14)
at Interface.helper.addEventListener (/home/pi/node_modules/puppeteer/lib/Launcher.js:329:50)
at emitNone (events.js:111:20)
at Interface.emit (events.js:208:7)
at Interface.close (readline.js:370:8)
at Socket.onend (readline.js:149:10)
at emitNone (events.js:111:20)
at Socket.emit (events.js:208:7)
at endReadableNT (_stream_readable.js:1064:12)
at _combinedTickCallback (internal/process/next_tick.js:138:11)

(node:2764) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:2764) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

@mv00621q
Copy link

Hi @MarkoBursic you shoud try to replace the line 26 of the script by this one const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); it should be enough for your issue.

@Ramesh-Karnati
Copy link

Ramesh-Karnati commented Aug 29, 2019

Hi @svet-b My Linux machine doesn't have active internet to install puppeteer through npm command (npm install puppeteer). Could someone suggest how i can I install puppeteer offline in linux machine.

@salvq
Copy link

salvq commented Feb 7, 2020

Hello @mv00621q Can you help to identify what is the issue ? I am trying to run pdf generation by crontab every x hour.

Standard execution from shell and using const browser = await puppeteer.launch(); works just fine
Running via shell generate the file
$ /bin/sh /tmp/grafana_report_pup.sh

grafana_report_pup.sh
node /tmp/grafana_pdf.js http://localhost:3000/d/qP-sbQEZz/new-dashboard?kiosk $GF_USER:$GF_PASSWORD $GF_OUT

However when I run via crontab it says error about no sandbox.

# m h  dom mon dow   command
44 10 * * * /bin/sh /tmp/grafana_report_pup.sh  > /tmp/graf.log 2>&1

Therefore I replaced as per suggestion const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); but still getting below error, what do I need to do to make it possible to execute via crontab ?

(node:56385) UnhandledPromiseRejectionWarning: Error: Evaluation failed: TypeError: Cannot read property 'getBoundingClientRect' of undefined
    at __puppeteer_evaluation_script__:2:67
    at ExecutionContext._evaluateInternal (/tmp/node_modules/puppeteer/lib/ExecutionContext.js:122:13)
    at process._tickCallback (internal/process/next_tick.js:68:7)
  -- ASYNC --
    at ExecutionContext.<anonymous> (/tmp/node_modules/puppeteer/lib/helper.js:111:15)
    at DOMWorld.evaluate (/tmp/node_modules/puppeteer/lib/DOMWorld.js:112:20)
    at process._tickCallback (internal/process/next_tick.js:68:7)
  -- ASYNC --
    at Frame.<anonymous> (/tmp/node_modules/puppeteer/lib/helper.js:111:15)
    at Page.evaluate (/tmp/node_modules/puppeteer/lib/Page.js:860:43)
    at Page.<anonymous> (/tmp/node_modules/puppeteer/lib/helper.js:112:23)
    at /tmp/grafana_pdf.js:59:30
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:56385) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:56385) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

EDIT: issue here are variables, using values instead of variables solved the issue

@salvq
Copy link

salvq commented Feb 7, 2020

1. dockerfile, need to update grafana_pdf.js line to const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});, nano and sendemail are optional as I am using those for further process or changes

FROM buildkite/puppeteer

WORKDIR .

COPY grafana_pdf.js ./

# update before install
RUN  apt-get update \
     && apt-get install -y sendemail \
     && apt-get install -y nano

# just run the container doing nothing
ENTRYPOINT ["sh", "-c", "sleep infinity"]

2. build image
sudo docker build -t pup:latest .

3. run docker (variables just an example)

sudo docker run -d \
--name="pup" \
--network=monitoring \
--ip 172.18.0.50 \
--net-alias=pup \
-v /home/aqmp/test:/pdf-reports pup

PDF generation from docker
node /grafana_pdf.js http://grafana:3000/d/qP-sbQEZz/new-dashboard?kiosk LOGIN:PASSWORD /pdf-reports/test.pdf

PDF creation from host
$ sudo docker exec -it pup node /grafana_pdf.js http://grafana:3000/d/qP-sbQEZz/new-dashboard?kiosk LOGIN:PASSWORD /pdf-reports/test.pdf

PDF creation from crontab (need to ensure no -it arguments)

# m h  dom mon dow   command
28 18 * * * docker exec pup node /grafana_pdf.js http://grafana:3000/d/qP-sbQEZz/new-dashboard?kiosk LOGIN:PASSWORD /pdf-reports/test.pdf

@salvq
Copy link

salvq commented Feb 8, 2020

Does anybody knows how to proceed with HTTP API to get a pdf instead of node execution ? I am using node type for crontab daily report generation however access to adhoc pdf generation would be great.

Example of HTTP call which would return pdf
http://localhost:1234/api/$DashboardId?apitoken=GrafanaTokenId

I would like to use report link on my dashboard to generate the report.

image

image

@Coreeze
Copy link

Coreeze commented May 18, 2020

Got any luck with this @salvq?

@salvq
Copy link

salvq commented May 18, 2020

@Coreeze no luck yet :) I gave up and switched to grafana docker image renderer for the link report.

I would definitely rather use PDF however did not have time for this yet

@edakacj
Copy link

edakacj commented Aug 6, 2020

there was a small problem.
it is necessary to save the dashbor with heavy graphs in the pdf.
as a result, pdf is half empty.
https://yadi.sk/i/vILdWpSyL-oz6Q

increasing the delay of 120 seconds did not solve the problem.
(in my browser it takes 60 seconds to load charts).

there are no problems with otalny dashboards. only with this.
have any ideas how to increase browser performance on puppeteer?

@svet-b
Copy link
Author

svet-b commented Aug 6, 2020

@salvq I'm not sure I have the answer to your questions unfortunately. What I did do, separately, was write an AWS Lambda function in Python (should probably also run under WSGI/Docker/etc) which uses Pyppeteer (and headless Chrome) to provide an API interface for dashboard downloads. It's somewhat half-baked, but I'd be happy to share if helpful.

I'm also conscious that the Grafana team has made a lot of progress in this respect, especially with their image renderer plugin. Or indeed their built-in reporting functionality (https://grafana.com/docs/grafana/latest/features/reporting/ ...only for Grafana Enterprise unfortunately).

@edakacj with respect to the issue you mentioned, am I right in thinking that some of your panels are rendered while others are not? This looks to me like a potential result of "lazy loading" of panels (https://grafana.com/blog/2019/07/08/a-closer-look-at-lazy-loading-grafana-dashboards/), rather than a PDF rendering/timeout issue. In short, since your dashboard is so "tall", some of your panels are outside the (headless) Chromium window, and are therefore not rendered. If this diagnosis is right, the best solution would be to play with the viewport (increase the height?) and maybe the scale factor, in order to ensure the whole dashboard is (virtually) visible and loaded.

@edakacj
Copy link

edakacj commented Aug 6, 2020

@edakacj with respect to the issue you mentioned, am I right in thinking that some of your panels are rendered while others are not? This looks to me like a potential result of "lazy loading" of panels (https://grafana.com/blog/2019/07/08/a-closer-look-at-lazy-loading-grafana-dashboards/), rather than a PDF rendering/timeout issue. In short, since your dashboard is so "tall", some of your panels are outside the (headless) Chromium window, and are therefore not rendered. If this diagnosis is right, the best solution would be to play with the viewport (increase the height?) and maybe the scale factor, in order to ensure the whole dashboard is (virtually) visible and loaded.

Thank you so much!
I increased the value for "height".
and that solved the problem!

@kartik468
Copy link

Hi @Optimusbumble - I'm afraid I didn't get a chance to play around with pagination properly. Indeed, I assume it's just a matter of passing the right set of parameters to puppeteer. Would love to hear more if you ever work it out!

@svet-b I am able to update the code and it works even with tall dashboards. If anyone interested checkout the code at

https://github.com/kartik468/grafana-generate-pdf-nodejs

@justynroberts
Copy link

@svet-b Just tried this with Docker/Grafana 7.1.0 - The panels render blank, ie No data series - I assume I have to wait for rendering to complete. Any thoughts ? I only tested with a couple of stock charts? Cheers

@mpetitjean
Copy link

Hi all, I'm using this code regularly. However, I noticed that the behavior is very inconsistent when working with images (i.e. text fields pointing to the URL of an image). While all the graphs are rendered properly, the images are sometimes not appearing. I am not able to properly reproduce and/or fix. Any ideas ?

@coelh0
Copy link

coelh0 commented Aug 21, 2021

Hi guys, we are using this application with docker and we adjusted it to use apikey and not user:password anymore, it works very well, but we have two problems.

  1. I'm trying to generate a pdf of a dashboard that has an option to choose a kubernetes node to see the graphs of those specific nodes, I saw that the pdf generated by calling the url of the dashboard takes the default value of the nodes option (and I can't use the "all" option). I thought of doing a "for" lace with the nodes name and using this array to collect from this dashboard a pdf of all nodes in different pdf files, is there any other way to do this?

  2. Some dashboards that I need to generate a pdf do not have the chart (panels) visible, I have to expand the chart (panel) manually to be able to see the values, can I use any parameter to expand the visualization of the charts (panels) before generating the pdf? I can't edit the dashboard because it's in a kubernetes and doesn't accept edits.

An observation, I needed to use a time range to generate my reports, I managed to do this by passing a parameter in the URL, if someone needs to do that too, I managed it this way.

This is my problem:
graph

Sorry for my bad english! Thank you!

@svet-b
Copy link
Author

svet-b commented Aug 21, 2021

Hi @coelh0 thanks for the thorough explanation!

For your question #1, I assume you do want separate PDFs for each node, rather than a single "PDF" for all nodes? If that's the case, then yes, I think you need to run a for loop and generate PDFs individually for each node. If you do want a single PDF, you should be able to pass a var-<YourVarName>=All parameter in the URL, which will be the same as clicking "all" in the drop-down.

For your second question, I'm afraid I don't have a good answer. Are you able to maybe clone the dashboard to a different location where you can edit it? Alternatively, I think there is a way to simulate clicks on those headings in the headless browser, but it might not be straightforward.

@coelh0
Copy link

coelh0 commented Aug 21, 2021

Hi @coelh0 thanks for the thorough explanation!

For your question #1, I assume you do want separate PDFs for each node, rather than a single "PDF" for all nodes? If that's the case, then yes, I think you need to run a for loop and generate PDFs individually for each node. If you do want a single PDF, you should be able to pass a var-<YourVarName>=All parameter in the URL, which will be the same as clicking "all" in the drop-down.

For your second question, I'm afraid I don't have a good answer. Are you able to maybe clone the dashboard to a different location where you can edit it? Alternatively, I think there is a way to simulate clicks on those headings in the headless browser, but it might not be straightforward.

Hi @svet-b! thanks for the quick response.

About #1

I understand that I could use "All" to make the dashboard show them all in a single graph. The problem is that when using this option, some dashboard items get broken.

The loop has solved the problem for now. I had to statically pass the name of the kubernetes nodes in the loop, I will try to identify the nodes via API (if possible).

I'm using a docker-compose and creating an array of all grafana's dashboards via APIKey (adjusting the names and ID's with JQ) and after that, I pass this array to your application that generates a pdf for each dashboard.

About #2

As I can't save the Elastic dashboard with the lines (panels) expanded (because the dashboard is applied via kubernetes services), I did the following: I manually expanded all the lines (panels) that were collapsed and copied the json code from the dashboard in this format . I edited the grafana service in kubernetes referring to the Elastic dashboard and applied this json. I was able to generate the PDF with all dashboard expanded items (panels), but this generated a new problem (#3) [lol]:

Now Elastic's dashboard needs more than one page. Only the graphics that fit on the first page of the PDF are displayed, the others (next pages of the PDF) show only the name and not the graphic. Is there any way to solve this?


By the way, whoever wants to use the APIkey and not user:password, just adjust it in the grafana_pdf.js file:

// Generate authorization header for basic auth
--//const auth_header = 'Basic ' + new Buffer.from(auth_string).toString('base64');
++const token = 'Bearer ' + auth_string;

// Set basic auth headers
--//await page.setExtraHTTPHeaders({'Authorization': auth_header});
++await page.setExtraHTTPHeaders({'Authorization': token});

Create an APIKey at Grafana and use with this command:

node grafana_pdf.js "http//grafanaurl.com/d/728b76cc/k8s-net-workload?orgId=1?kiosk" TOKEN out_file.pdf

Using with root, need this:
(async() => {
--//const browser = await puppeteer.launch();
++const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
const page = await browser.newPage();

Hope it helps someone!
And Thanks again!

@kartik468
Copy link

@coelh0 You can checkout this repo. It caters long dashboard issue
https://github.com/kartik468/grafana-generate-pdf-nodejs

@nillay
Copy link

nillay commented Apr 1, 2022

even thought I have dark theme why does the pdf get generated in white theme...

@softwareklinic
Because it is printing the pdf without background.
add printBackground: true, in the code for printing it with background

Example code block:


    await page.pdf({
      path: outfile,
      width: width_px + 'px',
      height: height_px + 'px',
      //    format: 'Letter', <-- see note above for generating "paper-sized" outputs
      scale: 1,
      displayHeaderFooter: false,
      printBackground: true,
      margin: {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0,
        
      },
    });

@edakacj
Copy link

edakacj commented Sep 19, 2022

I need to connect to a site using a certificate (.pem)

How can i do this?

@subodh2711
Copy link

subodh2711 commented Oct 12, 2022

Hi @coelh0 , i am getting blank pdf with error while opening "The dimensions of this page are out-of-range. Page content might be truncated.". can you please help.

@Bapths
Copy link

Bapths commented Feb 21, 2023

Hi everyone,

I am sorry to revive this conversation again but I managed to do something that may help. I'm not that much comfortable with nodeJS so I tried to do something similar using Python and playwright and I finally came with a satisfying solution for my needs.

import asyncio
from typing import Optional
from playwright.async_api import async_playwright


async def generate_pdf(
        target_url: str,
        auth_token: Optional[str],
        destination_file: str,
        page_width: int = 1920,
        margin: int = 80
) -> None:

    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page(device_scale_factor=4)  # You can change the scale_factor to +/- images quality
        page.set_default_navigation_timeout(120000.0)
        await page.set_viewport_size({'width': page_width, 'height': 1080})
        await page.set_extra_http_headers(
            {'Authorization': f'Bearer {auth_token}'}
        )
        await page.goto(target_url)
        await page.wait_for_selector('.react-grid-layout')
        page_height = await page.evaluate(
            'document.getElementsByClassName(\'react-grid-layout\')[0].getBoundingClientRect().bottom;'
        )
        await page.set_viewport_size({'width': page_width, 'height': page_height})
        await page.wait_for_load_state('networkidle')
        await page.pdf(
            path=destination_file,
            scale=1,
            width=f'{page_width + margin * 2}px',
            height=f'{page_height + margin * 2}px',
            display_header_footer=False,
            print_background=True,
            margin={
                'top': f'{margin}px',
                'bottom': f'{margin}px',
                'left': f'{margin}px',
                'right': f'{margin}px',
            }
        )
        await browser.close()


async def main():

    target_url = 'YOUR_DASHBOARD_URL'  # Tip: add '&kiosk' at the end of your grafana URL to hide tool tabs
    grafana_auth_token: str = 'YOUR_API_KEY'
    destination_file: str = f'./grafana_report.pdf'

    await generate_pdf(
        target_url, grafana_auth_token, destination_file
    )

asyncio.run(main())

To sum it up, it opens the page, wait for the panels to load, get the page total height, set the viewport height to the page total height and print the result in a PDF file. The code might be still up-gradable but it work as it is.

Thanks for your really helpful tips and ideas! 😉

@kami619
Copy link

kami619 commented Aug 31, 2023

@Bapths I can confirm that this playwright python script works even now :)

just a note to install the below pip3 packages prior to the execution of the script

pip3 install playwright asyncio typing

and if you see a missing playwright._impl._api_types.Error: Executable doesn't exist at exception, just run the playwright install to download the needed browsers for the script to work.

@rafalkrupinski
Copy link

I'm on grafana 10.1 and it seems the panels are dynamically loaded and you need to scroll down to load them. For now I'm using height:2000 in page.setViewport, but there's also a way to make puppeteer scroll down.

@rafalkrupinski
Copy link

OK, I've tried page.scrollTo, scrollBy and

 const [scroller] = await page.$x('//div[contains(@style, "overflow: scroll")]');
if (scroller) {
  await scroller.evaluate(node => {
    node.scrollTop = node.scrollHeight;
  });
}```

Neither of which worked for me. I'll stick with `height` attribute.

@rafalkrupinski
Copy link

Ah, it's the kiosk mode disabling scrolling...

@zach-betz-hln
Copy link

Thank you for this example @svet-b.
It allowed us to create a somewhat printer friendly single page pdf.
For future readers, here's a Playwright snippet that worked for us, see code comments.

// Setup...

// Our tallest known dashboard is 3010px
// So set an arbitrarily high height so that all panels are loaded, which will be lowered later to fit the content
await page.setViewportSize({ width: 1920, height: 6000 });

// Enable kiosk mode in case the user is an admin
dashboardUrl.searchParams.set('kiosk', '1');
await page.goto(dashboardUrl.href);
await page.waitForLoadState('networkidle');

const dashboardHeight = await page.evaluate(() => {
  return document.querySelector('.react-grid-layout')?.getBoundingClientRect().bottom ?? 0;
});
// Fit the page to the content
await page.setViewportSize({ width: 1920, height: dashboardHeight });
await page.waitForLoadState('networkidle');

const margin = '10px';
const pdf = await page.pdf({
  scale: 0.4, // Tweak this to your needs
  format: 'Letter',
  printBackground: true,
  margin: {
    top: margin,
    bottom: margin,
    left: margin,
    right: margin
  }
});

// Do stuff with the pdf...

@arthur-mdn
Copy link

I've been working on docker and used part of your script to make it easier to configure : https://github.com/arthur-mdn/grafana-export-to-pdf

@dlc-unisim
Copy link

puppeteer by default paginates the pages unlike here.. but changing to Letter format is giving some hope where it acts as pivot . First row is changed to first page here. But subsequent rows are missing. Any suggestions here ?

Hi @Optimusbumble, did you got success with pagination? I'm trying to do the same, without success. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment