mcpField
M
metabase-data-app-setup

metabase-data-app-setup

github.com/metabase开发49.9k804 次安装今天更新
代码仓库

关于 metabase-data-app-setup

面向 Metabase Data App 开发者的项目搭建指南,使用随附模板在远程同步 Git 仓库中创建 Vite、React 和 TypeScript 应用。涵盖开发预览、构建及应用目录配置。

主要功能

  1. 01复制随附的应用模板
  2. 02配置远程同步仓库中的应用目录
  3. 03使用沙箱预览应用
  4. 04构建单一 JS bundle
  5. 05查看开发诊断信息

适用场景

  1. 01在 Metabase 仓库中初始化 Data App
  2. 02为新应用配置目录与清单
  3. 03预览并构建 Data App

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/metabase/metabase --skill metabase-data-app-setup

安装后在 Claude Code 中输入/metabase-data-app-setup确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
template/
.env.local.example
.gitignore
data_app.yaml
LICENSE
package.json
README.md
tsconfig.json
vite.config.ts
template/actions/
README.md
template/queries/
README.md
template/src/
App.tsx
index.tsx
theme.ts

详细介绍

创建 Metabase 数据应用

Metabase 数据应用是一个单一的 JS bundle,宿主将其加载到 Near Membrane 沙箱中,并在自己的 React 树中渲染。脚手架是一个 Vite + React + TypeScript 项目:源代码位于 src/ 下,开发服务器会针对真实的 Metabase 预览应用——通过 Metabase 在生产环境中使用的同一个 Near Membrane 沙箱 + 失真规则——因此 npm run dev 的行为与生产环境一致,对于应用打包的第三方库也是如此——而 npm run build 会生成单个 dist/index.js。(因为沙箱运行的是构建后的 bundle,所以更改会重新构建它并进行一次软重载——在沙箱中重新求值 bundle 并重新挂载应用,同时保持 auth/SDK 已加载——而不是热替换模块;组件状态会重置,但不会进行完整的浏览器刷新。)开发预览还会显示角落处的 ⚠ Diagnostics 工具栏,用于捕获运行时错误——包括沙箱原本不透明的被阻止 API 消息——这样失败就会暴露出来,而不是被吞掉。同样的数据会以 JSON 形式提供在 http://localhost:5174/__data-app/diagnostics,这就是你读取它的方式(参见下文“读取诊断信息流”)——你拥有的是 shell,而不是浏览器,否则这些失败从终端中是不可见的。

数据应用由 Git 提供(也可以通过 /api/apps 创建和更新)。单个仓库通过远程同步(Admin → Settings → Remote sync)连接到 Metabase。每个应用都位于该仓库内自己的目录 data_apps/<slug>/ 中——包括其源代码、一个 data_app.yaml(slug/name/path/collection),以及提交到其 data_app.yaml 所声明 path 处的构建后 bundle(默认是 dist/index.js)。它的集合,连同其保存的问题以及它所使用的指标和操作的副本,都作为 YAML 位于仓库的 collections/data_apps/ 下,就像 data-apps 命名空间的每个集合一样。在每次远程同步导入时,Metabase 会物化每个应用并将其提供在 /apps/<slug>,其中 slug 就是其 data_app.yaml 所声明的 slug。因此,此技能始终脚手架到已连接仓库的 data_apps/<slug>/ 目录中,而绝不是作为一个独立项目。

脚手架随此技能一起提供,位于 ./template/——这是一个 Vite + React + TypeScript 项目,已随技能一起安装。第 3 步只是将其复制到应用目录中;然后该技能会指导你完成自定义 + 首个应用内容步骤——它从不会从头生成项目文件。如果你发现自己正在手写 package.json、vite.config.ts、tsconfig.json 或 src/index.tsx,请停下来——改为复制模板。

何时调用此技能

  • “脚手架一个新的数据应用” / “创建一个 Metabase 数据应用” / “设置一个数据应用项目”
  • “我想构建一个数据应用” / 任何编写数据应用的模糊意图
  • 开始一个将生成数据应用 bundle 的全新 Agent 任务。
  • “移除 数据应用” / “删除一个数据应用”——仅下文中的移除应用适用。
  • 当任务是为现有数据应用项目构建屏幕、使用 Metabase 数据、生成或刷新 schema 文件、连接保存的问题 / 表 / 指标 / 操作、添加筛选器或编写数据钩子时,不要使用此技能。 将这些视为现有数据应用的编辑任务,并根据用户的措辞使用 Agent 的正常技能发现流程。

第 1 步——定位远程同步仓库

数据应用位于通过远程同步连接到 Metabase 的 Git 仓库中。在脚手架之前先找到它:

  • 询问:“你是否已经有一个通过远程同步连接到此 Metabase 的 Git 仓库?”
    • 是 → 询问其本地路径。
    • 否 → 请用户创建一个(一个普通的 Git 仓库,他们将在 Admin → Settings → Remote sync 下连接)并分享其路径。此技能不会创建或连接该仓库——那由用户负责。
  • 验证该路径存在并且是一个 Git 工作树(它有一个 .git)。该仓库是下面每个步骤的工作目录。

步骤 2 — 为应用命名并创建其目录

  1. 在搭建脚手架之前先确定应用的 slug —— 即 /apps/<slug> URL —— 因此它 必须使用短横线连接:小写字母、数字和单个短横线([a-z0-9]+(?:-[a-z0-9]+)*),例如 sales-overview。其他任何形式(大写字母、空格、下划线)都会在同步时被拒绝。如果用途尚不明确,用一句话询问"这个应用是做什么的?"并提出一个 slug;然后确认。
  2. 确保 <repo>/data_apps/ 存在;若不存在则创建。
  3. 创建 <repo>/data_apps/<slug>/。如果它已经存在,将其视为现有项目(见下文)—— 未经确认绝不覆盖。
检测现有应用

