metabase-data-app-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreate a Metabase Data App
创建 Metabase 数据应用
A Metabase data-app is a single JS bundle that the host loads inside a Near Membrane sandbox and renders inside its own React tree. The scaffold is a Vite + React + TypeScript project: source under , a dev server that previews the app against a real Metabase through the same Near Membrane sandbox + distortion rules Metabase uses in production — so behaves like production, including for third-party libraries the app bundles — and producing a single . (Because the sandbox runs a built bundle, a change rebuilds it and does a soft reload — re-evaluates the bundle in the sandbox and remounts the app, keeping auth/SDK loaded — rather than hot-swapping modules; component state resets, but there's no full browser refresh.) The dev preview also shows a corner ⚠ Diagnostics toolbar that captures runtime errors — including the sandbox's otherwise-opaque blocked-API messages — so failures surface instead of being swallowed. The same data is served as JSON at , which is how you read it (see "Reading the diagnostics feed" below) — you have a shell, not a browser, and these failures are invisible from the terminal otherwise.
src/npm run devnpm run builddist/index.jshttp://localhost:5174/__data-app/diagnosticsData apps are served from Git, not uploaded. A single repository is connected to Metabase via remote-sync (Admin → Settings → Remote sync). Each app lives in its own directory inside that repo — its source, a (name/path), and the committed built bundle at the its declares ( by default). On each remote-sync import Metabase materializes one app per directory and serves it at url, where the slug is the directory's name. So this skill always scaffolds into the connected repo's directory, never as a standalone project.
data_apps/<app>/data_app.yamlpathdata_app.yamldist/index.js/apps/<slug>data_apps/<app>/The scaffold ships inside this skill at — a Vite + React + TypeScript project that was installed alongside the skill. Step 3 just copies it into the app directory; the skill then guides you through the customization + first-app-content steps — it never generates project files from scratch. If you find yourself writing , , , or by hand, stop — copy the template instead.
./template/package.jsonvite.config.tstsconfig.jsonsrc/index.tsxMetabase 数据应用(data-app) 是一个单独的 JS 包,宿主会在 Near Membrane 沙箱中加载它,并在自身的 React 树中渲染。该脚手架是一个 Vite + React + TypeScript 项目:源码位于 目录下;开发服务器通过与 Metabase 生产环境完全相同的 Near Membrane 沙箱 + 畸变规则,对接真实的 Metabase 来预览应用——因此 的表现与生产环境一致,包括应用打包的第三方库也遵循相同规则; 会生成单个 文件。(由于沙箱运行的是构建后的包,代码变更会触发重新构建并执行软重载——在沙箱中重新评估包并重新挂载应用,保持认证/SDK 加载状态——而非模块热替换;组件状态会重置,但不会刷新整个浏览器。)开发预览还会在角落显示一个 ⚠ 诊断(Diagnostics) 工具栏,用于捕获运行时错误——包括沙箱中原本不透明的 API 拦截消息——让错误得以暴露而非被静默吞掉。同样的数据以 JSON 格式在 提供,这是你读取这些信息的方式(见下文“读取诊断信息流”)——你只有 shell 没有浏览器,否则这些错误在终端中是不可见的。
src/npm run devnpm run builddist/index.jshttp://localhost:5174/__data-app/diagnostics数据应用通过 Git 提供,而非上传。 单个仓库通过远程同步(Admin → Settings → Remote sync)连接到 Metabase。每个应用都位于仓库内的独立目录 中——包含应用源码、(配置名称/路径),以及提交到 声明的 处的构建后包(默认为 )。每次远程同步导入时,Metabase 会为每个目录生成一个应用,并通过 URL 提供服务,其中 slug 就是目录名称。因此本技能始终将脚手架搭建在已连接仓库的 目录中,绝不会作为独立项目创建。
data_apps/<app>/data_app.yamldata_app.yamlpathdist/index.js/apps/<slug>data_apps/<app>/脚手架模板随本技能一同提供,位于 目录——这是一个随技能一起安装的 Vite + React + TypeScript 项目。第 3 步只是将其复制到应用目录中;之后本技能会引导你完成定制和首个应用内容的创建步骤——绝不会从零生成项目文件。如果你发现自己在手动编写 、、 或 ,请停下来——改用复制模板的方式。
./template/package.jsonvite.config.tstsconfig.jsonsrc/index.tsxWhen to invoke this skill
何时调用本技能
- "scaffold a new data app" / "create a Metabase data app" / "set up a data-app project"
- "I want to build a data app" / any vague intent to author a data app
- Starting a fresh agent task that will produce a data-app bundle.
- Do not use this skill for an existing data-app project when the task is to build screens, use Metabase data, generate or refresh schema files, wire saved questions / tables / metrics / actions, add filters, or author data hooks. Treat those as existing-data-app editing tasks and use the agent's normal skill-discovery flow from the user's wording.
- “搭建一个新的数据应用” / “创建一个 Metabase 数据应用” / “设置数据应用项目”
- “我想构建一个数据应用” / 任何开发数据应用的模糊意图
- 启动一个将生成数据应用包的全新 Agent 任务。
- 对于现有数据应用项目,如果任务是构建界面、使用 Metabase 数据、生成或刷新 schema 文件、关联已保存的问题/表/指标/操作、添加过滤器或编写数据 hooks,不要使用本技能。将这些视为现有数据应用的编辑任务,根据用户措辞使用 Agent 正常的技能发现流程。
Step 1 — Locate the remote-sync repository
步骤 1 — 定位远程同步仓库
Data apps live inside the Git repository connected to Metabase via remote-sync. Find it before scaffolding:
- Ask: "Do you already have a Git repository connected to this Metabase via remote-sync?"
- Yes → ask for its local path.
- No → ask the user to create one (a plain Git repo they will connect under Admin → Settings → Remote sync) and share its path. The skill does not create or connect the repo — the user owns that.
- Verify the path exists and is a Git working tree (it has a ). This repo is the working directory for every step below.
.git
数据应用存放在通过远程同步连接到 Metabase 的 Git 仓库中。搭建脚手架前先找到它:
- 询问:“你是否已经有一个通过远程同步连接到此 Metabase 的 Git 仓库?”
- 是 → 询问其本地路径。
- 否 → 请用户创建一个(一个普通的 Git 仓库,他们将在 Admin → Settings → Remote sync 中连接)并分享其路径。本技能不会创建或连接仓库——这由用户负责。
- 验证该路径存在且是一个 Git 工作树(包含 目录)。此仓库是以下所有步骤的工作目录。
.git
Step 2 — Name the app and create its directory
步骤 2 — 命名应用并创建其目录
- Settle on the app's directory name before scaffolding — it is used verbatim as the slug (the URL), so it must be dash-cased: lowercase letters, numbers, and single dashes (
/apps/<slug>), e.g.[a-z0-9]+(?:-[a-z0-9]+)*. Anything else (uppercase, spaces, underscores) is rejected on sync. If the purpose isn't clear yet, ask a one-line "what's this app for?" and propose a name; confirm it.sales-overview - Ensure exists; create it if missing.
<repo>/data_apps/ - Create . If it already exists, treat it as an existing project (see below) — never overwrite without confirmation.
<repo>/data_apps/<slug>/
- 搭建脚手架前先确定应用的目录名称——它会被直接用作 slug(即 URL),因此必须使用短横线命名法:小写字母、数字和单个短横线(
/apps/<slug>),例如[a-z0-9]+(?:-[a-z0-9]+)*。任何其他格式(大写、空格、下划线)都会在同步时被拒绝。如果用途还不明确,用一行话询问“这个应用是做什么的?”并提议一个名称;请用户确认。sales-overview - 确保 存在;如果不存在则创建。
<repo>/data_apps/ - 创建 。如果该目录已存在,将其视为现有项目(见下文)——未经确认绝不要覆盖。
<repo>/data_apps/<slug>/
Detecting an existing app
检测现有应用
If already holds a project, verify it matches the current . Check all of:
<repo>/data_apps/<slug>/data-app-template- is a one-liner:
vite.config.ts(fromexport default dataAppConfig()). There is no local@metabase/embedding-sdk-react/data-app-dev/configdirectory: the whole bundle contract (externals/globals, the dev sandbox entry, CSS/SVG handling) lives inside that SDK config, not the scaffold.config/takes only a curated set of overrides (currently justdataAppConfig); the contract plugin is always applied and can't be overridden.port - default-exports a
src/index.tsx(type fromDataAppFactory) returning@metabase/embedding-sdk-react/data-app(no args).{ component, providerProps? }
All checks pass → template-shaped. Ask: "Extend this app, or scaffold a new one under a different slug?" If extend → skip the copy step, edit . If new → pick a different slug and restart at Step 2.
src/Any check fails → not template-shaped (older scaffold or drift). Stop. Tell the user the structure differs from the current template, extending it risks breaking the bundle contract, and ask whether to (1) migrate it, (2) scaffold fresh under a new slug and port the code over, or (3) proceed anyway at their risk. Wait for the answer.
Never overwrite existing files without explicit confirmation.
如果 中已有项目,验证它是否与当前的 匹配。检查所有以下项:
<repo>/data_apps/<slug>/data-app-template- 是一行代码:
vite.config.ts(来自export default dataAppConfig())。没有本地@metabase/embedding-sdk-react/data-app-dev/config目录:整个包契约(externals/globals、开发沙箱入口、CSS/SVG 处理)都位于该 SDK 配置中,而非脚手架中。config/仅接受一组精选的覆盖项(目前只有dataAppConfig);契约插件始终会应用,且无法被覆盖。port - 默认导出一个
src/index.tsx(类型来自DataAppFactory),返回@metabase/embedding-sdk-react/data-app(无参数)。{ component, providerProps? }
所有检查都通过 → 符合模板结构。询问:“是扩展此应用,还是在其他 slug 下搭建新应用?”如果扩展 → 跳过复制步骤,直接编辑 。如果新建 → 选择另一个 slug 并从步骤 2 重新开始。
src/任何检查失败 → 不符合模板结构(旧版脚手架或已偏离)。停止。 告知用户其结构与当前模板不同,扩展它有破坏包契约的风险,并询问是要 (1) 迁移它,(2) 在新 slug 下重新搭建并移植代码,还是 (3) 自行承担风险继续。等待用户回答。
未经明确确认,绝不要覆盖现有文件。
Step 3 — Copy the template into the app directory
步骤 3 — 将模板复制到应用目录
The template ships inside this skill at (installed alongside the skill via ). Copy it into the app directory:
./template/skills add metabase/metabase/skills#release-x.<major>.xbash
APP_DIR="<repo>/data_apps/<slug>"模板随本技能一同提供,位于 目录(通过 与技能一起安装)。将其复制到应用目录:
./template/skills add metabase/metabase/skills#release-x.<major>.xbash
APP_DIR="<repo>/data_apps/<slug>"<skill-dir>
= the directory this SKILL.md was loaded from
<skill-dir><skill-dir>
= 加载此 SKILL.md 的目录
<skill-dir>(e.g. .claude/skills/metabase-data-app-setup
); the template is its template/
subfolder.
.claude/skills/metabase-data-app-setuptemplate/(例如 .claude/skills/metabase-data-app-setup
);模板是其 template/
子文件夹。
.claude/skills/metabase-data-app-setuptemplate/cp -R "<skill-dir>/template/." "$APP_DIR/"
A data app is a *subdirectory* of the remote-sync repo, not its own repository — so this is a plain copy, never a nested `git clone` / `git init`. Everything below runs **inside `$APP_DIR`**.
The copy includes two root-level directories, `queries/` and `actions/`, each holding only a `README.md`. Keep both, even while empty: every query the app runs is a `defineQuery(...)` export in `queries/`, every action a `defineAction(...)` export in `actions/`, and the hooks refuse anything else at compile time. Read both READMEs before writing the first `useMetabaseQuery` / `useMetabaseQueryObject` / `useAction` call.cp -R "<skill-dir>/template/." "$APP_DIR/"
数据应用是远程同步仓库的*子目录*,而非独立仓库——因此这是普通复制,绝不要嵌套执行 `git clone` / `git init`。以下所有操作都在 **`$APP_DIR` 内**运行。
复制的内容包括两个根级目录 `queries/` 和 `actions/`,每个目录中只有一个 `README.md`。请保留这两个目录,即使是空的:应用运行的每个查询都是 `queries/` 中的 `defineQuery(...)` 导出,每个操作都是 `actions/` 中的 `defineAction(...)` 导出,hooks 在编译时会拒绝任何其他形式的内容。在编写第一个 `useMetabaseQuery` / `useMetabaseQueryObject` / `useAction` 调用之前,请阅读这两个 README。Step 4 — Customize
步骤 4 — 定制
Once the template is in (run everything below from that directory):
<repo>/data_apps/<slug>/-
Edit
package.jsonto match the slug.name -
Pinto the published data-apps tag (the template ships with
@metabase/embedding-sdk-react):*bashnpm install @metabase/embedding-sdk-react@64-alphaThis resolves to the current internal-testing SDK build with theentrypoint (the app's APIs) and the@metabase/embedding-sdk-react/data-appentrypoint@metabase/embedding-sdk-react/data-app-dev/configuses (the dev/build preset, which serves the sandbox entry). Do not usevite.config.tsorlatestas data apps are not yet released.64-stable -
Ensure the repo-rootignores
.gitignore— do this before creating any credentials file so the secret can never be committed. Create the.env.localif the repo doesn't have one, then add the entry if it's missing:.gitignorebashROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else GITIGNORE="$ROOT/.gitignore" # Create the repo-root .gitignore if absent, then ensure `.env.local` is # ignored so the credentials file (next step) can never be committed. [ -f "$GITIGNORE" ] || : > "$GITIGNORE" grep -qxF ".env.local" "$GITIGNORE" || echo ".env.local" >> "$GITIGNORE" fi -
Set up the Metabase credentials at the repository root —(usually two levels up from the app dir), not the app dir. One
<repo>/.env.localthere serves every data app in the repo..env.localCreate it from the example if absent, then verify the two vars are set without printing the file (it may hold other secrets) —it and echo only a pass/fail signal, never the values:sourcebash# Resolve the repo root first; an unguarded $(git ...) would expand to # "/.env.local" outside a repo and touch a system-level file. ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else ENV_FILE="$ROOT/.env.local" [ -f "$ENV_FILE" ] || cp .env.local.example "$ENV_FILE" # Source inside a subshell so the vars never leak into your environment. ( source "$ENV_FILE" 2>/dev/null [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] ) && echo "creds present" || echo "MISSING" fiIf it prints, ask the user to fillMISSING(the running Metabase instance) andDATA_APP_MB_URL(Admin → Authentication → API keys) inDATA_APP_MB_API_KEYthemselves — up front, before anything needs the key.<repo>/.env.localNever ask the user to paste the API key into the chat, and never/catechoor its variables. It's git-ignored and may hold other secrets — the file's contents and the key must never enter the conversation or your context. Every command that needs the key.env.locals the file (as above) so the shell uses the value directly; you only ever see thesource/creds presentsignal, never the secret itself. (MISSINGonly means both vars are filled and not the defaultcreds presentplaceholder — not that the URL or key are valid; a bad key surfaces later when a request fails.)mb_replace_me -
(or whichever package manager the user prefers — the template ships with no lockfile, so
npm install/npm/yarn/pnpmall work; use the project's existing lockfile if one appears post-clone).bun -
Fix the app'sso the lockfile and the built bundle get committed. Two things must end up tracked in the remote-sync repo:
.gitignore- Lockfile — strip the lockfile-ignoring block (the chunk between and
# Lockfiles —, coveringbun.lockb/package-lock.json/yarn.lock/pnpm-lock.yaml/bun.lock) so the project commits its lockfile for reproducible installs.bun.lockb - The built bundle — Metabase serves the file at the declared in
path(the template builds todata_app.yaml, the defaultdist/index.js) straight from the committed Git tree, so that file must be committed. If the template'spathignores.gitignore(or wherever your build outputs), remove that line. Verify withdist/— aftergit status+ a build, both the generated lockfile and the built bundle (the filenpm installpoints at) must appear as untracked/committable files. If either doesn't, the relevantpathline is still there; remove it and re-check. Do not skip this — agents have repeatedly shipped projects with no committed lockfile or an un-synced bundle..gitignore
- Lockfile — strip the lockfile-ignoring block (the chunk between
-
and confirm the preview at http://localhost:5174 renders the starter "Hello, data app" message.
npm run dev -
If the preview hits CORS, addunder Admin → Embedding → Embedded analytics SDK → CORS.
http://localhost:5174 -
Edit(it ships with the template, in the app directory). This is the per-app config Metabase reads on sync — one file per app. Fill in its fields for this app:
data_app.yamlyamlname: Sales App # display name shown in the admin UI description: Pipeline health and quota attainment by region # optional — see below version: 1 # data app contract version — leave as-is (see below) path: ./dist/index.js # bundle path, relative to this app's directory — leave as-is unless you change the build output # allowed_hosts: # optional — external origins the app may fetch/XHR (see below) # - https://api.example.com # - https://*.internal.acme.comCommit it alongside the built bundle (the filepoints at).path— optional: a single short sentence saying what the app does, shown under its name in the admin UI so admins can tell apps apart at a glance. Sync folds any whitespace into single spaces and rejects anything over 255 characters; the admin list wraps what is left rather than cutting it off, so a sentence reads well there and a paragraph crowds out the rows around it. Replace the template's placeholder with a real sentence about this app, or delete the line entirely if it adds nothing beyond the name.description— the data app contract version this app's code targets, a whole number. The template ships the version this skill targets; do not change it by hand. Metabase bumps the version it serves only on a breaking change to the contract. An app on an older version is marked Outdated in the admin list, hidden from every other user, and refuses to open until it is migrated to the current contract, itsversionraised to match, rebuilt, and synced. Migrating an app from one version to the next is a separate, instructed procedure; never do it ad hoc.version— only needed if the app calls an external API directly withallowed_hosts/fetch. The sandbox blocks all network egress by default; listing an origin here (exact or aXHRsubdomain wildcard) opens it in both*.(dev-server CSP) and Metabase (iframe CSP + sandbox). Do not list the Metabase instance — Metabase data is read via thenpm run devdata hooks and written viauseMetabaseQuery(the SDK handles auth), never rawuseAction. Leavefetchout entirely when the app only talks to Metabase.allowed_hostsNativesubmissions and<form action="…">/navigations obey the same allowlist. Prefer a client-side<iframe src="…">that<form onSubmit>s and writes viapreventDefault/useAction: a native submit navigates the sandboxed iframe away from the app. If you do use one, the target host must be infetchor it's blocked (allowed_hostsfor submits,form-actionfor embeds/navigations). A host you navigate to or embed must also permit framing (frame-src/X-Frame-Options) — many public sites don't.frame-ancestors
模板放入 后(以下所有操作都在该目录中运行):
<repo>/data_apps/<slug>/-
编辑的
package.json字段,使其与 slug 匹配。name -
将固定到已发布的 data-apps 标签(模板默认使用
@metabase/embedding-sdk-react):*bashnpm install @metabase/embedding-sdk-react@64-alpha这会解析为当前内部测试的 SDK 构建版本,包含入口点(应用的 API)和@metabase/embedding-sdk-react/data-app使用的vite.config.ts入口点(开发/构建预设,提供沙箱入口)。不要使用@metabase/embedding-sdk-react/data-app-dev/config或latest,因为数据应用尚未正式发布。64-stable -
确保仓库根目录的忽略
.gitignore——在创建任何凭证文件之前执行此操作,这样密钥永远不会被提交。如果仓库没有.env.local则创建,然后如果缺少该条目则添加:.gitignorebashROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else GITIGNORE="$ROOT/.gitignore" # 如果仓库根目录不存在 .gitignore 则创建,然后确保忽略 `.env.local` # 这样凭证文件(下一步)永远不会被提交。 [ -f "$GITIGNORE" ] || : > "$GITIGNORE" grep -qxF ".env.local" "$GITIGNORE" || echo ".env.local" >> "$GITIGNORE" fi -
在仓库根目录设置 Metabase 凭证——即(通常在应用目录的上两级),而不是应用目录。一个
<repo>/.env.local可供仓库中的所有数据应用使用。.env.local如果文件不存在则从示例文件创建,然后验证两个变量是否已设置,不要打印文件内容(它可能包含其他密钥)——该文件并仅输出通过/失败信号,绝不输出值:sourcebash# 首先解析仓库根目录;未受保护的 $(git ...) 在仓库外会展开为 # "/.env.local",从而修改系统级文件。 ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else ENV_FILE="$ROOT/.env.local" [ -f "$ENV_FILE" ] || cp .env.local.example "$ENV_FILE" # 在子 shell 中 source,这样变量永远不会泄漏到你的环境中。 ( source "$ENV_FILE" 2>/dev/null [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] ) && echo "creds present" || echo "MISSING" fi如果输出,请用户自行在MISSING中填写<repo>/.env.local(运行中的 Metabase 实例地址)和DATA_APP_MB_URL(Admin → Authentication → API keys)——提前完成,在任何需要密钥的操作之前。DATA_APP_MB_API_KEY绝不要要求用户将 API 密钥粘贴到聊天中,也绝不要/cat/ 打印echo或其变量。 该文件已被 git 忽略,可能包含其他密钥——文件内容和密钥绝不能进入对话或你的上下文。每个需要密钥的命令都会.env.local该文件(如上所述),以便 shell 直接使用该值;你只会看到source/creds present信号,永远看不到密钥本身。(MISSING仅表示两个变量都已填写且不是默认的creds present占位符——并不表示 URL 或密钥有效;无效密钥会在后续请求失败时暴露出来。)mb_replace_me -
执行(或用户偏好的任何包管理器——模板没有附带 lockfile,因此
npm install/npm/yarn/pnpm都可以;如果克隆后项目有现有的 lockfile,则使用对应的包管理器)。bun -
修改应用的,使 lockfile 和 构建后的包都能被提交。 远程同步仓库中必须跟踪两样东西:
.gitignore- Lockfile——删除忽略 lockfile 的代码块(和
# Lockfiles —之间的部分,涵盖bun.lockb/package-lock.json/yarn.lock/pnpm-lock.yaml/bun.lock),这样项目会提交 lockfile 以实现可复现的安装。bun.lockb - 构建后的包——Metabase 直接从已提交的 Git 树中提供 中声明的
data_app.yaml处的文件(模板构建到path,即默认dist/index.js),因此该文件必须被提交。如果模板的path忽略了.gitignore(或你的构建输出目录),请删除该行。 用dist/验证——在git status+ 构建之后,生成的 lockfile 和构建后的包(npm install指向的文件)都必须显示为未跟踪/可提交的文件。如果其中任何一个没有,说明相关的path行仍然存在;删除它并重新检查。不要跳过这一步——Agent 曾多次交付没有提交 lockfile 或包未同步的项目。.gitignore
- Lockfile——删除忽略 lockfile 的代码块(
-
执行,并确认 http://localhost:5174 上的预览渲染了初始的“Hello, data app”消息。
npm run dev -
如果预览遇到 CORS 问题,将添加到 Admin → Embedding → Embedded analytics SDK → CORS 中。
http://localhost:5174 -
编辑(它随模板一起提供,位于应用目录中)。这是 Metabase 在同步时读取的每个应用的配置——每个应用一个文件。为这个应用填写其字段:
data_app.yamlyamlname: Sales App # 管理界面中显示的显示名称 description: Pipeline health and quota attainment by region # 可选 — 见下文 version: 1 # 数据应用契约版本 — 保持不变(见下文) path: ./dist/index.js # 包路径,相对于此应用目录 — 除非你更改构建输出,否则保持不变 # allowed_hosts: # 可选 — 应用可能 fetch/XHR 的外部源(见下文) # - https://api.example.com # - https://*.internal.acme.com将其与构建后的包(指向的文件)一起提交。path——可选:一句简短的话说明应用的用途,显示在管理界面的应用名称下方,让管理员一眼就能区分不同的应用。同步会将任何空白字符折叠为单个空格,并拒绝超过 255 个字符的内容;管理列表会换行显示剩余内容,而不是截断,因此一句话在那里显示效果很好,而段落会挤占周围的行。用关于此应用的真实句子替换模板的占位符,如果除了名称之外没有更多信息,也可以完全删除该行。description——此应用代码目标的数据应用契约版本,为整数。模板附带了本技能目标的版本;不要手动更改。Metabase 仅在契约发生破坏性变更时才会提高其提供的版本。使用旧版本的应用在管理列表中会被标记为过时(Outdated),对所有其他用户隐藏,并拒绝打开,直到它被迁移到当前契约、version提高到匹配版本、重新构建并同步。将应用从一个版本迁移到下一个版本是一个单独的、有指导的流程;绝不要临时随意操作。version——仅当应用使用allowed_hosts/fetch直接调用外部 API 时才需要。沙箱默认阻止所有网络出站流量;在此处列出源(精确匹配或XHR子域名通配符)会在*.(开发服务器 CSP)和 Metabase(iframe CSP + 沙箱)中都开放该源。不要列出 Metabase 实例——Metabase 数据通过npm run dev数据 hooks 读取,通过useMetabaseQuery写入(SDK 处理认证),绝不使用原始useAction。如果应用仅与 Metabase 通信,请完全省略fetch。allowed_hosts原生提交和<form action="…">/导航也遵循相同的允许列表。优先使用客户端的<iframe src="…">,调用<form onSubmit>并通过preventDefault/useAction写入:原生提交会将沙箱化的 iframe 导航离开应用。如果你确实使用原生提交,目标主机必须在fetch中,否则会被阻止(提交对应allowed_hosts,嵌入/导航对应form-action)。你导航到或嵌入的主机还必须允许被框架嵌入(frame-src/X-Frame-Options)——许多公共网站不允许。frame-ancestors
Step 5 — Verify the starter app
步骤 5 — 验证初始应用
At this point the data app exists. Keep this workflow focused on creating the
project scaffold and proving the starter bundle works.
-
Run.
npm run typecheck -
Run.
npm run build -
Confirmshows the app source, lockfile,
git status, and the built bundle (data_app.yamlby default) as committable files.dist/index.js -
If the user asked for a live preview, runand confirm the starter "Hello, data app" screen renders through the sandbox preview.
npm run dev -
With the preview open, check the diagnostics feed once — it is the only place runtime failures appear to you (see Reading the diagnostics feed):bash
curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"Expectand no entries withclients: 1."alert": truemeans no preview tab is open, so an empty feed proves nothing.clients: 0
Stop here if the user only asked to create, scaffold, or set up a data app.
If the next task is to build or iterate on the actual app UI — especially if it
mentions an existing data app, Metabase data, generated schema files, saved
questions, tables, metrics, actions, filters, semantic-layer entities, or data
hooks — treat it as a separate existing-data-app editing task. Use the agent's
normal skill-discovery flow from those terms. Do not expand this scaffold skill
with data-layer authoring rules.
Do not modify or unless the change is genuinely required. The whole build/dev setup lives in the SDK behind (which also serves the dev HTML shell — there's no to edit), so is just:
src/index.tsxtsconfig.jsondataAppConfig()index.htmlvite.config.tsts
import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config";
export default dataAppConfig();dataAppConfigportindex.htmlsrc/index.tsxThere is intentionally no escape hatch for extra Vite plugins, aliases, or s — is the only knob. If you think you need more, you almost certainly don't; solve it in instead.
defineportsrc/After every meaningful round of edits, run . It runs over and — catches wrong prop shapes against the SDK types, broken refactors, missing imports, etc. The Vite dev server does NOT typecheck (it only transpiles), so errors that would fail a production CI run can sit invisibly in a passing session. Run it before declaring a task complete.
npm run typechecktsc --noEmitsrc/vite.config.tsnpm run devBefore handoff, re-check package hygiene. should use the expected data-app SDK source/tag for the target environment. No date picker dependency should be installed when the app only needs date ranges — not , , , or a UI suite's picker (, , , …); that is from . should not be installed unless the chosen version actually needs it.
@metabase/embedding-sdk-reactreact-datepickerreact-day-pickerflatpickr@mui/x-date-pickersantdrsuiteDateRangePopover@metabase/embedding-sdk-react/data-app@types/react-datepickerreact-datepicker此时数据应用已创建。请保持本工作流专注于创建项目脚手架和验证初始包是否正常工作。
-
运行。
npm run typecheck -
运行。
npm run build -
确认显示应用源码、lockfile、
git status和构建后的包(默认为data_app.yaml)都是可提交的文件。dist/index.js -
如果用户要求实时预览,运行并确认初始的“Hello, data app”屏幕通过沙箱预览正确渲染。
npm run dev -
预览打开后,检查一次诊断信息流——这是你能看到运行时故障的唯一地方(见读取诊断信息流):bash
curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"预期结果是且没有clients: 1的条目。"alert": true表示没有打开的预览标签页,因此空的信息流不能说明任何问题。clients: 0
如果用户只是要求创建、搭建或设置数据应用,到此为止。
如果下一个任务是构建或迭代实际的应用 UI——特别是如果提到了现有数据应用、Metabase 数据、生成的 schema 文件、已保存的问题、表、指标、操作、过滤器、语义层实体或数据 hooks——将其视为单独的现有数据应用编辑任务。根据这些术语使用 Agent 正常的技能发现流程。不要用数据层编写规则扩展本脚手架技能。
不要修改 或 ,除非确实有必要。 整个构建/开发设置都位于 背后的 SDK 中(它还提供开发用的 HTML 外壳——没有可编辑的 ),因此 只是:
src/index.tsxtsconfig.jsondataAppConfig()index.htmlvite.config.tsts
import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config";
export default dataAppConfig();dataAppConfigportindex.htmlsrc/index.tsx故意没有为额外的 Vite 插件、别名或 提供逃生舱—— 是唯一的调节旋钮。如果你认为需要更多,几乎可以肯定你不需要;改为在 中解决。
defineportsrc/每轮有意义的编辑后,运行 。 它会对 和 运行 ——捕获与 SDK 类型不匹配的 prop 形状、失败的重构、缺失的导入等。Vite 开发服务器不会进行类型检查(它只进行转译),因此会导致生产 CI 运行失败的错误可能在 会话中不可见地存在。在宣布任务完成前运行它。
npm run typechecksrc/vite.config.tstsc --noEmitnpm run dev交接前,重新检查包的整洁性。 应使用目标环境预期的数据应用 SDK 源/标签。当应用只需要日期范围时,不应安装任何日期选择器依赖——不要安装 、、 或 UI 套件的选择器(、、 等);应该使用 中的 。除非所选的 版本确实需要,否则不应安装 。
@metabase/embedding-sdk-reactreact-datepickerreact-day-pickerflatpickr@mui/x-date-pickersantdrsuite@metabase/embedding-sdk-react/data-appDateRangePopoverreact-datepicker@types/react-datepickerReading the diagnostics feed
读取诊断信息流
npm run devnpm run typecheckLoop: note before editing → make the change (rebuilds
automatically) → re-read. is inclusive and survives page reloads.
nextEventIdstartEventIdbash
curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"jsonc
{
"entries": [{
"eventId": 31, "kind": "blocked-network", "alert": true,
"summary": "Blocked fetch to api.example.com (not in allowed_hosts)",
"detail": null, // stack frames, when any
"hint": "Add https://api.example.com to allowed_hosts in data_app.yaml …",
"buildId": 7 // the bundle generation that reported it
}],
"clients": 1, // connected preview tabs — 0 means nothing ran
"buildId": 7, // the generation running now
"staleEntries": 164, // held back, reported by an older build — see below
"nextEventId": 32 // pass back as ?startEventId=
}clients: 0entrieshttp://localhost:5174The feed answers for the bundle that is running, not for everything that ever
happened. Every save rebuilds and remounts the app, so a multi-step edit runs
through builds that don't compile or don't render — errors from those describe
code the preview has already replaced. They're withheld and counted in
; read mid-edit and you see the current build's failures, not a
pile of them. Nothing is lost: the rebuild re-runs the app from scratch, so
anything still broken reports itself again under the new , and
hands back the withheld entries when comparing builds is
the point. A failed build doesn't advance — the preview keeps
running the last good bundle, so its entries stay current.
staleEntriesbuildId?includeStale=truebuildIdTriage by (always read — it names the exact fix; never soften a
when reporting it):
kindhintsummary | Fix |
|---|---|
| add the origin to |
| blocked in production too — use an SDK API or drop the call, don't work around it |
| a Metabase request failed; fix the query — |
| a real bug; |
Text is truncated and the buffer holds the last 200 events. clears it for every reader.
curl -X DELETE .../__data-app/diagnosticsnpm run devnpm run typecheck循环流程: 编辑前记下 → 进行更改(会自动重新构建)→ 重新读取。 是包含性的,且在页面刷新后仍然有效。
nextEventIdstartEventIdbash
curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"jsonc
{
"entries": [{
"eventId": 31, "kind": "blocked-network", "alert": true,
"summary": "Blocked fetch to api.example.com (not in allowed_hosts)",
"detail": null, // 栈帧(如果有的话)
"hint": "Add https://api.example.com to allowed_hosts in data_app.yaml …",
"buildId": 7 // 报告此事件的包版本
}],
"clients": 1, // 已连接的预览标签页 — 0 表示没有运行任何内容
"buildId": 7, // 当前运行的版本
"staleEntries": 164, // 被保留的、由旧版本报告的条目 — 见下文
"nextEventId": 32 // 作为 ?startEventId= 的值回传
}clients: 0entrieshttp://localhost:5174信息流反映的是当前运行的包的状态,而不是所有历史事件。 每次保存都会重新构建并重新挂载应用,因此多步骤编辑会经历无法编译或无法渲染的构建——这些构建的错误描述的是预览已经替换的代码。它们会被保留并计入 ;在编辑过程中读取,你会看到当前构建的故障,而不是一堆历史错误。没有任何内容会丢失:重新构建会从头重新运行应用,因此任何仍然存在的问题都会在新的 下再次报告,而当你需要比较构建时, 会返回被保留的条目。失败的构建不会推进 ——预览会继续运行最后一个正常的包,因此它的条目保持最新。
staleEntriesbuildId?includeStale=truebuildId按 分类排查(始终阅读 ——它给出了确切的修复方法;报告时不要弱化 的内容):
kindhintsummary | 修复方法 |
|---|---|
| 将源添加到 |
| 在生产环境中也会被阻止——使用 SDK API 或删除该调用,不要绕过它 |
| Metabase 请求失败;修复查询—— |
| 真正的 bug; |
文本会被截断,缓冲区保存最后 200 个事件。 会为所有读者清除它。
curl -X DELETE .../__data-app/diagnosticsSource conventions
源码约定
1. Write TSX
1. 编写 TSX
Plain ESM + TSX, normal package imports:
tsx
// src/components/CustomerCard.tsx
import { StaticQuestion } from "@metabase/embedding-sdk-react";
type Customer = { name: string; questionId: number };
export default function CustomerCard({ customer }: { customer: Customer }) {
return (
<article>
<h3>{customer.name}</h3>
<StaticQuestion questionId={customer.questionId} height={300} width="100%" />
</article>
);
}使用普通 ESM + TSX,标准包导入:
tsx
// src/components/CustomerCard.tsx
import { StaticQuestion } from "@metabase/embedding-sdk-react";
type Customer = { name: string; questionId: number };
export default function CustomerCard({ customer }: { customer: Customer }) {
return (
<article>
<h3>{customer.name}</h3>
<StaticQuestion questionId={customer.questionId} height={300} width="100%" />
</article>
);
}2. Structure from the start
2. 从一开始就结构化
Default project layout once the starter app is extended:
queries/ (root level, beside package.json — NOT under src/)
│ └── orders.query.ts (defineQuery exports; one file per topic)
actions/ (root level, beside package.json — NOT under src/)
│ └── orders.action.ts (defineAction exports; one file per topic)
src/
├── index.tsx (template — the factory; don't edit)
├── App.tsx (routing + composition only)
├── theme.ts
├── metabase.data.ts (generated schema — see the semantic-layer skill)
├── pages/ (one file per screen)
│ ├── Overview.tsx
│ └── CustomerDetail.tsx
├── components/ (shared UI)
│ └── Card.tsx
├── hooks/ (custom hooks that wrap a query export, never the query itself)
│ └── useCustomers.ts
├── lib/ (pure helpers / derivations)
│ └── format.ts
└── types/ (shared TS types)
└── customer.tsVite bundles everything reachable from into a single IIFE — the layout is purely for your own readability. and are not: synchronizes exactly those two root-level directories to Metabase, and the query and action hooks accept only the / exports declared there. A query object written at a hook call, under , or spread from a definition does not compile (); the fix is to move it into and import it, never a cast.
src/index.tsxdist/index.jssrc/queries/actions/npm run builddefineQuerydefineActionsrc/Property 'definedWithDefineQuery' is missingqueries/If the app has multiple tabs (or any top-level screen switcher), the default — leftmost / first — tab MUST be selected on initial load. The app should never boot to a blank page, an empty shell, or a "nothing selected" state that waits for the user to click. Agents repeatedly forget this. For local-state tabs, initialize the active tab to the first one so the very first render shows it:
tsx
const [active, setActive] = useState(TABS[0].id); // default = leftmost tabIf the tabs are instead backed by URL routes (multiple pages), the same rule applies via the router — see the skill for making the base path resolve to the default tab. Either way, verify by loading the app fresh: the leftmost tab's content is visible immediately and reads as selected.
metabase-data-app-routing/The build output is one self-contained file — nothing else. The backend serves a single bundle, so there are no sidecar files: CSS is inlined into the JS, and every imported asset (images, fonts, SVGs-as-URLs) is base64-inlined as a data URI. So / give you a ready-to-use data-URI string, and SVGs can also be imported as React components with the suffix (built-in ): . Everything gets baked into — just keep large binaries out, since inlining inflates the bundle. (If your editor doesn't recognize a import, add to a in .)
.jsimport logo from "./logo.png"import iconUrl from "./icon.svg"?reactsvgrimport Icon from "./icon.svg?react"dist/index.js?reactdeclare module "*.svg?react";.d.tssrc/初始应用扩展后的默认项目布局:
queries/ (根级别,与 package.json 同级 — 不在 src/ 下)
│ └── orders.query.ts (defineQuery 导出;每个主题一个文件)
actions/ (根级别,与 package.json 同级 — 不在 src/ 下)
│ └── orders.action.ts (defineAction 导出;每个主题一个文件)
src/
├── index.tsx (模板 — 工厂函数;不要编辑)
├── App.tsx (仅路由 + 组合)
├── theme.ts
├── metabase.data.ts (生成的 schema — 见语义层技能)
├── pages/ (每个屏幕一个文件)
│ ├── Overview.tsx
│ └── CustomerDetail.tsx
├── components/ (共享 UI)
│ └── Card.tsx
├── hooks/ (包装查询导出的自定义 hooks,永远不要包装查询本身)
│ └── useCustomers.ts
├── lib/ (纯辅助函数 / 派生函数)
│ └── format.ts
└── types/ (共享 TS 类型)
└── customer.tsVite 将从 可达的所有内容打包到单个 IIFE 中—— 的布局纯粹是为了你自己的可读性。 和 则不同: 会将这两个根级目录精确同步到 Metabase,并且查询和操作 hooks 只接受在那里声明的 / 导出。在 hook 调用处、 下编写的查询对象,或从定义中展开的查询对象都无法编译();修复方法是将其移动到 中并导入,永远不要使用类型断言。
src/index.tsxdist/index.jssrc/queries/actions/npm run builddefineQuerydefineActionsrc/Property 'definedWithDefineQuery' is missingqueries/如果应用有多个标签页(或任何顶级屏幕切换器),默认的——最左侧/第一个——标签页必须在初始加载时被选中。 应用绝不应该启动到空白页面、空外壳或等待用户点击的“未选择任何内容”状态。Agent 经常忘记这一点。对于本地状态的标签页,将活动标签页初始化为第一个,这样第一次渲染就会显示它:
tsx
const [active, setActive] = useState(TABS[0].id); // 默认 = 最左侧标签页如果标签页由 URL 路由支持(多页面),则通过路由器应用相同的规则——请参阅 技能,了解如何使基础路径 解析为默认标签页。无论哪种方式,都要通过重新加载应用来验证:最左侧标签页的内容立即可见,并且显示为选中状态。
metabase-data-app-routing/构建输出是一个独立的 文件——没有其他内容。 后端只提供单个包,因此没有附属文件:CSS 被内联到 JS 中,每个导入的资源(图像、字体、作为 URL 的 SVG)都被 base64 内联为数据 URI。因此 / 会给你一个即用型数据 URI 字符串,SVG 也可以通过 后缀作为 React 组件导入(内置 ):。所有内容都被烘焙到 中——只需避免大型二进制文件,因为内联会增包大小。(如果你的编辑器无法识别 导入,请在 的 文件中添加 。)
.jsimport logo from "./logo.png"import iconUrl from "./icon.svg"?reactsvgrimport Icon from "./icon.svg?react"dist/index.js?reactsrc/.d.tsdeclare module "*.svg?react";3. Import SDK values from the correct SDK entrypoint
3. 从正确的 SDK 入口点导入 SDK 值
The build externalizes React, the JSX runtimes, , and to sandbox globals in both production and — in dev the sandbox entry endows them from the npm package, so the bundle runs identically in both. Just import from the entrypoints normally:
@metabase/embedding-sdk-react@metabase/embedding-sdk-react/data-appnpm run devtsx
// ✅ correct
import { StaticQuestion } from "@metabase/embedding-sdk-react";
import {
DataAppRouter,
DataAppLink,
useMetabaseQuery,
} from "@metabase/embedding-sdk-react/data-app";
// ❌ wrong — no globalThis pattern; you'd be reading nothing
const { MetabaseProvider, StaticQuestion } = globalThis;Do NOT render in . The dev entry (served by the SDK's dev preset) and the production host both wrap your tree in a provider that lives in their own realm — wrapping inside the bundle would route the SDK's -via-listener paths through the Near Membrane sandbox and silently break drill popups, plugin init, and similar.
<MetabaseProvider>App.tsxsetState构建会将 React、JSX 运行时、 和 外部化为沙箱全局变量,在生产环境和 中都是如此——在开发环境中,沙箱入口从 npm 包中提供这些全局变量,因此包在两种环境中运行方式完全相同。只需正常从入口点导入:
@metabase/embedding-sdk-react@metabase/embedding-sdk-react/data-appnpm run devtsx
// ✅ 正确
import { StaticQuestion } from "@metabase/embedding-sdk-react";
import {
DataAppRouter,
DataAppLink,
useMetabaseQuery,
} from "@metabase/embedding-sdk-react/data-app";
// ❌ 错误 — 不要使用 globalThis 模式;你读不到任何内容
const { MetabaseProvider, StaticQuestion } = globalThis;不要在 中渲染 。 开发入口(由 SDK 的开发预设提供)和生产宿主都将你的组件树包装在位于它们自己领域的 provider 中——在包内部包装会导致 SDK 的基于监听器的 路径穿过 Near Membrane 沙箱,从而静默破坏钻取弹窗、插件初始化等功能。
App.tsx<MetabaseProvider>setState4. Import react
normally too
react4. 也正常导入 react
The build externalizes and , so plain imports resolve the same way in both modes — the production host and the dev sandbox both endow them as sandbox globals:
reactreact-domtsx
import { useState, useEffect, useMemo } from "react";No needed in TSX files — the template uses the automatic JSX runtime ( in ; includes the React plugin). The compiler injects the JSX-runtime imports it needs ( in production, in dev — both externalized and endowed by the sandbox). Just write JSX and named imports — that's it.
import React from "react"jsx: "react-jsx"tsconfig.jsondataAppConfig()react/jsx-runtimereact/jsx-dev-runtimeFor React types (e.g. , , ), use named type imports rather than the namespace:
ComponentTypeReactNodeRefObjectReact.ts
import type { ComponentType, ReactNode } from "react";构建会将 和 外部化,因此普通导入在两种模式下的解析方式相同——生产宿主和开发沙箱都将它们作为沙箱全局变量提供:
reactreact-domtsx
import { useState, useEffect, useMemo } from "react";TSX 文件中不需要 ——模板使用自动 JSX 运行时( 中的 ; 包含 React 插件)。编译器会注入它需要的 JSX 运行时导入(生产环境中是 ,开发环境中是 ——两者都被外部化并由沙箱提供)。只需编写 JSX 和命名导入——就这么简单。
import React from "react"tsconfig.jsonjsx: "react-jsx"dataAppConfig()react/jsx-runtimereact/jsx-dev-runtime对于 React 类型(例如 、、),使用命名类型导入而不是 命名空间:
ComponentTypeReactNodeRefObjectReact.ts
import type { ComponentType, ReactNode } from "react";Theme rules
主题规则
MetabaseProviderthemesrc/theme.ts| Field | Purpose | Notes |
|---|---|---|
| Accent for SDK widgets | Use the data app's primary color. |
| Hover/accent backgrounds for SDK widgets | Use a subtle surface tint that contrasts with hover text. Do not use the same saturated color as |
| Chart palette | Array of strings. Use vivid brand-shade colors only; never pale tints — palette index 1+ may render labels and pale-on-white is invisible. |
| Semantic indicators | |
| SDK component surface | MUST match the immediate parent container of the SDK component. If the chart sits inside a white card, use |
| Text on SDK surfaces | MUST contrast with |
| SDK widget typography |
Hover colors are a contrast pair. If you set , verify it
contrasts with / in open menus,
chart type selectors, and visualization settings dropdowns. For a blue ,
use a pale hover surface like , not the brand blue itself.
colors.text-hovercolors.brand-hovercolors.brand-hover-lightbrand#EAF4FFThe theme only styles SDK widgets — never your own UI. For your page background, headers, card wrappers, etc., use inline or CSS modules on your own elements.
style={{ background: "#f5f5f7" }}Don't expect per-subtree theming. Multiple s on the page fight for the single CSS-variable slot. If the look needs to change with state, recompute at the App level and re-render — the whole tree re-themes.
<MetabaseProvider>sdkThemeMetabaseProviderthemesrc/theme.ts| 字段 | 用途 | 说明 |
|---|---|---|
| SDK 组件的强调色 | 使用数据应用的主色。 |
| SDK 组件的悬停/强调背景 | 使用与悬停文本形成对比的柔和表面色调。不要使用与 |
| 图表调色板 | 字符串数组。仅使用鲜艳的品牌色调;绝不要使用浅色调——调色板索引 1 及以上可能会渲染标签,浅色在白色背景上是不可见的。 |
| 语义指示器 | |
| SDK 组件的表面色 | 必须与 SDK 组件的直接父容器匹配。 如果图表位于白色卡片内,使用 |
| SDK 表面上的文本 | 必须与 |
| SDK 组件的排版 |
悬停颜色是一对对比色。如果你设置了 ,请在打开的菜单、图表类型选择器和可视化设置下拉菜单中验证它与 / 形成对比。对于蓝色 ,使用浅悬停表面如 ,而不是品牌蓝色本身。
colors.text-hovercolors.brand-hovercolors.brand-hover-lightbrand#EAF4FF主题仅为 SDK 组件设置样式——永远不要用于你自己的 UI。 对于你的页面背景、标题、卡片包装器等,在你自己的元素上使用内联 或 CSS 模块。
style={{ background: "#f5f5f7" }}不要期望每个子树都有独立的主题。 页面上的多个 会争夺单个 CSS 变量槽。如果外观需要随状态变化,请在 App 级别重新计算 并重新渲染——整个树都会重新应用主题。
<MetabaseProvider>sdkThemeCustom error UI (optional)
自定义错误 UI(可选)
When an embedded question or dashboard can't load — its was removed, the user lacks access, or a request fails — the host renders a neutral built-in error state in place of that component (a muted icon + the SDK's message). This needs no setup; leave it alone unless the app has a strong reason to restyle it.
questionIdTo match the app's look, return your own in the factory's (the same object that carries ). It's an ordinary key, not a change to the factory shape — safe to add:
errorComponentproviderPropsthemeproviderPropstsx
// src/components/AppError.tsx
import type { SdkErrorComponentProps } from "@metabase/embedding-sdk-react";
export default function AppError({ message }: SdkErrorComponentProps) {
return (
<div style={{ padding: 16, textAlign: "center", color: "#6b7280" }}>
{message}
</div>
);
}tsx
// src/index.tsx — add the key alongside `theme`
import AppError from "./components/AppError";
const factory: DataAppFactory = () => ({
component: App,
providerProps: { theme: sdkTheme, errorComponent: AppError },
});Rules:
- It replaces the error UI for every SDK component in the app, not just not-found — so keep the copy generic. Render (or your own wording), not an assumption about which error occurred.
message - is a
message— render it as-is; don't callReactNodeor index into it..toString() - Keep it a small presentational component. It renders inside the host's provider, but treat it as leaf UI — no data hooks, no .
useMetabaseQuery - Omit entirely to keep the neutral default. There's no need to re-implement it.
errorComponent
当嵌入的问题或仪表板无法加载时—— 被删除、用户没有访问权限或请求失败——宿主会在该组件的位置渲染一个中性的内置错误状态(一个柔和的图标 + SDK 的消息)。这不需要设置;除非应用有充分的理由重新设置样式,否则不要管它。
questionId为了匹配应用的外观,在工厂的 中返回你自己的 (与承载 的是同一个对象)。这是一个普通的 键,不是对工厂形状的修改——可以安全添加:
providerPropserrorComponentthemeproviderPropstsx
// src/components/AppError.tsx
import type { SdkErrorComponentProps } from "@metabase/embedding-sdk-react";
export default function AppError({ message }: SdkErrorComponentProps) {
return (
<div style={{ padding: 16, textAlign: "center", color: "#6b7280" }}>
{message}
</div>
);
}tsx
// src/index.tsx — 在 `theme` 旁边添加该键
import AppError from "./components/AppError";
const factory: DataAppFactory = () => ({
component: App,
providerProps: { theme: sdkTheme, errorComponent: AppError },
});规则:
- 它会替换应用中每个 SDK 组件的错误 UI,不仅仅是未找到的情况——因此文案要保持通用。渲染 (或你自己的措辞),不要假设发生了什么错误。
message - 是一个
message——按原样渲染;不要调用ReactNode或对其进行索引。.toString() - 保持它是一个小型的展示型组件。它在宿主的 provider 内部渲染,但将其视为叶子 UI——没有数据 hooks,没有 。
useMetabaseQuery - 完全省略 以保持中性默认值。没有必要重新实现它。
errorComponent
Available SDK surface
可用的 SDK 接口
The bundle imports React hooks/JSX, SDK components from , and data-app-specific routing/query APIs from . externalizes these packages at build time, so production references the host's copies at runtime ( / for SDK packages), and the Vite dev sandbox endows the same globals from the installed npm package.
@metabase/embedding-sdk-react@metabase/embedding-sdk-react/data-appdataAppConfig()globalThis.__metabase_sdk__globalThis.__metabase_data_app__<MetabaseProvider>App.tsx| Import | Purpose |
|---|---|
| Hooks ( |
| Non-drillable question. Props include |
| Drillable question. Same props as StaticQuestion plus drill behaviors. Use |
| Type-only import from |
| More question variants. |
| Dashboard variants. |
| Modal for new-dashboard flow. |
| Collection picker. |
| Data-app-only helpers for routing, schema-backed data reads, actions, clipboard, and sandbox-safe integration. Treat schema-backed queries, generated schema files, filters, metrics, actions, and other data-layer behavior as existing-data-app editing work; use skill discovery before authoring that code. |
| Date range selection for filter bars. |
包从 导入 React hooks/JSX,从 导入 SDK 组件,从 导入数据应用特有的路由/查询 API。 在构建时将这些包外部化,因此生产环境在运行时引用宿主的副本(SDK 包对应 / ),而 Vite 开发沙箱从已安装的 npm 包提供相同的全局变量。
react@metabase/embedding-sdk-react@metabase/embedding-sdk-react/data-appdataAppConfig()globalThis.__metabase_sdk__globalThis.__metabase_data_app__<MetabaseProvider>App.tsx| 导入 | 用途 |
|---|---|
| Hooks( |
| 不可钻取的问题。Props 包括 |
| 可钻取的问题。与 StaticQuestion props 相同,加上钻取行为。对于临时的 SDK 渲染问题,使用 |
| 从 |
| 更多问题变体。 |
| 仪表板变体。 |
| 新建仪表板流程的模态框。 |
| 集合选择器。 |
| 仅用于数据应用的辅助工具,用于路由、基于 schema 的数据读取、操作、剪贴板和沙箱安全集成。将基于 schema 的查询、生成的 schema 文件、过滤器、指标、操作和其他数据层行为视为现有数据应用的编辑工作;在编写该代码之前使用技能发现。 |
| 用于筛选栏的日期范围选择。 |
Blocked APIs
被阻止的 API
The Near Membrane sandbox throws at runtime on these globals. Use the endowed replacement instead:
| Blocked | Use instead |
|---|---|
Network — | Use the SDK/data-app APIs for Metabase reads and writes — never raw |
UI dialogs — | Render a React modal in your own tree. |
Storage — | Treat the data app as stateless across reloads; persist via a Metabase action. |
Window / history navigation — | |
Clipboard — | |
Other | Not available. |
Global | Attach the listener to your own element, or use the React handler ( |
Rule of thumb: if you're about to touch , , , , or any storage global, stop and pick the endowed replacement above. The endowed surface (React + React DOM + SDK components + data hooks + + DataAppRouter + + ) covers every routine need; anything outside it is intentionally unreachable.
window.Xdocument.Xnavigator.Xhistory.XuseActionDateRangePopovercopyNear Membrane 沙箱在运行时会对这些全局变量抛出错误。请改用提供的替代方案:
| 被阻止的 API | 替代方案 |
|---|---|
网络 — | 使用 SDK/数据应用 API 进行 Metabase 读写——永远不要用原始 |
UI 对话框 — | 在你自己的组件树中渲染 React 模态框。 |
存储 — | 将数据应用视为跨重新加载无状态的;通过 Metabase 操作持久化数据。 |
窗口 / 历史导航 — | 应用内导航使用 |
剪贴板 — | |
其他 | 不可用。 |
用于输入/剪贴板事件的全局 | 将监听器附加到你自己的元素上,或者在特定的输入/容器上使用 React 处理器( |
经验法则: 如果你要接触 、、、 或任何存储全局变量,请停下来并选择上面提供的替代方案。提供的接口(React + React DOM + SDK 组件 + 数据 hooks + + DataAppRouter + + )涵盖了所有常规需求;之外的任何内容都是故意不可达的。
window.Xdocument.Xnavigator.Xhistory.XuseActionDateRangePopovercopyRendering a chart: Metabase first
渲染图表:优先使用 Metabase
This is a per-element decision, not one taken once for a row, a section, or the app. One tile that needs custom code makes a row with one custom tile in it, not a row of them — the rest stay SDK components and keep their formatting, theming and drill-through. Consistent heights and card chrome come from the container each tile sits in, so "the others should match it" is not a reason to hand-build the others.
A built-in visualization carries the instance's theming, accessibility, tooltips, formatting and drill-through. A chart built in React carries none of that, and drifts from every other chart in the app as soon as either changes. So the question is never "which looks closer to my design" — it is can Metabase display this at all?
- Yes → +
useMetabaseQueryObject/StaticQuestion. Bar, line, area, combo, row, trend, pie/donut, scalar/smartscalar, gauge, progress, funnel, scatter, waterfall, boxplot, sankey, pivot, map, object/list views, sortable table, and anything else in the chart-type list. Also whenever visualization settings can carry the presentation — axes, labels, stacking, goals, trendlines, split panels, series, formatting, table/pie/pivot/list settings — or the user benefits from sorting, column inspection, drill-through, or downloading. Build the semantic query from generated schema objects, destructure the returnedInteractiveQuestion, and pass only that value in a card object —query. Never pass the whole<StaticQuestion card={{ query }} visualization="pie" ... />hook result as{ query, error, isLoading }.card.query - No → and your own component. Only when the user asked for a custom visualization, or nothing Metabase renders can express the request: bespoke scorecards, alert panels, narrative layouts or mixed-content cards, custom interactions its chart/table chrome cannot express, unusual forms such as calendar grids, timelines, heat strips, radial views, custom maps or domain-specific diagrams, and elements combining several queries into one visual unit. Keep the row handling typed. Use a charting dependency the app already has; otherwise SVG is fine.
useMetabaseQuery
StaticQuestionwithChartTypeSelectorwithDownloadstitleInteractiveQuestionisSaveEnabledtrueisSaveEnabled={false}InteractiveQuestion<InteractiveQuestion.QuestionVisualization />Pass when the question sits in a card or section that already carries a heading. 's default layout shows the question's own title, so you get two; hides it already.
title={false}InteractiveQuestionStaticQuestion"It has to match our styling" is not a reason to hand-build one: pass for the chart type, for setting-level changes, and theme the SDK for the rest. A pie chart in a data app is a Metabase pie chart — unless the user asked for a custom one, which is their call to make, not one to infer from a design reference, brand colours, or a screenshot of something bespoke.
visualizationvisualizationSettingsA single-value KPI is a scalar — or smartscalar, gauge, progress. If the tile wants a number plus something Metabase does not draw, such as a star row, a caption, or a total from a second query, render the scalar for the number and put the extra beside it. Hand-build it only when the number itself cannot come from one.
Always render a spinner (or skeleton) while is — never an empty slot or stale value, which causes layout shift when the data arrives. Same rule for lifted / derived queries (pass down) and for 's (spinner in the button + ).
isLoadingtrueisLoadinguseActionisExecutingdisabled={isExecuting}For the hook contract itself — generics, table sources, segments, measures, breakouts, sorting, and debugging — use skill discovery before authoring schema-backed data-layer code.
这是针对每个元素的决定,而不是针对一行、一个部分或整个应用的一次性决定。一个需要自定义代码的图块只会让一行中有一个自定义图块,而不是一整行都是——其余的保持为 SDK 组件,保留它们的格式、主题和钻取功能。一致的高度和卡片外观来自每个图块所在的容器,因此“其他的应该和它匹配”不是手动构建其他图块的理由。
内置的可视化带有实例的主题、可访问性、工具提示、格式和钻取功能。用 React 构建的图表没有这些,而且只要任何一方发生变化,就会与应用中的其他所有图表产生差异。因此问题从来不是“哪个看起来更接近我的设计”——而是 Metabase 到底能不能显示这个?
- 能 → +
useMetabaseQueryObject/StaticQuestion。 条形图、折线图、面积图、组合图、行图、趋势图、饼图/环形图、标量/智能标量、仪表盘、进度图、漏斗图、散点图、瀑布图、箱线图、桑基图、透视表、地图、对象/列表视图、可排序表格,以及图表类型列表中的任何其他内容。此外,只要可视化设置能够承载呈现需求——坐标轴、标签、堆叠、目标、趋势线、拆分面板、系列、格式、表格/饼图/透视/列表设置——或者用户能从排序、列检查、钻取或下载中受益,都应该使用。从生成的 schema 对象构建语义查询,解构返回的InteractiveQuestion,并仅将该值传递到卡片对象中——query。永远不要将整个<StaticQuestion card={{ query }} visualization="pie" ... />hook 结果作为{ query, error, isLoading }传递。card.query - 不能 → 和你自己的组件。 仅当用户要求自定义可视化,或者 Metabase 渲染的任何内容都无法表达需求时才使用:定制的记分卡、警报面板、叙述性布局或混合内容卡片、其图表/表格外壳无法表达的自定义交互、不常见的形式如日历网格、时间线、热条、径向视图、自定义地图或特定领域的图表,以及将多个查询组合成一个视觉单元的元素。保持行处理的类型化。使用应用已有的图表依赖;否则用 SVG 也可以。
useMetabaseQuery
StaticQuestionwithChartTypeSelectorwithDownloadstitleInteractiveQuestionisSaveEnabledtrueisSaveEnabled={false}InteractiveQuestion<InteractiveQuestion.QuestionVisualization />当问题位于已经有标题的卡片或部分中时,传递 。 的默认布局会显示问题自己的标题,因此你会得到两个标题; 已经隐藏了它。
title={false}InteractiveQuestionStaticQuestion“它必须匹配我们的样式”不是手动构建的理由:传递 指定图表类型,传递 进行设置级别的更改,其余的通过 SDK 主题设置。数据应用中的饼图就是 Metabase 饼图——除非用户要求自定义,这是他们的决定,而不是从设计参考、品牌颜色或某个定制截图中推断出来的。
visualizationvisualizationSettings单值 KPI 是标量——或者智能标量、仪表盘、进度图。如果图块需要一个数字加上 Metabase 不会绘制的内容,比如星级行、说明文字或来自第二个查询的总数,那么用标量渲染数字,然后把额外的内容放在旁边。只有当数字本身不能来自 Metabase 时,才手动构建。
当 为 时,始终渲染加载指示器(或骨架屏)——永远不要留空槽或显示过时的值,这会在数据到达时导致布局偏移。对于提升/派生的查询(向下传递 )和 的 (按钮中的加载指示器 + )也是同样的规则。
isLoadingtrueisLoadinguseActionisExecutingdisabled={isExecuting}关于 hook 契约本身——泛型、表源、段、度量、分组、排序和调试——在编写基于 schema 的数据层代码之前使用技能发现。
App layout — fill the frame height
应用布局——填满框架高度
Metabase and both give the bundle a full-height frame, but nothing between it and your root element sets a height — so an app shorter than the frame ends where its content ends, leaving its background, sidebar and footer in a short strip over bare white.
npm run devSo the app's outermost element carries (plus when it has padding) and paints the page background. , never — taller content must still scroll. Regions that should reach the bottom (sidebar, sticky footer) go in a flex-column root with . Check the emptiest screen — loading, empty state, a lone KPI — those are the ones that render short.
minHeight: "100vh"boxSizing: "border-box"minHeightheightflex: 1Metabase 和 都为包提供了全高框架,但在框架和你的根元素之间没有任何东西设置高度——因此比框架短的应用会在内容结束的地方结束,导致其背景、侧边栏和页脚只在空白的白色背景上形成一个短条。
npm run dev因此应用的最外层元素应设置 (如果有内边距,还要加上 )并绘制页面背景。用 ,永远不要用 ——更高的内容必须仍然可以滚动。应该延伸到底部的区域(侧边栏、粘性页脚)放在 flex 列布局的根元素中,并设置 。检查最空的屏幕——加载中、空状态、单个 KPI——这些是渲染得最短的情况。
minHeight: "100vh"boxSizing: "border-box"minHeightheightflex: 1SDK component sizing
SDK 组件尺寸
SDK components do NOT auto-fit their parent — without / they render at their intrinsic size and overflow. Setting only the outer container or card height is not enough either; the height goes on the SDK component that owns the visualization:
heightwidth- Chart only: pass to
height.InteractiveQuestion.QuestionVisualization - Default question layout with query bar: pass to
height.InteractiveQuestion - Static question: pass to
height.StaticQuestion
Use the actual body height available to the chart. For example, if a card is 560px tall and has a 60px header, pass .
height="500px"tsx
<div style={{ height: 360 }}>
<StaticQuestion
questionId={1}
height="100%"
width="100%"
withChartTypeSelector={false}
/>
</div>Do not wrap or in containers that clip or move on hover. Avoid , hover transforms, and hover-driven layout shifts around embedded Metabase UI; popovers, menus, and chart tooltips need stable geometry and visible overflow.
InteractiveQuestionStaticQuestionoverflow: hiddenSDK 组件不会自动适应其父容器——如果没有 /,它们会以固有大小渲染并溢出。仅设置外部容器或卡片的高度也不够;高度要设置在拥有可视化的 SDK 组件上:
heightwidth- 仅图表:将 传递给
height。InteractiveQuestion.QuestionVisualization - 带查询栏的默认问题布局:将 传递给
height。InteractiveQuestion - 静态问题:将 传递给
height。StaticQuestion
使用图表可用的实际主体高度。例如,如果卡片高 560px,头部高 60px,则传递 。
height="500px"tsx
<div style={{ height: 360 }}>
<StaticQuestion
questionId={1}
height="100%"
width="100%"
withChartTypeSelector={false}
/>
</div>不要将 或 包装在悬停时会裁剪或移动的容器中。避免在嵌入的 Metabase UI 周围使用 、悬停变换和悬停驱动的布局偏移;弹出框、菜单和图表工具提示需要稳定的几何位置和可见的溢出。
InteractiveQuestionStaticQuestionoverflow: hiddenSync to Metabase
同步到 Metabase
Data apps are delivered by Git, not uploaded — you commit the app directory and Metabase pulls it on its next remote-sync import.
- → produces the bundle at your
npm run builddata_app.yaml(the template builds topath).dist/index.js - From the repo root, commit the app directory — its , the built bundle (the file
data_app.yamlpoints at), the source, and the lockfile — and push:pathbashgit add data_apps/<slug> git commit -m "Add <slug> data app" git push - The app appears in Metabase on the next remote-sync import — a manual Pull changes (Admin → Data apps / Remote sync), the auto-import poll, or a restart — reachable at .
/apps/<slug>
Don't offer to "deploy" the app or ask how the bundle reaches a staging environment — there is no separate deploy step, and the question only confuses users: Metabase imports the committed bundle straight from the connected repo on its next sync. Once the change is on the branch Metabase syncs from — however the user gets it there (a merged PR, or a push straight to that branch) — just tell them to pull it in and open the app in Metabase at./apps/<slug>
- To update: commit a new build and pull again.
- To remove: delete the app's directory from the repo and push — the next sync removes it. You can't delete a repo-managed app from the UI; the Remove action on the Data apps admin page appears only after the repo is disconnected, to clear out apps left behind.
数据应用通过 Git 交付,而非上传——你提交应用目录,Metabase 在下次远程同步导入时拉取它。
- → 在你的
npm run build的data_app.yaml处生成包(模板构建到path)。dist/index.js - 从仓库根目录提交应用目录——包括 、构建后的包(
data_app.yaml指向的文件)、源码和 lockfile——并推送:pathbashgit add data_apps/<slug> git commit -m "Add <slug> data app" git push - 应用会在下次远程同步导入时出现在 Metabase 中——手动拉取更改(Admin → Data apps / Remote sync)、自动导入轮询或重启——可通过 访问。
/apps/<slug>
不要主动提出“部署”应用,也不要询问包如何到达预发布环境——没有单独的部署步骤,这个问题只会让用户困惑:Metabase 在下次同步时直接从连接的仓库导入已提交的包。一旦更改出现在 Metabase 同步的分支上——不管用户是怎么弄过去的(合并 PR,或直接推送到该分支)——只需告诉他们拉取更改,然后在 Metabase 中通过打开应用即可。/apps/<slug>
- 更新: 提交新的构建并再次拉取。
- 删除: 从仓库中删除应用目录并推送——下次同步会删除它。你无法从 UI 中删除仓库管理的应用;只有在断开仓库连接后,数据应用管理页面上的删除操作才会出现,用于清理遗留的应用。
Common pitfalls
常见陷阱
| Symptom | Fix |
|---|---|
| "Failed to fetch the user, the session might be invalid." | Bad API key or CORS — check `( ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"; [ -n "$ROOT" ] && source "$ROOT/.env.local" 2>/dev/null; [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] && curl -H "x-api-key: $DATA_APP_MB_API_KEY" "$DATA_APP_MB_URL/api/user/current" |
| Invisible chart labels. | Set |
| A chart looks unlike the rest of the instance, ignores the theme, or has no tooltips, formatting or drill-through. | It was built in React. Render it with |
| Chart overflows its container. | Pass |
| App background stops partway down, bare white below short content. | Give the root |
| "Invalid hook call" at runtime. | Two React copies. |
| Bundle is multi-MB. | React/the SDK should be externalized by the contract plugin — confirm |
| |
| Run |
| The hook got an inline object, a |
| Drill popups don't open / SDK components show empty / "MetabaseProvider not found" at runtime in dev. | |
Dev preview blank / | |
A network call works in | It was never allowed — you edited |
| The SDK was rebuilt or reinstalled but the preview still behaves like the old version. | Vite pre-bundles the SDK into |
| Component state resets on every edit. | Expected: dev rebuilds the bundle and does a soft reload — re-evaluates it in the sandbox and remounts the app (auth/SDK stay loaded, no browser refresh). There's no module-level HMR / Fast Refresh because the app is an evaluated bundle in an isolated realm, so local component state resets. |
| URL changes but UI doesn't update in production (works in dev). | Import the routing primitives from |
| 症状 | 修复方法 |
|---|---|
| “Failed to fetch the user, the session might be invalid.” | API 密钥或 CORS 有问题——检查 `( ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"; [ -n "$ROOT" ] && source "$ROOT/.env.local" 2>/dev/null; [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] && curl -H "x-api-key: $DATA_APP_MB_API_KEY" "$DATA_APP_MB_URL/api/user/current" |
| 图表标签不可见。 | 在主题中设置 |
| 图表看起来与实例的其他部分不同、忽略主题,或者没有工具提示、格式或钻取功能。 | 它是用 React 构建的。改用 |
| 图表溢出其容器。 | 将 |
| 应用背景中途停止,短内容下方是空白白色。 | 给根元素设置 |
| 运行时出现“Invalid hook call”。 | 有两个 React 副本。 |
| 包大小达到数 MB。 | React/SDK 应该被契约插件外部化——确认 |
| |
| 运行 |
hook 调用时出现 | hook 收到的是内联对象、 |
| 钻取弹窗无法打开 / SDK 组件显示为空 / 开发环境运行时出现“MetabaseProvider not found”。 | |
开发预览空白 / | |
网络调用在 | 它从未被允许——你编辑了 |
| SDK 已重新构建或重新安装,但预览仍然表现得像旧版本。 | Vite 将 SDK 预打包到 |
| 每次编辑后组件状态都会重置。 | 预期行为:开发环境会重新构建包并执行软重载——在沙箱中重新评估它并重新挂载应用(认证/SDK 保持加载状态,无需浏览器刷新)。没有模块级 HMR / 快速刷新,因为应用是在隔离领域中评估的包,因此本地组件状态会重置。 |
| 生产环境中 URL 变化但 UI 不更新(开发环境正常)。 | 从 |