Skip to content

Instantly share code, notes, and snippets.

@michaelpayne02
Last active April 27, 2024 09:34
Show Gist options
  • Save michaelpayne02/cd0c0b628560079b46c9dffce9157495 to your computer and use it in GitHub Desktop.
Save michaelpayne02/cd0c0b628560079b46c9dffce9157495 to your computer and use it in GitHub Desktop.
iOS Grafana widgets using Scriptable and grafana-image-renderer

Grafana iOS widgets

Graphs are generated server-side with grafana-image-renderer. The script fetches the image and displays it on the widget. All home screen, lock screen, and apple watch widget types are supported.

Server Setup

  1. Install Docker
  2. Download docker-compose.yaml and place it in a folder
$ mkdir grafana && cd grafana && wget https://gist.githubusercontent.com/michaelpayne02/cd0c0b628560079b46c9dffce9157495/raw/751a82a877cb7c1615d861b3d648ec1283f13f6b/docker-compose.yml
  1. Bring up Grafana stack
$ docker compose up -d

Grafana Setup

  1. Create a service account in your Grafana instance.
  2. Create a dashboard to hold the panels or use an existing one. For best results, turn on Transparent background under Panel Options. You should delete the title as well because it's provided via the script using the system font.

Device Configuration

  1. Download Scriptable
  2. Long press the raw link for the file below and select Download Linked File to save the script. Open it from the downloads folder and use the share sheet to import it into Scriptable.
  3. Click the share icon in the top right corner of any Grafana panel. You will be presented with a URL directly linking to the chosen panel. Use this information to update all the configuration variables at the top of the script except for deviceSize.
  4. Find your corresponding device resolution on Apple Developer Docs and set the deviceSize variable. For most retina devices, divide the pixel dimensions by 3 to get pts. Forolder, non-retina devices, divide by 2.
  5. Add a widget to your home screen or lock screen, and select the script "Grafana."
  6. Fill in the Parameter field using this format:
< widget type: sm|med|lg|rect|circ>,<panelId>,<title>,<title alignment: l|c|r>

The default config is med,1,Grafana,c

Dark/light mode

Unfortunately, Scriptable widgets do not support automatic dark/light mode switching outside of the native widget background and text colors. To change the color scheme, edit the darkMode variable in the configuration section of the script. Set it to true for dark mode and false for light mode.

