[Tutorial] Creating an embedded Metabase Internal Page

This is how to embed Metabase into an internal Frappe page:

  1. Set up Metabase.

  2. Go into Metabase Settings/Admin/Embedding in Other Applications and enable embedding.

  3. Make your dashboard.

  4. Enable sharing of the dashboard

  1. Click on Embed

image

  1. Choose your settings.

  1. Click the code button

image

  1. Copy the Python and Mustache embed code. Don’t forget to PUBLISH the embedded dashboard.

  1. Go into Frappe and make a new Page

  1. Go to your page folder in whatever module you made your new page.

image

  1. Make a .html file and a .py file with the same name as the .js file. In my example, it is called “test_page”

image

  1. Go into the test_page.js file and modify the code to look like this, where “test_page” is the name of your html file without the html extension:

frappe.pages[‘test-page’].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: ‘A test page’,
single_column: true
});
$(frappe.render_template(‘test_page’)).appendTo(page.body);
}

  1. Install pyJWT

pip install PyJWT

  1. Paste your python code from Metabase into the .py file and add a print command as shown:
import jwt

METABASE_SITE_URL = "http://localhost:3000"
METABASE_SECRET_KEY = "XXXXXXXX"

payload = {
"resource": {"dashboard": 1},
"params": {
    
}
}
token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm="HS256")

iframeUrl = METABASE_SITE_URL + "/embed/dashboard/" + token.decode("utf8") + "#theme=night&bordered=true&titled=true"
print(iframeUrl)
  1. Go to your terminal, run python test_page.py and copy the output URL

  2. Remove the print command from the test_page.py file.

  3. Copy the mustache code into the test_page.html file, replace src with your URL from step 15. Modify the length and width as desired.

<p><iframe
src="http://localhost:3000/embed/dashboard/hfgdo8gheorhgorgnlskg#bordered"
frameborder="0"
 width="100%"
 height=2300
allowtransparency
</iframe></p>
  1. Go to your site url (example: http://localhost:8081/desk#test-page)

12 Likes

After embedding it on our site “Powered by metabase” footer will display on bottom of our page???

Yes… to remove branding we should opt for premium embedding which is expensive($3000/Year).

Any idea on how to keep the metabase.jar launched (ON) in production mode.

As everytime it goes down i have to manually execute the command

java -jar metabase.jar

either you can use nohup java -jar metabase.jar & command
or
you can refer below for use metabase as a service
https://numediaweb.com/metabase-service-ubuntu/1589

2 Likes

Hi,

Great tutorial but I want to point out a few things.

  1. You don’t need to install pyJWT since it’s in frappe requirements.
  2. You might want to tweak code from tutorial a bit (Adding expiration for token) to improve security.

Improving security

Using code from tutorial, user can copy iframe url and share with anyone then access those metabase dashboard anytime, anywhere.

By adding expiration for token it’ll limit valid time of those token more detail “Metabase Discourse - Embed Security (research shared)”.

__init__.py file

# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import frappe
import jwt
import time


@frappe.whitelist()
def get_url():
	METABASE_SITE_URL = 'http://localhost:3000'
	METABASE_SECRET_KEY = 'YOUR-SECRET-KEY'

	payload = {
		'resource': {'dashboard': 1},
		'params': {},
		'exp': round(time.time()) + (60 * 10)  # 10 minute expiration
	}
	token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm='HS256')

	iframeUrl = METABASE_SITE_URL + '/embed/dashboard/' + token.decode('utf8') + '#bordered=true&titled=false'

	return iframeUrl

page_name.js file

frappe.pages['page_name'].on_page_load = function(wrapper) {
	const page = frappe.ui.make_app_page({
		parent: wrapper,
		title: 'Page Title',
		single_column: true,
	});

	const createIframe = (page, iframeUrl) => {
		const iframe = `
		<iframe
			src="${iframeUrl}"
			frameborder="0"
			width="100%"
			height="600"
			allowtransparency
		></iframe>
		`;
		$(iframe).appendTo(page.body);
	};

	frappe.call({
		'method': 'app_name.app_name.page.page_name.get_url',
		'callback': function(r) {
			const url = r.message;
			if (url) {
				createIframe(page, url);
			}
		},
	});
};

Optional: Remove key from codebase

Use Frappe site config to store URL and Key

bench/site/site.name/site_config.json

{
 "db_name": "YOUR_DB_NAME",
 "db_password": "YOUR_DB_PASS",
 "metabase_site_url": "http://YOUR_METABASE.URL",
 "metabase_secret_key": "YOUR_METABASE_KEY",
 "developer_mode": 1,
}

__init__.py file

# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import frappe
import jwt
import time


@frappe.whitelist()
def get_url():
	METABASE_SITE_URL = frappe.conf.metabase_site_url
	METABASE_SECRET_KEY = frappe.conf.metabase_secret_key

	payload = {
		'resource': {'dashboard': 1},
		'params': {},
		'exp': round(time.time()) + (60 * 10)  # 10 minute expiration
	}
	token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm='HS256')

	iframeUrl = METABASE_SITE_URL + '/embed/dashboard/' + token.decode('utf8') + '#bordered=true&titled=false'

	return iframeUrl

Thank you for the tutorial.
Cheers!

4 Likes

how did you solve this problem, same issue with me ?

Not tried to resolve it. but now v13 has good dashboard feature which is worth exploring.

v13.6 I am implementing this and receiving this error. If Anyone can help, that would be much appreciated. I have opened a Github issue plus another thread but no response received.

This is the traceback when I check the console:

/api/method/frappe.desk.desk_page.getpage:1 Failed to load resource: the server responded with a status of 500 (INTERNAL SERVER ERROR)
request.js:387 Traceback (most recent call last):
  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 68, in application
    response = frappe.api.handle()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/api.py", line 55, in handle
    return frappe.handler.handle()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/handler.py", line 31, in handle
    data = execute_cmd(cmd)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/handler.py", line 67, in execute_cmd
    return frappe.call(method, **frappe.form_dict)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 1172, in call
    return fn(*args, **newargs)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/desk/desk_page.py", line 32, in getpage
    doc = get(page)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/desk/desk_page.py", line 15, in get
    page.load_assets()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/core/doctype/page/page.py", line 113, in load_assets
    self.script = render_include(f.read())
  File "/home/frappe/frappe-bench/env/lib/python3.6/codecs.py", line 321, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x91 in position 187: invalid start byte