如果 <repo>/data_apps/<slug>/ 中已经包含一个项目,请验证它是否与当前的 data-app-template 匹配。检查 以下全部 项:

  1. vite.config.ts 是一行代码:export default dataAppConfig() (来自 @metabase/embedding-sdk-react/data-app-dev/config)。没有 本地 config/ 目录:整个 bundle 契约(externals/globals、 开发沙箱入口、CSS/SVG 处理)都在该 SDK 配置中,而不是在 脚手架中。dataAppConfig 只接受一组精选的覆盖项(目前仅 port);契约插件始终会被应用,且无法被覆盖。
  2. src/index.tsx 默认导出一个 DataAppFactory(类型来自 @metabase/embedding-sdk-react/data-app),返回 { component, providerProps? } (无参数)。
  3. data_app.yaml 声明的 version: 与本技能的 template/data_app.yaml 相同(没有该行的清单即为版本 1)。版本较低不是漂移,而是应用过时:停止。 迁移它是一个 单独的任务;在扩展一个过时的数据应用之前,请使用 Agent 的常规技能发现流程来迁移它。

所有检查通过 → 符合模板结构。询问:"是扩展此应用,还是以不同的 slug 搭建一个新应用?" 如果要扩展 → 跳过复制步骤,直接编辑 src/。如果要新建 → 选择另一个 slug 并从步骤 2 重新开始。

任何检查失败 → 不符合模板结构(较旧的脚手架或漂移)。停止。 告知用户其结构与当前模板不同,扩展它可能会破坏 bundle 契约,并询问是否要 (1) 迁移它(单独的任务;使用技能发现来迁移过时的数据应用),(2) 以新的 slug 从头搭建并将代码移植过去,或 (3) 自行承担风险继续。等待答复。

未经明确确认,绝不覆盖现有文件。

步骤 3 — 将模板复制到应用目录