services:
grafana:
container_name: grafana
image: grafana/grafana
restart: unless-stopped
volumes:
- grafana:/var/lib/grafana
environment:
GF_RENDERING_SERVER_URL: "http://renderer:8081/render"
GF_RENDERING_CALLBACK_URL: "http://grafana:3000/"
RENDERING_MODE: "reusable"
GF_LOG_FILTERS: "rendering:debug"
ENABLE_METRICS: true
renderer:
container_name: grafana_image_renderer
image: grafana/grafana-image-renderer:latest
environment:
BROWSER_TZ: "${TZ}"
volumes:
grafana:
{
"always_run_in_app" : false,
"icon" : {
"color" : "orange",
"glyph" : "chart-area"
},
"name" : "Grafana",
"script" : "\/\/ Configuration\nconst url = \"http:\/\/grafana.example.com\";\nconst token = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\";\nconst orgId = 1;\nconst dashboardName = \"widgets\";\nconst dashboardId = \"xxxxxxxxxxxxxx\";\nconst tz = \"America\/Chicago\";\nconst darkMode = true;\nconst deviceSize = \"393x852\";\n\nparams = config.runsInWidget\n ? args.widgetParameter?.split(\",\") : []\n\nconst opts = {\n type: params[0] ? params[0] : \"md\",\n panel: params[1] ? params[1] : \"1\",\n from: params[2] ? params[2] : \"now-30m\",\n to: params[3] ? params[3] : \"now\",\n title: params[4] ? params[4] : \"Grafana\",\n align: params[5] ? params[5] : \"c\",\n};\n\n\/\/ Scriptable limits displayed image resolution to a max of 500x500\nconst imageSizes = {\n sm: new Size(474, 474),\n md: new Size(500, 210),\n lg: new Size(500, 500),\n re: new Size(480, 216),\n ci: new Size(216, 216),\n};\n\nconst widgetSizes = {\n \/\/ iOS sizes\n \"430x932\": {\n sm: new Size(170, 170),\n md: new Size(364, 170),\n lg: new Size(364, 382),\n ci: new Size(76, 76),\n re: new Size(172, 76),\n },\n \"428x926\": {\n sm: new Size(170, 170),\n md: new Size(364, 170),\n lg: new Size(364, 382),\n ci: new Size(76, 76),\n re: new Size(172, 76),\n },\n \"414x896\": {\n sm: new Size(169, 169),\n md: new Size(360, 169),\n lg: new Size(360, 379),\n ci: new Size(76, 76),\n re: new Size(160, 72),\n },\n \"414x736\": {\n sm: new Size(159, 159),\n md: new Size(348, 157),\n lg: new Size(348, 357),\n ci: new Size(76, 76),\n re: new Size(170, 76),\n },\n \"393x852\": {\n sm: new Size(158, 158),\n md: new Size(338, 158),\n lg: new Size(338, 354),\n ci: new Size(72, 72),\n re: new Size(160, 72),\n },\n \"390x844\": {\n sm: new Size(158, 158),\n md: new Size(338, 158),\n lg: new Size(338, 354),\n ci: new Size(72, 72),\n re: new Size(160, 72),\n },\n \"375x812\": {\n sm: new Size(155, 155),\n md: new Size(329, 155),\n lg: new Size(329, 345),\n ci: new Size(72, 72),\n re: new Size(157, 72),\n },\n \"375x667\": {\n sm: new Size(148, 148),\n md: new Size(321, 148),\n lg: new Size(321, 324),\n ci: new Size(68, 68),\n re: new Size(153, 68),\n },\n \"360x780\": {\n sm: new Size(155, 155),\n md: new Size(329, 155),\n lg: new Size(329, 345),\n ci: new Size(72, 72),\n re: new Size(157, 72),\n },\n \"320x568\": {\n sm: new Size(141, 141),\n md: new Size(292, 141),\n lg: new Size(292, 311),\n ci: null,\n re: null,\n },\n \/\/ iPadOS sizes\n \"768x1024\": {\n sm: new Size(141, 141),\n md: new Size(305.5, 141),\n lg: new Size(305.5, 305.5),\n lg: new Size(634.5, 305.5),\n xl: new Size(634.5, 305.5),\n },\n \"744x1133\": {\n sm: new Size(141, 141),\n md: new Size(305.5, 141),\n lg: new Size(305.5, 305.5),\n lg: new Size(634.5, 305.5),\n xl: new Size(634.5, 305.5),\n },\n \"810x1080\": {\n sm: new Size(146, 146),\n md: new Size(320.5, 146),\n lg: new Size(320.5, 320.5),\n lg: new Size(669, 320.5),\n xl: new Size(669, 320.5),\n },\n \"820x1180\": {\n sm: new Size(155, 155),\n md: new Size(342, 155),\n lg: new Size(342, 342),\n lg: new Size(715.5, 342),\n xl: new Size(715.5, 342),\n },\n \"834x1112\": {\n sm: new Size(150, 150),\n md: new Size(327.5, 150),\n lg: new Size(327.5, 327.5),\n lg: new Size(682, 327.5),\n xl: new Size(682, 327.5),\n },\n \"834x1194\": {\n sm: new Size(155, 155),\n md: new Size(342, 155),\n lg: new Size(342, 342),\n lg: new Size(715.5, 342),\n xl: new Size(715.5, 342),\n },\n \"954x1373\": {\n sm: new Size(162, 162),\n md: new Size(350, 162),\n lg: new Size(350, 350),\n lg: new Size(726, 350),\n xl: new Size(726, 350),\n },\n \"970x1389\": {\n sm: new Size(162, 162),\n md: new Size(350, 162),\n lg: new Size(350, 350),\n lg: new Size(726, 350),\n xl: new Size(726, 350),\n },\n \"1024x1366\": {\n sm: new Size(170, 170),\n md: new Size(378.5, 170),\n lg: new Size(378.5, 378.5),\n lg: new Size(795, 378.5),\n xl: new Size(795, 378.5),\n },\n \"1192x1590\": {\n sm: new Size(188, 188),\n md: new Size(412, 188),\n lg: new Size(412, 412),\n lg: new Size(860, 412),\n xl: new Size(860, 412),\n },\n\n \/\/ Apple watch sizes\n \"304x139\": { sm: new Size(304, 139) },\n \"330x145\": { sm: new Size(330, 145) },\n \"346x153\": { sm: new Size(346, 153) },\n \"368x161\": { sm: new Size(368, 161) },\n \"382x163\": { sm: new Size(382, 163) },\n};\n\n\/\/ Get widget dimensions based on device size\nsize = widgetSizes[deviceSize][opts.type];\n\n\/\/ Get image dimensions based on widget type\nconst imageSize = imageSizes[opts.type];\nconst theme = darkMode ? \"dark\" : \"light\";\n\n\/\/ Main container\nconst list = new ListWidget();\nlist.setPadding(0, 0, 0, 0);\nlist.backgroundColor = new Color(darkMode ? \"#111216\" : \"#F4F5F5\");\nlist.url = `${url}\/d\/${dashboardId}\/${dashboardName}?orgId=${orgId}&viewPanel=${opts.panel}&from=${opts.from}&to=${opts.to}kiosk`;\n\n\/\/ Exclude lock screen widgets\nif (!(opts.type == \"re\" || opts.type == \"ci\")) {\n list.addSpacer(5);\n const textStack = list.addStack();\n\n \/\/ In order for spacers to function correly, the stack must be the full width of the widget\n textStack.size = new Size(size.width, 0);\n textStack.setPadding(5, 0, 0, 0);\n\n \/\/ Add flexible or fixed spacer based on alignment\n \/\/ Text is centered horizontally in stacks by default\n if (opts.align == \"l\") textStack.addSpacer(15);\n if (opts.align == \"r\") textStack.addSpacer(null);\n\n const text = textStack.addText(opts.title);\n text.font = Font.mediumSystemFont(12);\n text.textColor = new Color(darkMode ? \"#ccccdc\" : \"#24292e\");\n\n if (opts.align == \"l\") textStack.addSpacer(null);\n if (opts.align == \"r\") textStack.addSpacer(15);\n}\n\n\/\/ Image container\nconst imageStack = list.addStack();\nimageStack.size = new Size(size.width, 0);\n\nconst image_url = `${url}\/render\/d-solo\/${dashboardId}\/${dashboardName}?orgId=${orgId}&panelId=${opts.panel}&from=${opts.from}&to=${opts.to}&width=${imageSize.width}&height=${imageSize.height}&theme=${theme}&tz=${tz}`;\n\nconst req = new Request(image_url);\nreq.headers = {\n Authorization: `Bearer ${token}`,\n};\n\nconst image = await req.loadImage();\nconst imageWidget = imageStack.addImage(image);\n\nif (opts.type == \"re\") imageWidget.cornerRadius = 12;\n\nif (opts.type == \"ci\") imageWidget.cornerRadius = 100;\n\nif (config.runsInWidget) {\n Script.setWidget(list);\n} else {\n switch (opts.type) {\n case \"sm\":\n list.presentSmall();\n break;\n case \"md\":\n list.presentmdium();\n break;\n case \"lg\":\n list.presentLarge();\n break;\n case \"re\":\n list.presentAccessoryRectangular();\n break;\n case \"ci\":\n list.presentAccessoryCircular();\n break;\n case \"li\":\n list.presentAccessoryInline();\n break;\n }\n}\n\nScript.complete();",
"share_sheet_inputs" : [
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment