将数据可视化发布到 Web
与互联网上的优秀人士分享独立的图表和仪表板。
在本教程中,我们将向您展示几种发布 Metabase 图表和仪表板的选项,从最不费力到最费力。
我们将从无需代码的公共链接开始,逐步介绍只需单个代码片段的公共嵌入,最后为那些希望启动自己的 Web 应用程序的人提供示例代码。
如果您想了解何时以及为何选择一种发布方式而不是另一种方式的一些示例,请查看Metabase 疑团。
公共链接和嵌入
要快速分享问题和仪表板,您只需发送一个公共链接,或在您的网站(或任何渲染 HTML 的地方)中放置一个 iframe。这是即时分享图表和仪表板的好方法。
分享选项
假设我们要分享一个仪表板。我们将点击分享图标并选择分享和嵌入选项。
这将显示我们的分享选项
公共链接
公共链接是分享仪表板最简单的方式。公共链接甚至不是嵌入;它们只是指向单个问题或仪表板的链接,尽管这些公共项目与它们的原始版本略有不同。
这些公共链接将包含一个由 Metabase 提供支持的页脚,如果您有专业版或企业版计划,可以将其删除。图表也将禁用钻取功能,并且我们无法自定义仪表板上的点击行为。
如果我们设置了默认筛选器值,Metabase 将把该筛选器应用到问题或仪表板。人们将能够更改筛选器,因此我们不能依赖筛选器来限制人们可以看到的数据。要锁定(或隐藏)筛选器,我们需要使用静态嵌入。
我们还可以格式化 URL 以向筛选器分配值,或者完全隐藏筛选器——但请记住,收件人可以简单地编辑 URL。
虽然这些公共链接很难猜测,但任何拥有该链接的人都可以查看我们的仪表板,因此它不是分享敏感数据的最佳解决方案。不过,我们可以快速将公共链接分享给仪表板(例如,与客户),然后一旦他们看过,就将其禁用。如果我们再次分享仪表板,Metabase 将生成一个新链接(因此我们无需担心旧链接的访问)。如果您不小心分享了链接,您可以随时禁用它;只需将分享切换为关闭。管理员可以从管理面板查看和禁用所有公共链接。
公共嵌入
我们可以使用 iframe 在我们的网站中嵌入问题或仪表板。这就像从 Metabase 复制粘贴代码并将其放入网页的源代码中一样简单。我们甚至可以在无代码网站构建器中使用它——任何我们可以放入 HTML 的地方。例如,您可以在博客中嵌入仪表板以帮助用数据讲故事,或者简单地用仪表板填满整个页面。
这是一个显示仪表板的 iframe
<iframe
src="http://your-website.com/public/dashboard/f54f5ae5-39a4-4662-85d5-a02e78165279"
frameborder="0"
width="800"
height="600"
allowtransparency
></iframe>
iframe 在当前浏览器窗口中创建一个嵌套的浏览器窗口。iframe 窗口指向其自己的 URL,并显示该地址的响应——在本例中,是我们要显示的图表或仪表板。与公共链接一样,图表将带有由 Metabase 提供支持的页脚。
我们可以调整宽度和高度以适应我们的图表或仪表板。如果我们要嵌入仪表板,并且 iframe 的宽度不足以容纳仪表板布局,Metabase 将按照它们在仪表板上从左到右出现的顺序堆叠问题。
启用在其他应用程序中嵌入
如果我们要限制谁可以看到我们的图表或仪表板,或锁定过滤器,我们需要使用静态嵌入。此功能独立于公共共享选项,并且仅限管理员访问。我们还需要启用 Embedding in other Applications
设置才能使其工作。在管理面板的设置选项卡中,我们将单击 Embedding in other Applications
并切换 Enabled
。
让我们回到我们要分享的仪表板。启用嵌入后,我们会得到一个密钥。
要设置静态嵌入,我们需要在服务器上插入一些代码来为我们的用户签署 JSON Web 令牌 (JWT)。Metabase 将为 Clojure、Python、Ruby 和 JavaScript (Node.js) 生成代码,但您应该能够将该代码翻译成其他语言编写的服务器。
在点击发布按钮之前,让我们回顾一下一些选项。
隐藏或锁定参数以限制显示的数据
如果我们的问题或仪表板有过滤器,我们可以禁用过滤器,或锁定参数以设置固定的过滤器值。
假设我们要向某人展示一个仪表板,但我们只希望他们看到 Gadget
类别中的订单。
在我们的示例中,参数设置了仪表板上的过滤器。这里我们将类别过滤器设置为 Gadget
。
这是一些用 Clojure 编写的服务器示例代码
(require '[buddy.sign.jwt :as jwt])
(def metabase-site-url "MY-DOMAIN-HERE")
(def metabase-secret-key "SECRET-KEY-HERE")
(def payload
{:resource {:dashboard 3}
:params {"category" ["Gadget"]}
:exp (+ (int (/ (System/currentTimeMillis) 1000)) (* 60 10))}) ; 10 minute expiration
(def token (jwt/sign payload metabase-secret-key))
(def iframe-url (str metabase-site-url "/embed/dashboard/" token "#bordered=true&titled=true"))
此代码将使用 Metabase 给我们的密钥来签署 JWT 令牌:我们的用户将不会——也不应该——看到密钥。
事情是这样的。
- 在 Metabase 中,我们发布要嵌入到应用程序中的仪表板。
- 我们将 iframe 插入到应用程序的页面中。
- 我们将签署 JSON Web 令牌的代码放在我们的服务器中。
- 我们的用户登录到我们的应用程序。
- 用户请求我们应用程序中带有嵌入式仪表板的页面。
- 当服务器处理该页面的请求时,它将签署用户的令牌,并将该令牌作为 iframe 的源 URL 插入。
- 页面加载时,页面使用签名令牌从我们的 Metabase 实例请求仪表板。
- 仪表板在我们的应用程序中的 iframe 中加载,其中包含由我们的应用程序服务器设置的参数和过期时间。
如果令牌未签名或以任何方式被修改,仪表板将无法加载。
在 payload
映射中,我们可以指定签名的 JWT 何时过期(在上面的代码中,令牌在 10 分钟后过期)。
:params
字段是我们可以锁定参数以在仪表板或问题上设置默认固定过滤器的地方。例如,我们可以创建一个带有用户 ID 过滤器的仪表板,然后在我们的服务器上以编程方式将用户 ID 作为参数插入。签名令牌将过滤器锁定到该 ID,因此任何看到该仪表板的用户都只会看到已按其 ID 过滤的数据。
示例
我们在公共 Git 存储库中维护了多种语言的嵌入示例。以下小节将通过 Django(一个流行的 Python Web 框架)和 Shiny(R 最流行的 Web 框架)中的一个最小示例进行介绍。
使用 Django 的示例
我们的最小 Django 应用程序只包含两个文件:index.html
中的 HTML 模板和 index.py
中的简短 Python 程序。模板简短而甜美
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>Embed {{ title }}</h1>
<iframe
src="{{iframeUrl}}"
frameborder="1"
width="800"
height="600"
></iframe>
</body>
</html>
它需要两个值:title
中的页面标题和 iframeUrl
中包含静态嵌入的 iframe 的 URL。标题只是一个字符串,但如上所述,我们需要做一些工作来构造 URL。该程序以 Django 所需的一些库和设置开始
# Required by Django
import os
from django.conf.urls import url
from django.http import HttpResponse
from django.template.loader import render_to_string
DEBUG = True
SECRET_KEY = '4l0ngs3cr3tstr1ngw3lln0ts0l0ngw41tn0w1tsl0ng3n0ugh'
ROOT_URLCONF = __name__
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.getcwd()]
}]
然后我们包含几个库并定义几个嵌入值
# Required for Metabase embedding
import time
import jwt
METABASE_SITE_URL = 'https://:3000'
METABASE_SECRET_KEY = '40e0106db5156325d600c37a5e077f44a49be1db9d02c96271e7bd67cc9529fa'
我们需要 time
来计算签名令牌的过期时间,以及 jwt
库来签名令牌。METABASE_SITE_URL
告诉程序在哪里找到我们的 Metabase 实例——在这种情况下,我们正在本地运行它——而 METABASE_SECRET_KEY
是 Metabase 生成的值,我们将其发送回去以证明我们被允许访问问题。我们不会在生产环境中将此放入源代码;相反,我们会将其存储在环境变量或单独的配置文件中。
我们必须在发送到 Metabase 的令牌中包含三个值。由于其中一个令牌是令牌过期时间,它会随每个请求而变化,因此我们将构建令牌的代码放在处理页面请求的函数中。为了示例目的,我们想要问题 #1,并且我们要求令牌从现在起十分钟内有效
# Handle requests for '/'.
def home(request):
payload = {
'resource': {'question': 1},
'params': {},
'exp': round(time.time()) + (60 * 10)
}
token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm='HS256')
iframeUrl = METABASE_SITE_URL + '/embed/question/' + token + '#bordered=true&titled=true'
html = render_to_string('index.html', {
'title': 'Embedding Metabase',
'iframeUrl': iframeUrl
})
return HttpResponse(html)
我们使用 jwt
库中的 jwt.encode
函数使用我们从 metabase 获取的密钥来加密我们的令牌参数。(参数 algorithm='HS256'
告诉 jwt.encode
使用哪种哈希算法——我们必须始终使用该算法。)然后我们将该加密令牌插入到 URL 中,渲染 HTML 模板,并将其返回给应用程序。最后,我们的程序通过告诉 Django 如何将传入请求与渲染函数匹配来结束
urlpatterns = [
url(r'^$', home, name='homepage')
]
如果我们在命令行中运行我们的应用程序
$ django-admin runserver --pythonpath=. --settings=index
然后将浏览器指向 https://:8000/
,会依次发生以下事情
-
浏览器向监听端口 8000 的 Django 应用程序发送一个针对
/
(网站根目录)的 HTTP 请求。 -
该应用程序将请求中的 URL 与
home
函数匹配。 -
home
生成一个新令牌,其过期时间为未来 10 秒。 -
然后它读取
index.html
并将{{title}}
替换为“嵌入 Metabase
”,将{{iframeUrl}}
替换为包含新生成令牌的 URL。 -
home
然后将该 HTML 发送回浏览器。 -
当浏览器显示该 HTML 时,它会遇到 iframe。
iframe
标签中的src
属性告诉它向 Metabase 发送请求。 -
当 Metabase 收到以
/embed/question
开头的 URL 请求时,它会从 URL 中提取其余路径并对其进行解密。 -
由于 URL 是用 Metabase 生成的密钥编码的,解密成功,这告诉 Metabase 发送方被允许查看问题。令牌中嵌入的值告诉 Metabase 正在请求哪个问题。
-
Metabase 运行问题并生成它在自己的界面中显示的 HTML,然后将该 HTML 发送回发出请求的浏览器。
-
浏览器将该 HTML 插入 iframe 并将其显示给用户。
使用 Shiny 的示例
我们的最小 Shiny 示例比上面显示的 Django 示例稍微简单一些,因为 Shiny 需要更少的样板。首先,我们加载 Shiny 本身、管理 Web 令牌以及将字符串拼接在一起的库
library(shiny)
library(jose)
library(glue)
然后我们定义两个值,它们指定 Metabase 正在运行的位置(我们将使用本地实例)以及 Metabase 提供的用于身份验证的密钥
METABASE_SITE_URL <- 'https://:3000'
METABASE_SECRET_KEY <- '40e0106db5156325d600c37a5e077f44a49be1db9d02c96271e7bd67cc9529fa'
用户界面基于流行的 CSS 框架 Bootstrap,包含两个元素:包含页面标题的 H1 标题和包含 iframe 的 div
。UI 不会自己构建 iframe;相反,它使用 uiOutput
函数来渲染一个名为 container
的东西
ui <- bootstrapPage(
h1('Page title'),
uiOutput('container')
)
container
从何而来?在 Shiny 中,答案是“服务器”。如下所示,server
构建 JWT 声明并对其进行加密以构建 iframe 的 URL。然后它调用 renderUI
来获取 iframe 的 HTML 内容,并将其分配给 output$container
。当发生此赋值时,Shiny 会自动告诉 UI 需要重新绘制页面
server <- function(input, output) {
# Token expires 10 minutes in the future.
expiry <- as.integer(unclass(Sys.time())) + (60 * 10)
# Construct params in two steps so that JSON conversion knows it's a list.
params <- list()
names(params) <- character(0)
# Create the JWT claim.
claim <- jwt_claim(exp = expiry, resource = list(question = 1), params = params)
# Encode token and use it to construct iframe URL.
token <- jwt_encode_hmac(claim, secret = METABASE_SECRET_KEY)
url <- glue("{METABASE_SITE_URL}/embed/question/{token}#bordered=true&titled=true")
output$container <- renderUI(tags$iframe(width = "600", height = "600", src = url))
}
我们更详细地介绍一下 server
函数。首先,我们希望令牌有效十分钟,因此我们将当前时间作为整数并添加 600 秒
expiry <- as.integer(unclass(Sys.time())) + (60 * 10)
然后我们使用 jose
库中的 jwt_claim
函数来构建令牌。此函数的名称来源于我们声明我们被允许做某事的事实,它接受任意数量的命名参数作为输入
params <- list()
names(params) <- character(0)
claim <- jwt_claim(exp = expiry, resource = list(question = 1), params = list())
上面代码的前两行是必需的,因为 jose
依赖另一个名为 jsonlite
的库将结构转换为 JSON 字符串,默认情况下该库将空列表转换为空数组 []
而不是空映射 {}
。这是一个很小的区别,但当 Metabase 解码结构时,它要求 params
是一个带键的映射。解决方法是给 params
一个空的名称列表,以便 jsonlite
生成所需的 {}
;为确保这能正常工作,请确保您的 jose
版本为 0.3 或更高。
一旦我们有了声明,我们就可以用 jwt_encode_hmac
对其进行编码并构建 iframe URL。jwt_encode_hmac
默认使用 HS256 编码算法,因此我们无需显式指定它
token <- jwt_encode_hmac(claim, secret = METABASE_SECRET_KEY)
url <- glue("{METABASE_SITE_URL}/embed/question/{token}#bordered=true&titled=true")
最后,我们使用 renderUI
来获取 iframe 的 HTML 并将其分配给 output$container
,这将自动触发浏览器页面的更新
output$container <- renderUI(tags$iframe(width = "600", height = "600", src = url))
文件的最后一行在端口 8001 上运行我们的应用程序
shinyApp(ui = ui, server = server, options = list(port = 8001))
如果我们用 Rscript app.R
从命令行运行这个程序,然后将我们的浏览器指向 https://:8001
,我们就会看到我们嵌入的问题。
交互式嵌入
如果您想在嵌入时充分发挥 Metabase 的潜力,允许人们钻取数据,或将他们发送到自定义目的地,例如其他仪表板或外部 URL,您需要交互式嵌入。要了解更多信息,请参阅向客户提供分析和在您的应用程序中嵌入 Metabase 以提供多租户自助分析。