模板随 本技能 一起发布,位于 ./template/(通过 skills add metabase/metabase/skills#release-x.<major>.x 与技能一同安装)。将其复制到应用目录:

APP_DIR="<repo>/data_apps/<slug>"
# `<skill-dir>` = the directory this SKILL.md was loaded from
# (e.g. `.claude/skills/metabase-data-app-setup`); the template is its `template/` subfolder.
cp -R "<skill-dir>/template/." "$APP_DIR/"

数据应用是远程同步仓库的 子目录,而不是独立的仓库 —— 因此这是普通的复制,绝不是嵌套的 git clone / git init。以下所有操作都在 $APP_DIR 内 运行。

复制内容包含两个根级目录 queries/ 和 actions/,每个目录中仅有一个 README.md。即使它们为空也要保留两者:应用运行的每个查询都是 queries/ 中的 defineQuery(...) 导出,每个动作都是 actions/ 中的 defineAction(...) 导出,而钩子会在编译时拒绝其他任何内容。在编写第一个 useMetabaseQuery / useMetabaseQueryObject / useAction 调用之前,请先阅读这两个 README。

步骤 4 — 自定义

模板放入 <repo>/data_apps/<slug>/ 后(以下所有操作都在该目录中运行):

  1. 将 package.json 的 name 编辑为与 slug 一致。

  2. 将 @metabase/embedding-sdk-react 固定到已发布的 data-apps 标签(模板自带的是 *):

    npm install @metabase/embedding-sdk-react@64-alpha
    

    这会解析为当前的内部测试 SDK 构建,入口点为 @metabase/embedding-sdk-react/data-app(应用的 API)以及 @metabase/embedding-sdk-react/data-app-dev/config,后者是 vite.config.ts 使用的入口点(开发/构建预设,用于提供沙箱入口)。不要使用 latest 或 64-stable,因为数据应用尚未发布。

  3. 确保仓库根目录的 .gitignore 忽略 .env.local——在创建任何凭据文件之前就要完成此操作,这样密钥就永远不会被提交。如果仓库没有 .gitignore,请创建它,然后如果缺少相应条目就添加进去:

    ROOT="$(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
    
  4. 在仓库根目录设置 Metabase 凭据——<repo>/.env.local(通常位于应用目录往上两级),而不是应用目录。那里的一个 .env.local 就可供仓库中的每个数据应用使用。

    如果不存在,就从示例文件创建它,然后在不打印文件内容的情况下验证这两个变量已设置(该文件可能还包含其他密钥)——source 它,并且只输出通过/失败信号,绝不输出值:

    # 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"
    fi
    

    如果它输出 MISSING,请用户自行在 <repo>/.env.local 中填写 DATA_APP_MB_URL(正在运行的 Metabase 实例)和 DATA_APP_MB_API_KEY(Admin → Authentication → API keys,Administrators 组中的密钥,因为类型化 schema 只应答管理员)——要提前完成,赶在任何东西需要该密钥之前。

    绝不要让用户把 API key 粘贴到聊天中,也绝不要 cat / echo / 打印 .env.local 或其变量。 它已被 git 忽略,并且可能包含其他密钥——文件内容和该密钥绝不能进入对话或你的上下文。每个需要该密钥的命令都要(像上面那样)source 该文件,这样 shell 会直接使用该值;你只会看到 creds present / MISSING 信号,绝不会看到密钥本身。(creds present 只表示这两个变量都已填写,并且不是默认的 mb_replace_me 占位符——并不表示 URL 或密钥有效;错误的密钥会在之后请求失败时暴露出来。)

  5. npm install(或用户偏好的任何包管理器——模板不附带 lockfile,因此 npm / yarn / pnpm / bun 都可以;如果克隆后出现了项目现有的 lockfile,就使用它)。

  6. lockfile 和构建出的 bundle 都必须提交。 Metabase 会直接从已提交的 Git 树中提供 data_app.yaml 里声明的 path 处的文件(模板构建到 dist/index.js),而 lockfile 则确保安装可复现。在 npm install 和一次构建后,用 git status 验证:两者都必须作为可提交文件出现。

  7. npm run dev,并确认 http://localhost:5174 的预览渲染出起始的 "Hello, data app" 消息。

  8. 如果预览遇到 CORS,请在 Admin → Embedding → Embedded analytics SDK → CORS 下添加 http://localhost:5174。

  9. 填写 data_app.yaml,这是模板的清单文件,现在位于应用目录中。这是 Metabase 在同步时读取的按应用配置,每个应用一个文件。它的字段、其中哪些是必需的,以及它们的规则,都在 Metabase representation format 规范的 Data App 部分中定义:加载用于读取和写入 Metabase representation YAML 的 skill(使用 skill discovery),并遵循其规范。如果没有这样的 skill 可用,请用 npx skills add metabase/agent-skills/skills --skill metabase-representation-format 安装它;如果仍然无法加载,请停止并告诉用户。为此应用设置 name、slug 和 description,并将 entity_id 设置为来自 npx representations generate-entity-id 的 ID(该命令来自应用自身的开发依赖,在 npm install 之后)。version 和 serdes/meta 保持模板附带的原样,path 也一样,除非你更改了构建输出。collection 在第 10 项之前保持为空。绝不要从另一个应用的清单中复制值。 description —— 可选:一句话简短说明该应用的作用,显示在管理 UI 中应用名称下方,便于管理员一眼区分不同应用。同步会将任何空白折叠为单个空格,并拒绝超过 255 个字符的内容;管理列表会对剩余内容自动换行而不是截断,因此一句话在此处读起来很合适,而一段话则会挤占周围的行。请用一句关于此应用的真实描述替换模板中的占位内容,如果它除了名称之外没有补充任何信息,则干脆删除该行。

    version —— 此应用的代码所针对的数据应用契约版本,为一个整数。模板自带的是本技能所针对的版本;请勿手动更改。Metabase 仅在契约发生破坏性变更时才会提升其提供的版本。版本较旧的应用会在管理列表中被标记为已过时,对其他所有用户隐藏,并且拒绝打开,直到它迁移到当前契约、将其 version 提升至匹配、重新构建并同步。该迁移是一个独立的、有指引的流程,每次迁移一个版本;请通过技能发现来找到它,切勿临时随意执行。

    allowed_hosts —— 仅当应用使用 fetch/XHR 直接调用外部 API 时才需要。沙箱默认阻止所有网络出站;在此列出一个源(精确匹配或 *. 子域通配符)会在 npm run dev(开发服务器 CSP)和 Metabase(iframe CSP + 沙箱)中同时放行它。请不要列出 Metabase 实例——Metabase 数据通过 useMetabaseQuery 数据钩子读取,通过 useAction 写入(SDK 负责认证),绝不使用原始 fetch。当应用只与 Metabase 通信时,请完全省略 allowed_hosts。

    原生 <form action="…"> 提交以及 <iframe src="…">/页面跳转遵循相同的允许列表。优先使用客户端侧的 <form onSubmit>,在其中 preventDefault 并通过 useAction/fetch 写入:原生提交会使沙箱 iframe 离开应用。如果你确实使用了原生提交,目标主机必须位于 allowed_hosts 中,否则会被阻止(提交对应 form-action,嵌入/跳转对应 frame-src)。你跳转或嵌入的主机还必须允许被嵌套(X-Frame-Options/frame-ancestors)——许多公共网站并不允许。

  10. 编写该应用的 collection。 使用 npx representations generate-entity-id 生成其实体 ID,将该 collection 的文件写入 仓库的 collections/data_apps/ 下(使用技能发现来查找关于编写应用 collection 文件的 数据应用指引),并将该 ID 设置为 data_app.yaml 中的 collection。

应用各文件的顺序

应用的 YAML 按此顺序编写,每一步都依赖前一步:

  1. data_app.yaml,从第 3 步的模板复制而来,并在第 9 项中填写。
  2. collections/data_apps/ 下该 collection 的文件,在第 10 项中于 data_app.yaml 中命名为 collection。
  3. npm run print-resources,在应用于 queries/ 或 actions/ 中有定义之后运行。它会将已保存的问题打印到 data_app.yaml 所指定的 collection 中,因此在清单指定 collection 之前它拒绝运行。
  4. 该 collection 目录中的其余文件:每个查询对应一个已保存问题,以及它们所使用的 指标和操作的副本,按它打印出的内容编写。

脚手架在第二步之后结束。最后两步是构建应用数据层的一部分; 请为此使用技能发现。

第 5 步 — 验证起始应用

此时数据应用已经存在。请让这个工作流专注于创建项目脚手架并验证起始包可以正常工作。

  1. 运行 npm run typecheck。

  2. 运行 npm run check-resources。当 data_app.yaml 未指定任何 collection,或指定的 collection 没有对应文件时,它会失败;同时它会按照格式校验仓库中的 YAML。它不会更改 Metabase 中的任何内容。

  3. 运行 npm run build。

  4. 确认 git status 显示应用源代码、lockfile、data_app.yaml、collections/data_apps/ 下该 collection 的文件,以及构建出的 bundle(默认是 dist/index.js)均为可提交文件。

  5. 如果用户要求实时预览,运行 npm run dev,并确认起始的 “Hello, data app” 界面通过沙箱预览正常渲染。

  6. 在预览打开的情况下,检查一次诊断信息流 — 这是运行时故障唯一会显示给你的地方(参见 读取诊断信息流):

    curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"
    

    预期 clients: 1,且没有带有 "alert": true 的条目。clients: 0 表示没有打开任何预览标签页,因此空的信息流不能证明任何问题。

如果用户只要求创建、搭建脚手架或设置数据应用,请到此为止。

如果下一个任务是构建或迭代实际的应用 UI — 尤其是当它提到已有数据应用、Metabase 数据、生成的 schema 文件、保存的问题、表、指标、操作、过滤器、语义层实体或数据 hook 时 — 请将其视为一个单独的、针对已有数据应用的编辑任务。根据这些术语使用 Agent 的常规技能发现流程。不要用数据层编写规则来扩展这个脚手架技能。

除非确实需要,否则不要修改 src/index.tsx 或 tsconfig.json。 整个构建/开发设置都位于 dataAppConfig() 背后的 SDK 中(它也负责提供开发用 HTML shell — 没有可编辑的 index.html),因此 vite.config.ts 只是:

import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config";

export default dataAppConfig();

dataAppConfig 只暴露一组精选的覆盖项(目前仅有 port)。整个契约 — 工厂形态、externals/globals、开发沙箱入口、CSS 内联以及 SVG 作为组件的支持 — 都已内置,且无法被覆盖;这是有意为之,这样数据应用就不会偏离 Metabase 实际加载的内容。没有本地构建配置需要改动(也没有 index.html — SDK 的开发服务器会提供 HTML shell),而对 src/index.tsx 的调整仍有可能破坏工厂形态,并悄无声息地破坏下钻弹窗和路由。

这里有意不提供逃生通道来添加额外的 Vite 插件、别名或 define — port 是唯一的可调项。如果你认为自己需要更多,那你几乎肯定不需要;请在 src/ 中解决。

每一轮有意义的编辑之后,都要运行 npm run typecheck。 它会对 src/ 和 vite.config.ts 运行 tsc --noEmit — 能捕获与 SDK 类型不匹配的 prop 形态、损坏的重构、缺失的 import 等问题。Vite 开发服务器不会进行类型检查(它只做转译),因此那些会导致生产 CI 运行失败的错误,可能会在一个通过的 npm run dev 会话中隐形存在。在宣布任务完成之前请运行它。

交接之前,请再次检查 package 卫生。 @metabase/embedding-sdk-react 应使用目标环境所期望的 data-app SDK 来源/标签。当应用只需要日期范围时,不应安装任何日期选择器依赖 — 不要安装 react-datepicker、react-day-picker、flatpickr,或某个 UI 套件的选择器(@mui/x-date-pickers、antd、rsuite 等);那应该使用来自 @metabase/embedding-sdk-react/data-app 的 DateRangePopover。除非所选的 react-datepicker 版本确实需要,否则不应安装 @types/react-datepicker。

读取诊断信息流

npm run dev 会将工具栏显示的一切以 JSON 形式提供——这是你看到运行时失败的唯 一途径,因为沙箱拦截、CSP 拒绝、失败的查询和未捕获的错误既不会到达终端,也不会出现在 npm run typecheck 中。在任何你无法通过阅读代码来验证的更改之后,都请检查它。

**循环:**在编辑前记下 nextEventId → 进行更改(自动重新构建)→ 重新读取。startEventId 是包含性的,并且在页面重新加载后仍然有效。

curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"
{
  "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: 0 并不意味着健康——它意味着没有打开预览标签页,因此空的 entries 什么也证明不了。请先打开 http://localhost:5174。

**信息流针对的是正在运行的 bundle,而不是曾经发生过的一切。**每次保存都会重新构建并重新挂载应用,因此多步编辑会经历无法编译或无法渲染的构建——这些构建中的错误描述的是预览已经替换掉的代码。它们会被扣留并计入 staleEntries;在编辑中途读取,你看到的是当前构建的失败,而不是一堆失败。不会有任何丢失:重新构建会从头重新运行应用,因此任何仍然损坏的地方都会在新的 buildId 下再次报告自身,而当比较构建是目的时,?includeStale=true 会交回被扣留的条目。失败的构建不会推进 buildId——预览会继续运行最后一个良好的 bundle,因此其条目保持为最新。

按 kind 进行分类(始终阅读 hint——它会指出确切的修复方法;在报告时切勿弱化 summary):

kind修复
blocked-network / csp-violation将源添加到 data_app.yaml 中的 allowed_hosts,然后重启 npm run dev(allowlist + CSP 在启动时读取)
blocked-api在生产环境中也会被阻止——使用 SDK API 或放弃该调用,不要绕过它
sdk-call + alert: true一个 Metabase 请求失败;修复查询——summary 中有 endpoint 和 status
error真正的 bug;detail 中有堆栈

文本会被截断,缓冲区保留最后 200 个事件。curl -X DELETE .../__data-app/diagnostics 会为所有读取者清除它。

源码约定

1. 编写 TSX

普通 ESM + 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. 从一开始就规划好结构

扩展起始应用后的默认项目布局:

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 — use skill discovery for the data-app semantic layer)
├── 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.ts

Vite 会将从 src/index.tsx 可达的所有内容打包成单个 dist/index.js IIFE——src/ 布局纯粹是为了方便你自己阅读。但 queries/ 和 actions/ 不是:这两个根级目录中的每个定义都由仓库 collections/data_apps/ 下应用 collection 中的一个文件支持,npm run build 在它们匹配之前会失败,并且 query 和 action 钩子只接受在那里声明的 defineQuery / defineAction 导出。在钩子调用处、src/ 下或从定义展开(spread)编写的 query 对象无法编译(Property 'definedWithDefineQuery' is missing);修复方法是把它移到 queries/ 中并导入,绝不要用类型断言。

**如果应用有多个标签页(或任何顶层屏幕切换器),初始加载时必须选中默认——最左侧/第一个——标签页。**应用绝不应启动到空白页、空壳,或等待用户点击的"未选择任何内容"状态。Agent 会反复忘记这一点。对于本地状态标签页,将活动标签页初始化为第一个,这样首次渲染就会显示它:

const [active, setActive] = useState(TABS[0].id); // default = leftmost tab

如果标签页改为由 URL 路由支持(多个页面),同样的规则通过路由器适用——使用技能发现进行 data-app 路由,使基础路径 / 解析到默认标签页。无论哪种方式,都要通过全新加载应用来验证:最左侧标签页的内容立即可见,并且读起来像是已选中。

**构建输出是一个自包含的 .js 文件——没有其他东西。**后端只提供一个 bundle,因此没有附属文件:CSS 内联到 JS 中,每个导入的资源(图片、字体、作为 URL 的 SVG)都以 data URI 的形式 base64 内联。所以 import logo from "./logo.png" / import iconUrl from "./icon.svg" 会给你一个可直接使用的 data-URI 字符串,SVG 也可以通过 ?react 后缀(内置的 svgr)作为 React 组件导入:import Icon from "./icon.svg?react"。所有内容都会烘焙进 dist/index.js——只需避免放大型二进制文件,因为内联会使 bundle 膨胀。(如果你的编辑器无法识别 ?react 导入,请在 src/ 中的 .d.ts 里添加 declare module "*.svg?react";。)

3. 从正确的 SDK 入口点导入 SDK 值

构建会在生产环境和 npm run dev 中都将 React、JSX 运行时、@metabase/embedding-sdk-react 和 @metabase/embedding-sdk-react/data-app 外部化到沙箱全局变量——在 dev 中,沙箱入口会从 npm 包赋予它们,因此 bundle 在两种模式下运行方式完全相同。只需正常从入口点导入:

// ✅ 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;

**不要在 App.tsx 中渲染 <MetabaseProvider>。**dev 入口(由 SDK 的 dev 预设提供)和生产宿主都会在你自己的 realm 中把你的树包裹在 provider 里——在 bundle 内部包裹会使 SDK 通过监听器调用 setState 的路径经过 Near Membrane 沙箱,并静默破坏下钻弹窗、插件初始化等。

4. 也正常导入 react

构建会外部化 react 和 react-dom,因此普通导入在两种模式下解析方式相同——生产宿主和 dev 沙箱都将它们赋予为沙箱全局变量:

import { useState, useEffect, useMemo } from "react";

TSX 文件中不需要 import React from "react"——模板使用自动 JSX 运行时(tsconfig.json 中的 jsx: "react-jsx";dataAppConfig() 包含 React 插件)。编译器会注入它所需的 JSX 运行时导入(生产中是 react/jsx-runtime,dev 中是 react/jsx-dev-runtime——两者都被外部化并由沙箱赋予)。只需编写 JSX 和具名导入——就这样。

对于 React 类型(例如 ComponentType、ReactNode、RefObject),请使用具名类型导入,而不是 React. 命名空间:

import type { ComponentType, ReactNode } from "react";

主题规则

MetabaseProvider 的 theme(定义于 src/theme.ts)是改变 SDK 组件外观的唯一方式。它并不是用于该 bundle 自身界面外壳的样式表。

字段用途说明
colors.brandSDK 组件的强调色使用数据应用的主色。
colors.brand-hover、colors.brand-hover-lightSDK 组件的悬停/强调背景色使用与悬停文字形成对比的微妙表面色调。不要使用与 brand 相同的饱和色。
colors.charts图表调色板字符串数组。仅使用鲜艳的品牌色调;绝不要使用浅淡色调 —— 调色板索引 1+ 可能用于渲染标签,而浅色在白色背景上会不可见。
colors.positive / colors.negative语义指示色
colors.background、colors.background-secondarySDK 组件表面必须与 SDK 组件的直接父容器匹配。 如果图表位于白色卡片内,请使用 "white"。如果它位于带色调的页面上,请使用该色调。
colors.text-primary、text-secondary、text-tertiarySDK 表面上的文字必须与 background 形成对比。 对于白色背景:使用 #1f2937 或类似的深色。SDK 默认的 text-primary 解析结果接近白色,因此在白色表面上不设置它会导致文字变成不可见的白色。覆盖时务必成对设置 background 和 text-primary。
fontFamilySDK 组件排版

悬停颜色是一组对比色对。如果你设置了 colors.text-hover,请验证它与打开菜单、图表类型选择器和可视化设置下拉菜单中的 colors.brand-hover / colors.brand-hover-light 形成对比。对于蓝色 brand,请使用浅淡的悬停表面色,如 #EAF4FF,而不是品牌蓝本身。

主题只作用于 SDK 组件 —— 绝不会作用于你自己的 UI。 对于页面背景、页眉、卡片包裹容器等,请在你自己的元素上使用内联 style={{ background: "#f5f5f7" }} 或 CSS modules。

不要期望按子树进行主题设置。 页面上多个 <MetabaseProvider> 会争抢唯一的 CSS 变量槽位。如果外观需要随状态变化,请在 App 层级重新计算 sdkTheme 并重新渲染 —— 整棵树都会重新应用主题。

自定义错误 UI(可选)

当嵌入的问题或仪表板无法加载时 —— 其 questionId 已被移除、用户缺少访问权限,或请求失败 —— 宿主会渲染一个中性的内置错误状态来替代该组件(一个柔和的图标 + SDK 的提示消息)。这无需任何设置;除非应用有充分理由重新设计其样式,否则请保持原样。

要匹配应用的外观,请在工厂的 providerProps(与携带 theme 的同一个对象)中返回你自己的 errorComponent。它只是一个普通的 providerProps 键,不会改变工厂的结构 —— 可以安全添加:

// 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>
  );
}
// src/index.tsx — add the key alongside `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 即可保留中性默认值。无需重新实现它。

可用的 SDK 接口

该 bundle 会导入 React hooks/JSX、来自 @metabase/embedding-sdk-react 的 SDK 组件,以及来自 @metabase/embedding-sdk-react/data-app 的数据应用专用路由/查询 API。dataAppConfig() 会在构建时将这些包外部化,因此生产环境在运行时会引用宿主的副本(SDK 包对应 globalThis.__metabase_sdk__ / globalThis.__metabase_data_app__),而 Vite 开发沙箱则会从已安装的 npm 包中注入相同的全局变量。

<MetabaseProvider> 不会由 bundle 的 App.tsx 渲染 —— 开发入口和宿主会针对各自的模式对其进行包裹。Bundle 作者只渲染下面的内容。

导入用途
React(来自 "react")Hooks(useState、useEffect 等)、JSX 运行时。通过 react: "React" 外部化到宿主的 React。
StaticQuestion不可下钻的问题。Props 包括 questionId、card、withChartTypeSelector、height、width。
InteractiveQuestion可下钻的问题。Props 与 StaticQuestion 相同,另加下钻行为。对于临时的 SDK 渲染问题,使用 card={{ query }}。关于如何在两者之间选择以及 visualization / visualizationSettings,参见 Rendering a chart: Metabase first。
MetabaseCard仅类型导入,来自 @metabase/embedding-sdk-react,用于带有 visualization 或 visualizationSettings 的临时 SDK 渲染卡片;在编写数据层代码之前,使用 skill discovery 获取完整的生成查询卡片契约。
CreateQuestion、MetabotQuestion更多问题变体。
StaticDashboard、InteractiveDashboard、EditableDashboard仪表盘变体。
CreateDashboardModal用于新建仪表盘流程的模态框。
CollectionBrowser集合选择器。
@metabase/embedding-sdk-react/data-app 导出仅限数据应用的辅助工具,用于路由、基于 schema 的数据读取、操作、剪贴板以及沙箱安全集成。将基于 schema 的查询、生成的 schema 文件、筛选器、指标、操作以及其他数据层行为视为现有数据应用的编辑工作;在编写该代码之前,使用 skill discovery。
DateRangePopover、DateRangeCalendar、useDateFormatter(来自 @metabase/embedding-sdk-react/data-app)用于筛选栏的日期范围选择。DateRangePopover 会包裹应用自身的触发元素,并在其下方打开一个 Metabase 风格的日期范围日历;DateRangeCalendar 则是该日历的内联形式。value/onChange 是 YYYY-MM-DD 字符串的 [start, end] 对,半选状态下任一端为 null。useDateFormatter().formatDateRange(value) 会以实例的 locale 生成触发器的标签,且不会出现 new Date("YYYY-MM-DD") 的 UTC 解析 bug。请使用这些,而不是 <input type="date"> 或第三方选择器 —— 无依赖,无需导入 CSS。
被屏蔽的 API

Near Membrane 沙箱在运行时会对以下全局对象抛出错误。请改用内置的替代方案:

被屏蔽改用
网络 — fetch、XMLHttpRequest、WebSocket使用 SDK/数据应用 API 进行 Metabase 读写 —— 绝不要使用原生 fetch。原生 fetch/XHR 仅能访问 data_app.yaml 中 allowed_hosts 列出的外部源(第 4 步);其他所有内容(包括 Metabase 源)都会抛出错误。WebSocket 始终被屏蔽。
UI 对话框 — alert、confirm、prompt在你自己的组件树中渲染一个 React 模态框。
存储 — localStorage、sessionStorage、indexedDB、document.cookie将数据应用视为跨刷新无状态的;通过 Metabase action 进行持久化。
窗口 / 历史导航 — window.open、history.pushState、history.replaceState应用内导航使用 useDataAppLocation().navigate;外部导航使用 <a target="_blank" rel="noopener">。
剪贴板 — document.execCommand("copy")、navigator.clipboardcopy(只写):import { copy } from "@metabase/embedding-sdk-react/data-app",然后在用户事件中 await copy(text)。没有读取/粘贴 API —— 那会让 bundle 窃取用户复制的任何内容。
其他 navigator.* 设备 API — geolocation 等不可用。
用于输入/剪贴板事件的全局 document/window 监听器 — keydown、keyup、keypress、beforeinput、input、paste、copy、cut、before*paste/copy/cut、compositionstart/update/end、storage将监听器附加到你自己的元素上,或在特定的 input/容器上使用 React 处理函数(onKeyDown、onPaste 等)。在脚本自有元素上的相同监听器仍然有效。

经验法则: 如果你即将访问 window.X、document.X、navigator.X、history.X 或任何存储全局对象,请停下来,选择上面的内置替代方案。内置接口(React + React DOM + SDK 组件 + 数据钩子 + useAction + DataAppRouter + DateRangePopover + copy)涵盖了所有日常需求;除此之外的内容均被有意设为不可访问。

渲染图表:优先使用 Metabase

这是针对每个元素的决策,而不是针对某一行、某个区块或整个应用只做一次的决定。一个需要自定义代码的磁贴,会让所在行中只有一个自定义磁贴,而不是让整行都是自定义磁贴——其余磁贴仍保持为 SDK 组件,并保留其格式、主题和钻取能力。一致的高度和卡片外观来自每个磁贴所在的容器,因此“其他磁贴应该与它匹配”并不是手工构建其他磁贴的理由。

内置可视化会继承实例的主题、无障碍能力、工具提示、格式和钻取能力。用 React 构建的图表则不包含这些,并且只要其中任一方发生变化,它就会与应用中的其他所有图表逐渐不一致。所以问题从来不是“哪个更接近我的设计”,而是 Metabase 能否显示这个?

  • 能 → useMetabaseQueryObject + StaticQuestion / InteractiveQuestion。 条形图、折线图、面积图、组合图、行图、趋势图、饼图/环形图、标量/智能标量、仪表、进度、漏斗、散点、瀑布、箱线图、桑基图、透视表、地图、对象/列表视图、可排序表格,以及图表类型列表中的其他所有类型。此外,只要可视化设置能够承载展示效果,也应如此——坐标轴、标签、堆叠、目标、趋势线、拆分面板、序列、格式、表格/饼图/透视表/列表设置——或者用户能从排序、列检查、钻取或下载中受益。基于生成的 schema 对象构建语义查询,解构返回的 query,并且只将该值传入卡片对象——<StaticQuestion card={{ query }} visualization="pie" ... />。切勿将整个 { query, error, isLoading } hook 结果作为 card.query 传入。
  • 不能 → useMetabaseQuery 和你自己的组件。 仅当用户要求自定义可视化,或者 Metabase 渲染的任何内容都无法表达该请求时:定制记分卡、告警面板、叙述式布局或混合内容卡片、其图表/表格外观无法表达的自定义交互、日历网格、时间线、热力条、径向视图、自定义地图或领域特定图表等不常见形式,以及将多个查询组合为一个视觉单元的元素。保持行处理的类型化。使用应用已有的图表依赖;否则 SVG 即可。

StaticQuestion 是两者中的默认选择。 它只渲染可视化,不渲染其他内容——没有查询栏、没有编辑器、没有保存——并接受 withChartTypeSelector、withDownloads、title 和尺寸相关 props。当该元素需要被探索时使用 InteractiveQuestion:钻取、筛选、切换图表类型或 notebook 编辑器。除非你将其关闭,否则它会显示 Save 按钮——isSaveEnabled 默认为 true——这为查看者提供了保存到集合的流程,而该流程属于 Metabase,不属于应用,因此除非应用有意作为编辑界面,否则请传入 isSaveEnabled={false}。这适用于默认布局;为 InteractiveQuestion 提供自己的 children 会替换该布局,因此组合的 <InteractiveQuestion.QuestionVisualization /> 不渲染工具栏,也不渲染 Save 按钮。

当问题位于已经带有标题的卡片或区块中时,传入 title={false}。 InteractiveQuestion 的默认布局会显示问题自身的标题,因此你会得到两个标题;StaticQuestion 则已经隐藏了它。

“它必须匹配我们的样式”不是手工构建其中一个的理由:传入 visualization 指定图表类型,传入 visualizationSettings 进行设置层面的更改,并为其余部分设置 SDK 主题。数据应用中的饼图就是 Metabase 饼图——除非用户要求自定义饼图,而这是他们自己要做出的决定,而不是根据设计参考、品牌颜色或某个定制内容的截图推断出来的决定。

单值 KPI 就是标量——或者智能标量、仪表、进度。如果该磁贴想要一个数字,外加 Metabase 不绘制的内容,例如星标行、说明文字或来自第二个查询的总计,则渲染该数字的标量,并将额外内容放在其旁边。只有当数字本身无法来自单个标量时,才手工构建它。 当 isLoading 为 true 时始终渲染一个加载动画(或骨架屏)——绝不能是一个空槽位或过期值,否则数据到达时会导致布局偏移。对于提升 / 派生查询(向下传递 isLoading)以及 useAction 的 isExecuting(按钮中显示加载动画 + disabled={isExecuting})也是如此。

关于 hook 契约本身——泛型、表来源、分段、度量、拆分、排序和调试——在编写基于 schema 的数据层代码之前,请使用技能发现。

应用布局——填满框架高度

Metabase 和 npm run dev 都会为 bundle 提供一个全高的框架,但它与你的根元素之间没有任何东西设置高度——因此比框架短的应用会终止于其内容结束之处,导致其背景、侧边栏和页脚在裸白背景上呈现为一条短带。

因此,应用的最外层元素应携带 minHeight: "100vh"(当它有内边距时再加上 boxSizing: "border-box")并绘制页面背景。是 minHeight,绝不是 height——更高的内容仍必须能够滚动。应当延伸到底部的区域(侧边栏、粘性页脚)放在一个 flex 列根元素中,并使用 flex: 1。检查最空的屏幕——加载中、空状态、单个 KPI——这些正是渲染出来偏短的屏幕。

SDK 组件尺寸

SDK 组件不会自动适应其父元素——如果没有 height/width,它们会以固有尺寸渲染并溢出。只设置外层容器或卡片的高度也不够;高度要设置在拥有该可视化的 SDK 组件上:

  • 仅图表:将 height 传递给 InteractiveQuestion.QuestionVisualization。
  • 带查询栏的默认问题布局:将 height 传递给 InteractiveQuestion。
  • 静态问题:将 height 传递给 StaticQuestion。

使用图表实际可用的主体高度。例如,如果一张卡片高 560px 且有一个 60px 的头部,则传递 height="500px"。

<div style={{ height: 360 }}>
  <StaticQuestion
    questionId={1}
    height="100%"
    width="100%"
    withChartTypeSelector={false}
  />
</div>

不要将 InteractiveQuestion 或 StaticQuestion 包裹在会裁剪或在悬停时移动的容器中。在嵌入式 Metabase UI 周围避免使用 overflow: hidden、悬停变换和悬停驱动的布局偏移;弹出框、菜单和图表工具提示需要稳定的几何形状和可见的溢出。

同步到 Metabase

数据应用通过 Git 交付——你提交应用目录,Metabase 会在下一次远程同步导入时拉取它。

  1. 对 queries/ 或 actions/ 的任何更改之后,更新 collections/data_apps/ 下该应用集合的文件以使其匹配(如果 src/metabase.data.ts 未覆盖这些定义所使用的内容,则先重新生成它),并运行 npm run check-resources。在步骤 4 的拉取应用它之前,什么都不会到达 Metabase。
  2. npm run build 会在你的 data_app.yaml 的 path 处生成 bundle(模板构建到 dist/index.js)。当应用的集合文件与定义不匹配时它会失败;修复它们并重新构建。
  3. 从仓库根目录,提交应用目录——它的 data_app.yaml、构建出的 bundle(path 指向的文件)、源代码和 lockfile——连同应用的集合文件一起,然后推送:
    git add data_apps/<slug> collections/data_apps
    git commit -m "Add <slug> data app"
    git push
    
  4. 该应用会在下一次远程同步导入时出现在 Metabase 中——手动拉取更改(Admin > Data apps / Remote sync)、自动导入轮询或重启——可通过 /apps/<slug> 访问。

不要提议“部署”应用,也不要询问 bundle 如何到达预发布环境——没有单独的部署步骤,而且这个问题只会让用户困惑:Metabase 会在下一次同步时直接从所连接的仓库导入已提交的 bundle。一旦更改位于 Metabase 所同步的分支上——无论用户如何将其送到那里(合并的 PR,或直接推送到该分支)——只需告诉他们将其拉取进来,并在 Metabase 中通过 /apps/<slug> 打开该应用。

  • 要更新: 如果定义发生了变化,更新应用的集合文件,提交新的构建,然后再次拉取。
  • 要移除: 参见下方的移除应用。

移除应用

当用户要求移除或删除某个数据应用时,在一次提交中从仓库根目录同时删除其目录以及其集合的文件,然后推送:

git rm -r data_apps/<slug> collections/data_apps/<collection>.yaml collections/data_apps/<collection>
git commit -m "Remove <slug> data app"
git push

<collection> 是 data_app.yaml 中指定为 collection 的集合:即 collections/data_apps/ 下 entity_id 为该值的文件,以及其旁边同名的目录(对于 Data App: Order Desk 即为 data_app__order_desk)。下一次拉取会移除该应用,并随之移除其集合、已保存的问题、指标副本和操作副本。

仅删除目录而不删除其集合的文件,仍会在下一次拉取时移除该应用,并随之移除该集合;之后从 Metabase 的下一次推送会从仓库中删除这些文件。但无论如何都应同时删除两者:在那次推送之前,如果一次拉取重新加载了所有文件,该集合会作为无应用拥有的集合重新出现,随后的推送便会保留它。反过来则会被拒绝:如果应用的目录仍在而集合文件已消失,拉取会失败。

常见陷阱

现象修复
“Failed to fetch the user, the session might be invalid.”API key 错误或 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"
图表标签不可见。在主题中设置 text-primary(参见 主题规则)。
某个图表看起来与实例的其余部分不一致、忽略主题,或没有提示框、格式设置或钻取功能。它是用 React 构建的。改用 StaticQuestion/InteractiveQuestion 和 visualization 来渲染它(参见 渲染图表:Metabase 优先)。
图表溢出其容器。向 SDK 组件传入 height / width(参见 SDK 组件尺寸)。
应用背景在中间某处停止,内容较短时下方为纯白色。为根元素设置 minHeight: 100vh(参见 应用布局)。
运行时出现 “Invalid hook call”。存在两份 React 副本。dataAppConfig() 会将 react 外部化 — 请确保已安装 react/react-dom,且没有添加第二个 React 或版本不匹配的副本。
打包体积达数 MB。React/SDK 应被契约插件外部化 — 请确认 vite.config.ts 仍使用 dataAppConfig(),且已安装固定版本的数据应用 SDK 标签。(体积大但不足数 MB 的打包也可能是内联资源 — 参见上文单文件说明。)
dist/index.js 未赋值给 __dataAppFactory__。src/index.tsx 必须 export default 导出 DataAppFactory — 预设会将其接入 IIFE 全局变量。
Cannot find module '@metabase/embedding-sdk-react'。运行 npm install(或你所用的包管理器的等效命令)。类型直接来自该包。
在 hook 调用时出现 Property 'definedWithDefineQuery' is missing / Property 'definedWithDefineAction' is missing。该 hook 收到的是内联对象、用 satisfies 标注类型的对象,或展开副本,而不是定义。请用根级 queries/ 中的 defineQuery(或 actions/ 中的 defineAction)导出它,并传入该导入。不要进行类型断言,也不要在调用处将对象包裹在 defineQuery(...) 中:那样能编译,但永远不会由已保存的问题支撑。
钻取弹窗无法打开 / SDK 组件显示为空 / 开发环境中运行时出现 “MetabaseProvider not found”。App.tsx 正在渲染它自己的 <MetabaseProvider> — 请将其移除。开发入口(SDK)和生产宿主会提供 provider;在打包内包裹它会让 SDK 的状态路径穿过沙箱并破坏它们。
开发预览空白 / Bundle did not assign a function to __dataAppFactory__ / 开发环境中出现沙箱错误。src/index.tsx 未默认导出工厂函数,或你的应用代码在沙箱求值打包时抛出异常。从诊断源(curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0")或开发工具栏的 Diagnostics 面板中读取真正的错误。
某个网络调用在 npm run dev 中正常工作,但同步到 Metabase 后被阻止。它从未被允许 — 你编辑了 allowed_hosts 却未重启开发服务器,因此正在运行的沙箱和 CSP 仍使用启动时的列表。重启 npm run dev;诊断源的 manifest 部分会将其标记为 restartRequired。
SDK 已重新构建或重新安装,但预览仍表现得像旧版本。Vite 会将 SDK 预打包到 node_modules/.vite,并以包版本作为该缓存的键,因此原地重新构建不会使其失效。停止开发服务器,删除 node_modules/.vite 目录,然后再次启动它(或运行 npx vite --force)。
每次编辑都会重置组件状态。属预期行为:开发环境会重新构建打包并进行软重载 — 在沙箱中重新求值并重新挂载应用(认证/SDK 保持加载,不刷新浏览器)。由于应用是在隔离领域中求值的打包,不存在模块级 HMR / Fast Refresh,因此局部组件状态会被重置。
在生产环境中 URL 改变但 UI 不更新(在开发环境中正常)。从 @metabase/embedding-sdk-react/data-app(而非主入口)导入路由原语,并继续使用 dataAppConfig() — 它会将 /data-app 外部化,从而其路由不会被内联(内联会触发 React 状态批处理穿过 Near-Membrane 的 bug)。

评论

支持 Markdown