metabase-data-app-semantic-layer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMetabase Data App Semantic Layer
Metabase 数据应用语义层
Core Rules
核心规则
Keep the semantic layer and presentation layer separate.
- All Metabase context must come from the generated schema file, usually or
src/metabase.data.ts.src/*.metabase.data.ts - Do not discover data through MCP tools, create Metabase content, create tables, or edit the semantic layer while building the React UI.
- Import data app query helpers from .
@metabase/embedding-sdk-react/data-app - Every query is a named export in the root-level
defineQuery(...)directory, and every action aqueries/named export in the root-leveldefineAction(...)directory, both besideactions/. Create both directories before writing the first hook call; the template ships them, each with a README. The hooks enforce this at compile time:package.json,useMetabaseQuery, anduseMetabaseQueryObjectreject an inline object, auseActionobject, and a spread copy of a definition. The error readssatisfies MetabaseQueryOptions(orProperty 'definedWithDefineQuery' is missing); the fix is always to move the object into'definedWithDefineAction'orqueries/as a definition and import it, never a cast.actions/ - Never remove, edit, or copy a generated or
savedQuestionSourceId, even if it appears unused. Preserve it during refactors; usecopiedActionIdto repair or replace generated IDs.npm run sync-resources - Prefer generated schema objects over raw IDs or strings. Extract local constants for top-level table objects.
- Never hand-write /MBQL objects in app code. Do not pass inline query objects like
DatasetQuery, raw{ type: "query", query: { "source-table": table.id } }clauses, raw field IDs, bare table IDs, or metric IDs to SDK components,source-table, oruseMetabaseQuery. Prefer generated table and metric schema objects; for simple table-source queries, an explicit source reference likeuseMetabaseQueryObjectis also valid.{ type: "table", id: table.id } - Build queries with , generated
source: schema.tables.<name>, generatedfields, generatedsegments, generated metrics inmeasures, generated metricaggregations,dimensions,filter(...),breakout(...), andorderBy(...)helpers such asaggregationsandaggregations.count(). Do not useaggregations.sum(...); metrics are aggregation expressions, not query sources.source: schema.metrics.<name> - Do not use existing saved questions as or
useMetabaseQuerysources. Typed schemas do not exposeuseMetabaseQueryObjector supportschema.questionswhile data-app reconciliation cannot copy existing saved questions into the app collection.question-collections - Prefer semantically rich table queries over shallow table dumps. Use curated table measures, segments, filters, and breakouts when they make the generated app more useful.
- Prefer semantic-layer definitions over React-side inference. If the schema has a segment or measure for a concept, use it instead of recreating the concept from raw rows. Both belong to the static query only — the dynamic second argument cannot take them, see Static and dynamic query parts.
- Filter UI must default to showing data. Empty controls, "All" options, and incomplete custom ranges should produce no filter instead of blocking queries or showing a blank dashboard.
- Do not hardcode categorical filter option values. A generated schema field only proves the field exists, not which values exist; query options from Metabase at runtime using the same generated schema field that the filter applies.
- Dashboard-level filters should visibly affect every compatible card, table, KPI, and trend. If a filter can only apply to one query, make that scope obvious in the UI; do not show duplicate or no-op date controls.
- Entity filters, where the stored value is an id/key and the UI shows a label, must use a single searchable combobox. Click/focus must open the option list immediately, before typing. Query options at runtime, search labels, and store the raw value. Never render entity filters as ; plain selects are only for short closed enums explicitly provided by the user.
<select> - Use from
DateRangePopoverfor custom date ranges: it wraps the app's own trigger element and opens a Metabase-styled range calendar under it. The trigger stays the app's — style it like the other filter controls — and@metabase/embedding-sdk-react/data-appfrom the same entry produces its label. It ships with the SDK and needs no dependency and no CSS import.useDateFormatter()is the same calendar inline, for when the app already has a container. Do not install a date picker library for a range — notDateRangeCalendar,react-datepicker,react-day-picker, or a UI suite's picker (flatpickr,@mui/x-date-pickers,antd, …). Do not use nativersuiteeither: its placeholder and calendar popover are browser-controlled, often show<input type="date">, and cannot be reliably themed.mm/dd/yyyy - Never build a date label with — a date-only string parses as UTC and shows the previous day west of Greenwich. Use
new Date("YYYY-MM-DD")/formatDateRangefromformatDate, which parse in local time and format in the instance's locale.useDateFormatter() - Reach for a third-party date picker only for what the SDK calendar does not cover, such as single-date or date-time selection; is the default pick. Then import its stylesheet (
react-datepicker), add small CSS overrides for the app's visual style if needed, and passreact-datepicker/dist/react-datepicker.css— neverDate | nullor another invalid date for incomplete ranges; type strict callback parameters explicitly, such asnew Date("").onChange={(date: Date | null) => ...} - Date bars must include Custom last by default: duration presets, All time, then Custom. Omit Custom only when the user explicitly asks for fixed presets only or no date range control.
- Never invent aggregation or measure objects such as or
{ name: "count" }. Use generated table measures or exported aggregation helpers.{ name: "sum", field: ... } - Only render values returned by Metabase or deterministic transforms of returned values. Do not invent KPI values, trends, labels, statuses, ratings, timestamps, rankings, insights, segments, or chart series.
- Do not custom-render ambiguous business fields such as ,
margin,rate,score,percent,health, orrisk. Do not addefficiency, multiply by 100, color-code, or render stars unless semantic-layer units explicitly support it; use an SDK table/chart, omit the field, or ask for curation.% - Visualization data must come from Metabase through or
useMetabaseQuerywithuseMetabaseQueryObject/InteractiveQuestion. Do not hardcode chart-ready arrays, sample data, demo values, or schema-shaped mock values.StaticQuestion - Render charts with /
InteractiveQuestion. When a custom visualization is allowed instead, and which of the two to use, is decided by the setup skill's Rendering a chart: Metabase first.StaticQuestion - returns
useMetabaseQueryObject(...). Pass only the{ query, error, isLoading }property asquerytocard={{ query }}orInteractiveQuestion; never pass the whole hook result asStaticQuestion.card.query - are keyed objects, not tuple arrays. Never read
useMetabaseQuery().rows/row[0], and never silence this withrow[1],as unknown as [string, number][], or another tuple cast. If TypeScript says propertyDisplayRowdoes not exist, it is catching a real bug. For typed0, use literal keys such asdata.rowsor generated field names such asrow.count. Userow[ordersTable.fields.createdAt.name]withdata.columnsor after explicitly narrowing a key; do not index typed rows with arbitraryrawRowsvalues fromstring.data.columns - Do not cast query objects to or to
Parameters<typeof useMetabaseQuery>[0]. That erases the generated table/metric validation and the definition contract. Validate table ownership at the definition withDefinedQuery; the hooks take the export with no generics.defineQuery<typeof table>(...) - Do not build shared filter arrays with or
ReturnType<typeof filter>[]; this can collapse overload inference. Pass raw filter state between components and build each query'spush(...)inline with spreads.filters: [...] - Keep runtime state out of the base query in . A clause whose value comes from a control — a selected plan, a date range, a search box — belongs in the second argument to
queries//useMetabaseQuery, not in the query. See "Static and dynamic query parts".useMetabaseQueryObject - Do not include in queries with
fieldsandaggregations; breakouts determine grouped result columns. Usebreakoutsonly for row-selection queries.fields - Before rendering a field, verify it exists in the generated schema object and is returned by the query. Do not guess table keys, field keys, or column names from the Metabase API, business intuition, or old mock data; only use entries actually emitted in .
src/metabase.data.ts - Avoid unsupported freshness or operational claims such as "real-time", "live", "understaffed", or "risk" unless the returned data or curated semantic-layer definition supports them.
- Before claiming the work is done or preparing a final handoff, run a TypeScript type-only check and report the command/result. If the check fails, fix the type errors before any final summary.
保持语义层与表现层分离。
- 所有Metabase上下文必须来自生成的schema文件,通常为或
src/metabase.data.ts。src/*.metabase.data.ts - 构建React UI时,不得通过MCP工具发现数据、创建Metabase内容、创建表或编辑语义层。
- 从导入数据应用查询辅助工具。
@metabase/embedding-sdk-react/data-app - 每个查询都是根级目录下的
queries/命名导出,每个操作都是根级defineQuery(...)目录下的actions/命名导出,这两个目录均与defineAction(...)同级。在编写第一个钩子调用前先创建这两个目录;模板已自带这两个目录,每个目录下都有README。钩子会在编译时强制执行此规则:package.json、useMetabaseQuery和useMetabaseQueryObject会拒绝内联对象、useAction类型的对象以及定义的展开拷贝。报错信息为satisfies MetabaseQueryOptions(或Property 'definedWithDefineQuery' is missing);修复方法始终是将该对象作为定义移至'definedWithDefineAction'或queries/目录中并导入,绝不要使用类型断言。actions/ - 切勿删除、编辑或复制生成的或
savedQuestionSourceId,即使它们看起来未被使用。重构时也要保留;使用copiedActionId来修复或替换生成的ID。npm run sync-resources - 优先使用生成的schema对象,而非原始ID或字符串。为顶层表对象提取局部常量。
- 切勿在应用代码中手写/MBQL对象。不得将内联查询对象(如
DatasetQuery)、原始{ type: "query", query: { "source-table": table.id } }子句、原始字段ID、裸表ID或指标ID传递给SDK组件、source-table或useMetabaseQuery。优先使用生成的表和指标schema对象;对于简单的表源查询,显式的源引用(如useMetabaseQueryObject)也是有效的。{ type: "table", id: table.id } - 使用、生成的
source: schema.tables.<name>、生成的fields、生成的segments、measures中的生成指标、生成的指标aggregations、dimensions、filter(...)、breakout(...)以及orderBy(...)辅助函数(如aggregations和aggregations.count())来构建查询。不得使用aggregations.sum(...);指标是聚合表达式,而非查询源。source: schema.metrics.<name> - 不得将现有已保存问题用作或
useMetabaseQuery的源。类型化schema不会暴露useMetabaseQueryObject,也不支持schema.questions,因为数据应用对账无法将现有已保存问题复制到应用集合中。question-collections - 优先使用语义丰富的表查询,而非浅层的表数据导出。当精心设计的表度量、分段、过滤器和分组能让生成的应用更实用时,应优先使用它们。
- 优先使用语义层定义,而非React端的推断。如果schema中已有某个概念的分段或度量,请直接使用,而不要从原始行数据重新创建该概念。两者都仅属于静态查询——动态的第二个参数无法接收它们,参见查询的静态与动态部分。
- 过滤器UI默认必须展示数据。空控件、“全部”选项以及不完整的自定义范围应等同于无过滤器,而非阻塞查询或显示空白仪表盘。
- 不得硬编码分类过滤器的选项值。生成的schema字段仅能证明字段存在,无法证明存在哪些值;请在运行时使用过滤器所应用的同一生成schema字段从Metabase查询选项。
- 仪表盘级别的过滤器应能明显影响所有兼容的卡片、表格、KPI和趋势图。如果某个过滤器仅能应用于一个查询,请在UI中明确说明其作用范围;不要显示重复或无效的日期控件。
- 实体过滤器(存储值为ID/键,UI显示标签)必须使用单个可搜索的combobox。点击/聚焦时必须立即打开选项列表,无需先输入。运行时查询选项、搜索标签,并存储原始值。切勿将实体过滤器渲染为;普通选择框仅适用于用户明确提供的短封闭枚举。
<select> - 自定义日期范围请使用中的
@metabase/embedding-sdk-react/data-app:它会包裹应用自身的触发元素,并在其下方打开Metabase样式的范围日历。触发元素仍由应用控制——可像其他过滤器控件一样设置样式——同一入口导出的DateRangePopover可生成其标签。它随SDK一起提供,无需额外依赖,也无需导入CSS。useDateFormatter()是同款内联日历,适用于应用已有容器的场景。不要为日期范围安装日期选择器库——不要用DateRangeCalendar、react-datepicker、react-day-picker,也不要用UI组件库的选择器(flatpickr、@mui/x-date-pickers、antd等)。也不要使用原生rsuite:其占位符和日历弹出层由浏览器控制,通常显示<input type="date">,且无法可靠地自定义主题。mm/dd/yyyy - 切勿用构建日期标签——仅含日期的字符串会被解析为UTC时间,格林威治以西的用户会看到前一天的日期。请使用
new Date("YYYY-MM-DD")中的useDateFormatter()/formatDateRange,它们会按本地时间解析,并按实例的区域设置格式化。formatDate - 仅当SDK日历无法满足需求时(如单日选择或日期时间选择),才使用第三方日期选择器;默认选择。然后导入其样式表(
react-datepicker),如需可添加少量CSS覆盖以适配应用的视觉风格,并传递react-datepicker/dist/react-datepicker.css类型的值——绝不要为不完整的范围传入Date | null或其他无效日期;请显式设置严格的回调参数类型,例如new Date("")。onChange={(date: Date | null) => ...} - 日期选项栏默认必须包含“自定义”且置于最后:时长预设、全部时间,然后是自定义。仅当用户明确要求仅使用固定预设或不需要日期范围控件时,才省略自定义选项。
- 切勿发明聚合或度量对象,例如或
{ name: "count" }。请使用生成的表度量或导出的聚合辅助函数。{ name: "sum", field: ... } - 仅渲染Metabase返回的值或对返回值的确定性转换结果。不得发明KPI值、趋势、标签、状态、评分、时间戳、排名、洞察、分段或图表序列。
- 不得自定义渲染含义模糊的业务字段,例如、
margin、rate、score、percent、health或risk。除非语义层单位明确支持,否则不要添加efficiency、乘以100、颜色编码或渲染星级;请使用SDK表格/图表、省略该字段,或请求进行语义层整理。% - 可视化数据必须通过或
useMetabaseQuery结合useMetabaseQueryObject/InteractiveQuestion从Metabase获取。不得硬编码可直接用于图表的数组、示例数据、演示值或符合schema结构的模拟值。StaticQuestion - 使用/
InteractiveQuestion渲染图表。何时允许使用自定义可视化,以及两者中应使用哪一个,由设置技能的渲染图表:优先使用Metabase决定。StaticQuestion - 返回
useMetabaseQueryObject(...)。仅将{ query, error, isLoading }属性作为query传递给card={{ query }}或InteractiveQuestion;绝不要将整个钩子结果作为StaticQuestion传递。card.query - 是键控对象,而非元组数组。切勿读取
useMetabaseQuery().rows/row[0],也绝不要用row[1]、as unknown as [string, number][]或其他元组类型断言来屏蔽此错误。如果TypeScript提示属性DisplayRow不存在,它正在捕获一个真实的bug。对于类型化的0,请使用字面量键(如data.rows)或生成的字段名(如row.count)。请将row[ordersTable.fields.createdAt.name]与data.columns配合使用,或在显式缩小键的范围后使用;不要用rawRows中的任意data.columns值索引类型化的行。string - 不得将查询对象断言为或
Parameters<typeof useMetabaseQuery>[0]。这会抹除生成的表/指标验证和定义契约。请在定义处使用DefinedQuery验证表所有权;钩子接收导出时无需泛型。defineQuery<typeof table>(...) - 不要使用或
ReturnType<typeof filter>[]构建共享过滤器数组;这会导致重载推断失效。请在组件之间传递原始过滤器状态,并通过展开运算符内联构建每个查询的push(...)。filters: [...] - 不要将运行时状态放入的基础查询中。值来自控件的子句(所选计划、日期范围、搜索框)应放在
queries//useMetabaseQuery的第二个参数中,而非查询定义内。参见“查询的静态与动态部分”。useMetabaseQueryObject - 带有和
aggregations的查询中不要包含breakouts;分组决定了结果列的分组方式。fields仅用于行选择查询。fields - 渲染字段前,请验证该字段存在于生成的schema对象中,且由查询返回。不要根据Metabase API、业务直觉或旧的模拟数据猜测表键、字段键或列名;仅使用中实际生成的条目。
src/metabase.data.ts - 避免做出无依据的新鲜度或运营性声明,例如“实时”、“在线”、“人员不足”或“风险”,除非返回的数据或精心整理的语义层定义支持这些说法。
- 在声称工作完成或准备最终交付前,请运行TypeScript纯类型检查并报告命令/结果。如果检查失败,请先修复类型错误,再给出最终总结。
Generate Schema
生成Schema
If the schema file already exists, use it. If it is missing or stale, treat schema generation as semantic-layer curation for this data app, not a mechanical export.
Choose the export scope before generating:
- Honor an explicit scope. Otherwise infer the required content types from the app's purpose: tables, curated metrics, or actions. For example, "show orders" needs tables; row counts and sums can also use table aggregations.
- Choose the narrowest supported scope that covers those needs, using collection IDs and database names or IDs from the request or project context. When the library type is clear but a narrower collection is unknown, use that library's whole tree.
- Ask only for context needed to select a scope, such as which database to use when the request requires a database scope but does not identify one. Once the scope is determined, state it briefly and generate without waiting for confirmation.
Scope parameters:
- for the whole
include-data-library=truetree.Library / Data - for the whole
include-metric-library=truetree.Library / metrics - for specific Data or metrics library subcollections.
library-collections=<id-or-entity-id>[,<id-or-entity-id>] - for readable models that have actions. When combined with
include-models=true, it includes models with actions for that database only.database=<name-or-id> - when the app should use tables from one database. Use it separately from library scopes; the API rejects that combination.
database=<name-or-id>
Combine library scopes when the app needs both tables and curated metrics.
Use when the app needs any saved action under ; it includes all readable models with executable actions, unless scopes them to one database. Models without executable actions are omitted to keep generated schemas compact. It can be combined with , , or so one schema can include selected tables/metrics plus all readable actions.
include-models=trueschema.models.<model>.actionsdatabaselibrary-collectionsinclude-data-libraryinclude-metric-libraryIf the user asks for any mutation-like flow, such as creating, updating, deleting, submitting, approving, executing an action, or running a write operation, include in the typed-schema URL. Do this even when the user names one specific model/action, because actions are only discoverable through generated model entries.
include-models=trueThe Metabase URL and API key live in the repo-root as
and (one file per repo, usually two levels up
from the app dir, not in the app dir). The command below s that file so
the shell substitutes the values straight into — you never read, extract,
or handle the credentials yourself.
.env.localDATA_APP_MB_URLDATA_APP_MB_API_KEYsourcecurlNever ask the user to paste the API key into the chat, and never/catecho— it's git-ignored and may hold other secrets, so its contents must stay out of the conversation..env.localit so the shell uses the values without exposing them. Ifsourceor$DATA_APP_MB_API_KEYis empty or still set to the default$DATA_APP_MB_URLplaceholder after sourcing, ask the user to add real values themselves, then continue.mb_replace_me
Source the credentials from the repo-root and generate the scoped
schema. The example below exports table data from the Data library; replace its
query parameters with the scope chosen above:
.env.localbash
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ -z "$ROOT" ]; then
echo "Not inside the connected git repo — cd into it first." >&2
exit 1
fi
(
source "$ROOT/.env.local" 2>/dev/null
# Fail early (before curl) if either var is missing or placeholder-only.
if [ -z "$DATA_APP_MB_URL" ] || [ "$DATA_APP_MB_URL" = "mb_replace_me" ] ||
[ -z "$DATA_APP_MB_API_KEY" ] || [ "$DATA_APP_MB_API_KEY" = "mb_replace_me" ]; then
echo "Set real DATA_APP_MB_URL / DATA_APP_MB_API_KEY in repo-root .env.local" >&2
exit 1
fi
curl \
-o src/metabase.data.ts \
-H "x-api-key: $DATA_APP_MB_API_KEY" \
-H "Accept: text/typescript" \
"$DATA_APP_MB_URL/api/typed-schemas/v1/typescript?include-data-library=true"
)After a successful export, verify that the schema contains every entity needed for the requested app. If any are missing, revise the scope using available context or ask for the missing context before building the UI.
If schema generation fails while building a selected model or model action, do not hide, paraphrase away, or retry past the error. Surface the typed-schema error to the user, including the failing / / , / , dropped action ids, and message when present.
card-idcard-namecard-typemodel-idmodel-name如果schema文件已存在,请直接使用。如果文件缺失或过时,请将schema生成视为该数据应用的语义层整理工作,而非机械导出。
生成前先选择导出范围:
- 遵循明确指定的范围。否则根据应用用途推断所需的内容类型:表、精心整理的指标或操作。例如,“展示订单”需要表;行计数和求和也可以使用表聚合。
- 使用请求或项目上下文中的集合ID和数据库名称或ID,选择能覆盖这些需求的最窄支持范围。当库类型明确但未知更窄的集合时,使用该库的整个树。
- 仅询问选择范围所需的上下文,例如当请求需要数据库范围但未指定数据库时,询问使用哪个数据库。确定范围后,简要说明并直接生成,无需等待确认。
范围参数:
- :包含整个
include-data-library=true树。库 / 数据 - :包含整个
include-metric-library=true树。库 / 指标 - :包含特定的数据或指标库子集合。
library-collections=<id-or-entity-id>[,<id-or-entity-id>] - :包含具有操作的可读模型。与
include-models=true结合使用时,仅包含该数据库的带操作模型。database=<name-or-id> - :当应用应使用单个数据库中的表时使用。请与库范围分开使用;API会拒绝该组合。
database=<name-or-id>
当应用同时需要表和精心整理的指标时,请组合使用库范围。
当应用需要下的任何已保存操作时,请使用;它会包含所有具有可执行操作的可读模型,除非将其范围限定为单个数据库。为保持生成的schema紧凑,不包含无可执行操作的模型。它可以与、或结合使用,这样一个schema可以包含选定的表/指标以及所有可读操作。
schema.models.<model>.actionsinclude-models=truedatabaselibrary-collectionsinclude-data-libraryinclude-metric-library如果用户要求任何类似变更的流程,例如创建、更新、删除、提交、审批、执行操作或运行写入操作,请在类型化schema的URL中加入。即使用户指定了一个特定的模型/操作也要这么做,因为操作只能通过生成的模型条目发现。
include-models=trueMetabase URL和API密钥存储在仓库根目录的中,变量名为和(每个仓库一个文件,通常在应用目录的上两级,而非应用目录内)。下面的命令会该文件,以便shell将值直接替换到中——你永远不需要读取、提取或处理凭据本身。
.env.localDATA_APP_MB_URLDATA_APP_MB_API_KEYsourcecurl切勿要求用户将API密钥粘贴到聊天中,也切勿使用/cat查看echo—— 该文件已被git忽略,且可能包含其他密钥,因此其内容绝不能出现在对话中。请.env.local该文件,让shell在不暴露值的情况下使用它们。如果执行source后,source或$DATA_APP_MB_API_KEY为空,或仍为默认的$DATA_APP_MB_URL占位符,请让用户自行添加真实值后再继续。mb_replace_me
从仓库根目录的中加载凭据,并生成限定范围的schema。以下示例从数据库导出表数据;请将其查询参数替换为上面选择的范围:
.env.localbash
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ -z "$ROOT" ]; then
echo "Not inside the connected git repo — cd into it first." >&2
exit 1
fi
(
source "$ROOT/.env.local" 2>/dev/null
# Fail early (before curl) if either var is missing or placeholder-only.
if [ -z "$DATA_APP_MB_URL" ] || [ "$DATA_APP_MB_URL" = "mb_replace_me" ] ||
[ -z "$DATA_APP_MB_API_KEY" ] || [ "$DATA_APP_MB_API_KEY" = "mb_replace_me" ]; then
echo "Set real DATA_APP_MB_URL / DATA_APP_MB_API_KEY in repo-root .env.local" >&2
exit 1
fi
curl \
-o src/metabase.data.ts \
-H "x-api-key: $DATA_APP_MB_API_KEY" \
-H "Accept: text/typescript" \
"$DATA_APP_MB_URL/api/typed-schemas/v1/typescript?include-data-library=true"
)导出成功后,请验证schema包含请求的应用所需的所有实体。如果有缺失,请使用可用上下文修改范围,或在构建UI前请求缺失的上下文。
如果在构建选定的模型或模型操作时schema生成失败,不要隐藏、淡化错误或重试跳过错误。请向用户展示类型化schema错误,包括失败的 / / 、 / 、被丢弃的操作ID(如有)以及错误消息。
card-idcard-namecard-typemodel-idmodel-nameSynchronize every query and action
同步所有查询与操作
Everything an end-to-end prototype runs is permission-bound: it runs against a copy in the app's own collection; read access to that collection lets viewers run the app's cards, but they see the data only if they already have access to the underlying tables — the app grants the collection, not the tables. Declare each one as a named export in a root-level directory beside — for , for . scans only those two directories, so a definition under , , or any other source directory is silently never synchronized. Discovery covers , , , , , , , and .
package.jsonqueries/defineQuery(...)actions/defineAction(...)npm run sync-resourcessrc/queries/src/actions/.js.jsx.ts.tsx.cjs.cts.mjs.mtsts
import { defineAction, defineQuery } from "@metabase/embedding-sdk-react/data-app";
import schema from "../src/metabase.data";
// queries/revenue.query.ts
export const RevenueQuery = defineQuery({ source: schema.tables.orders });
// actions/orders.action.ts
export const CreateOrder = defineAction({
action: schema.models.orders.actions.create,
});One run reconciles both. For a query it materializes the authored table query as a saved question and injects . For an action it copies the action's parent model into the app collection, copies the action onto that copy, and injects ; a model is copied once no matter how many of its actions the app declares, siblings reuse that copy, and it disappears with the last declaration. Never copy a model into the app collection by hand.
sync-resourcessavedQuestionSourceIdcopiedActionIdPass the definition itself to the hook and let the SDK resolve what runs — a production build runs the copy, while the dev preview runs the authored table or action, so an app works before its first synchronization:
ts
const { data } = useMetabaseQuery(RevenueQuery, {
filters: [filter(RevenueQuery.source.fields.status, "=", selectedStatus)],
});
const { execute, isExecuting, error } = useAction(CreateOrder);Never pass an inline table-source query (not even a read-only, filter-option, or helper query), a raw action id, , , or a hand-built , and never spread a definition into a new object. Each defeats the swap; the authored ids also bypass the permission boundary. TypeScript rejects most of these: the hooks accept only what / returned, so an inline object, a -typed object, a spread copy, and all fail to compile. When reports or , the argument is not a definition: move it into or and import the export. Do not silence it with a cast or by wrapping the inline object in at the call site, which compiles but leaves the query unsynchronized. Keep fixed permission-boundary filters, aggregations, and breakouts inside — synchronization bakes them into the saved question, so don't apply them again outside it, and put runtime clauses in the hook's second argument (see Static and dynamic query parts). needs no generics: the definition types 's parameters and .
savedQuestionSourceIdcopiedActionId{ source: { type: "card", id } }defineQuerydefineActionsatisfiesschema.models.<model>.actions.<action>tscProperty 'definedWithDefineQuery' is missingProperty 'definedWithDefineAction' is missingqueries/actions/defineQuery(...)defineQueryuseActionexecuteresultWire with and , then run after adding, changing, renaming, or removing any definition; run directly only to inspect generated state before a build. It reads and from the repo-root .
package.json"sync-resources": "embedding-sdk-react data-apps sync-resources""build": "npm run sync-resources && vite build"npm run buildsync-resourcesDATA_APP_MB_URLDATA_APP_MB_API_KEY.env.localInline generated IDs and are generated state: never delete or hand-edit either. A missing ID is restored automatically when the definition still identifies its resource — a query by its table and authored hash matching one unclaimed lockfile entry, an action by naming the same action — while a duplicated ID fails the run. Do not test or hand off the app until succeeds, every live definition carries a positive generated ID, and holds its matching entry. Commit every generated change. The build stops before bundling when synchronization fails.
resources_metadata.jsonnpm run buildresources_metadata.jsonIf synchronization fails, surface the exact error and stop. Fix local shape, serialization, duplicate-ID, or lockfile errors before retrying. A confirmed is recovered automatically; authentication, permission, network, server, collection-ownership, and Card-type failures must not trigger manual Card creation, deletion, ID replacement, or lockfile editing. Treat a successful run that discovers nothing as a failure when the app has queries or actions. Synchronization copies actions but never creates them, so an action the app needs must already exist in Metabase and be picked up by a regenerated schema; if the run reports that actions are not enabled for the database, stop and tell the user to enable them rather than working around it.
404端到端原型运行的所有内容都受权限约束:它针对应用自身集合中的副本运行;对该集合的读取权限让查看者可以运行应用的卡片,但他们只有在已有底层表的访问权限时才能看到数据——应用授予的是集合权限,而非表权限。请在旁的根级目录中将每个项声明为命名导出——用于,用于。仅扫描这两个目录,因此、或任何其他源目录下的定义会被静默忽略,永远不会同步。发现范围覆盖、、、、、、和文件。
package.jsonqueries/defineQuery(...)actions/defineAction(...)npm run sync-resourcessrc/queries/src/actions/.js.jsx.ts.tsx.cjs.cts.mjs.mtsts
import { defineAction, defineQuery } from "@metabase/embedding-sdk-react/data-app";
import schema from "../src/metabase.data";
// queries/revenue.query.ts
export const RevenueQuery = defineQuery({ source: schema.tables.orders });
// actions/orders.action.ts
export const CreateOrder = defineAction({
action: schema.models.orders.actions.create,
});一次运行会同时对账两者。对于查询,它会将编写的表查询具体化为已保存问题,并注入。对于操作,它会将操作的父模型复制到应用集合中,将操作复制到该副本上,并注入;无论应用声明了一个模型的多少个操作,该模型只会被复制一次,同级操作会复用该副本,当最后一个声明被移除时副本也会随之删除。切勿手动将模型复制到应用集合中。
sync-resourcessavedQuestionSourceIdcopiedActionId将定义本身传递给钩子,让SDK解析运行的内容——生产构建运行副本,而开发预览运行编写的表或操作,因此应用在首次同步前即可工作:
ts
const { data } = useMetabaseQuery(RevenueQuery, {
filters: [filter(RevenueQuery.source.fields.status, "=", selectedStatus)],
});
const { execute, isExecuting, error } = useAction(CreateOrder);切勿传递内联表源查询(即使是只读、过滤器选项或辅助查询也不行)、原始操作ID、、,或手动构建的,也绝不要将定义展开到新对象中。每一种做法都会破坏切换机制;编写的ID还会绕过权限边界。TypeScript会拒绝其中大多数情况:钩子仅接收/返回的内容,因此内联对象、类型的对象、展开拷贝以及都会编译失败。当报告或时,说明参数不是定义:将其移至或并导入导出项。不要用类型断言或在调用处用包裹内联对象来屏蔽错误,这样虽然能编译,但查询不会被同步。将固定的权限边界过滤器、聚合和分组放在内部——同步会将它们固化到已保存问题中,因此不要在外部再次应用它们,并将运行时子句放在钩子的第二个参数中(参见查询的静态与动态部分)。不需要泛型:定义会为的参数和提供类型。
savedQuestionSourceIdcopiedActionId{ source: { type: "card", id } }defineQuerydefineActionsatisfiesschema.models.<model>.actions.<action>tscProperty 'definedWithDefineQuery' is missingProperty 'definedWithDefineAction' is missingqueries/actions/defineQuery(...)defineQueryuseActionexecuteresult在中配置和,然后在添加、更改、重命名或删除任何定义后运行;仅在构建前检查生成状态时才直接运行。它会从仓库根目录的中读取和。
package.json"sync-resources": "embedding-sdk-react data-apps sync-resources""build": "npm run sync-resources && vite build"npm run buildsync-resources.env.localDATA_APP_MB_URLDATA_APP_MB_API_KEY内联的生成ID和是生成的状态:切勿删除或手动编辑其中任何一个。如果定义仍能识别其资源——查询通过其表和编写的哈希匹配一个未认领的锁文件条目,操作通过命名相同的操作——缺失的ID会自动恢复,而重复的ID会导致运行失败。在成功、每个生效的定义都带有有效的生成ID、且包含其匹配条目之前,不要测试或交付应用。提交所有生成的更改。同步失败时,构建会在打包前停止。
resources_metadata.jsonnpm run buildresources_metadata.json如果同步失败,请展示确切的错误并停止。重试前请修复本地结构、序列化、重复ID或锁文件错误。确认的会自动恢复;身份验证、权限、网络、服务器、集合所有权或Card类型的失败绝不能触发手动Card创建、删除、ID替换或锁文件编辑。当应用有查询或操作时,如果一次成功的运行未发现任何内容,应视为失败。同步会复制操作但绝不会创建操作,因此应用需要的操作必须已存在于Metabase中,并被重新生成的schema拾取;如果运行报告数据库未启用操作,请停止并告知用户启用它们,而不要绕过该限制。
404Standard pattern
标准模式
Two files per query: the definition in , the hook call in the component.
queries/ts
// queries/orders.query.ts
import {
aggregations,
breakout,
defineQuery,
filter,
orderBy,
} from "@metabase/embedding-sdk-react/data-app";
import schema from "../src/metabase.data";
const ordersTable = schema.tables.orders;
export const PaidRevenueByMonth = defineQuery({
source: ordersTable,
filters: [
ordersTable.segments.completed,
filter(ordersTable.fields.status, "=", "paid"),
],
aggregations: [aggregations.sum(ordersTable.fields.amount)],
breakouts: [breakout(ordersTable.fields.createdAt, { unit: "month" })],
orderBys: [orderBy(ordersTable.fields.createdAt, "desc", { unit: "month" })],
limit: 100,
});tsx
// src/pages/Overview.tsx
import { useMetabaseQuery } from "@metabase/embedding-sdk-react/data-app";
import { PaidRevenueByMonth } from "../../queries/orders.query";
const { data, isLoading, error } = useMetabaseQuery(PaidRevenueByMonth);useMetabaseQuery(...)defineQuery<typeof ordersTable>({ ... })data.rowsdefineQueryqueries/Call each schema entry at most once per render tree. Multiple calls on the same (or same + identical filters/measures/breakouts) mount independent subscriptions, fire duplicate queries, and let consumers disagree mid-load. Lift the call to the highest component that needs the data; pass / / down as props. Different ids — or the same id with different filters / breakouts — are different data sources; call them separately.
useMetabaseQueryquestionIdtableIddataisLoadingerrorUse keyed schema objects:
- Tables:
source: schema.tables.<table> - metrics: inside
schema.metrics.<metric>aggregations - Fields:
schema.tables.<table>.fields.<field> - Segments:
schema.tables.<table>.segments.<segment> - Measures:
schema.tables.<table>.measures.<measure> - metric dimensions:
schema.metrics.<metric>.dimensions.<group>.<dimension>
Do not pass raw dimension strings like or .
"created_at""segment"每个查询对应两个文件:中的定义,以及组件中的钩子调用。
queries/ts
// queries/orders.query.ts
import {
aggregations,
breakout,
defineQuery,
filter,
orderBy,
} from "@metabase/embedding-sdk-react/data-app";
import schema from "../src/metabase.data";
const ordersTable = schema.tables.orders;
export const PaidRevenueByMonth = defineQuery({
source: ordersTable,
filters: [
ordersTable.segments.completed,
filter(ordersTable.fields.status, "=", "paid"),
],
aggregations: [aggregations.sum(ordersTable.fields.amount)],
breakouts: [breakout(ordersTable.fields.createdAt, { unit: "month" })],
orderBys: [orderBy(ordersTable.fields.createdAt, "desc", { unit: "month" })],
limit: 100,
});tsx
// src/pages/Overview.tsx
import { useMetabaseQuery } from "@metabase/embedding-sdk-react/data-app";
import { PaidRevenueByMonth } from "../../queries/orders.query";
const { data, isLoading, error } = useMetabaseQuery(PaidRevenueByMonth);useMetabaseQuery(...)defineQuery<typeof ordersTable>({ ... })data.rowsdefineQueryqueries/每个schema条目在渲染树中最多调用一次。 对同一个(或同一个 + 相同的过滤器/度量/分组)进行多次调用会挂载独立的订阅,触发重复查询,并导致消费者在加载过程中出现不一致。将调用提升到需要数据的最高层组件;将 / / 作为props向下传递。不同的ID——或同一ID但带有不同的过滤器/分组——是不同的数据源;请分别调用它们。
questionIdtableIduseMetabaseQuerydataisLoadingerror使用键控的schema对象:
- 表:
source: schema.tables.<table> - 指标:中的
aggregationsschema.metrics.<metric> - 字段:
schema.tables.<table>.fields.<field> - 分段:
schema.tables.<table>.segments.<segment> - 度量:
schema.tables.<table>.measures.<measure> - 指标维度:
schema.metrics.<metric>.dimensions.<group>.<dimension>
不要传递原始维度字符串,例如或。
"created_at""segment"Static and dynamic query parts
查询的静态与动态部分
Both query hooks take an optional second argument: the clauses that change while the app runs.
ts
// revenue.query.ts — static, and identical on every render
const orders = schema.tables.orders;
export const RevenueQuery = defineQuery({
source: orders,
aggregations: [aggregations.sum(orders.fields.total)],
breakouts: [
breakout(orders.fields.createdAt, { unit: "month" }),
breakout(orders.fields.plan),
],
});
// the component supplies only what the UI changes
const { data } = useMetabaseQuery(RevenueQuery, {
filters: plan === null ? [] : [filter(orders.fields.plan, "=", plan)],
});Split them this way even when nothing appears to depend on it: the first argument must be identical on every render, and only the second may vary with runtime state.
The dynamic clauses run as their own stage, so they see the result columns of the static query, not its source table. That is why is a breakout above: a control that filters on a source column only works if that column survives into the result. If it does not, add it as a breakout, or leave the static query unaggregated. Likewise, filter an aggregated static query on /, not on the fields behind them.
plancountsumSegments and measures belong to the static part only. They are defined against a table, and the dynamic stage has no table — so and are rejected, at compile time and again at runtime. This is the one place the usual "prefer the curated definition" rule does not apply.
filters: [orders.segments.completed]aggregations: [orders.measures.revenue]Put the curated definition in the static query where it resolves, and let the dynamic clause work on what came out:
ts
export const CompletedOrders = defineQuery({
source: orders,
filters: [orders.segments.completed], // the segment resolves here
aggregations: [orders.measures.revenue],
breakouts: [breakout(orders.fields.plan)],
});
const { data } = useMetabaseQuery(CompletedOrders, {
// a result column, not a segment or measure
filters: plan === null ? [] : [filter({ type: "column", name: "PLAN" }, "=", plan)],
});If a control must switch a segment on and off, that is a choice between static queries, not a dynamic clause: define one query per state and pick the query, or express the same condition as a filter on a result column.
Do not remove or hand-edit if you find it on a query object, or on an action definition. Both are generated synchronization state — see Synchronize every query and action.
savedQuestionSourceIdcopiedActionId两个查询钩子都接受可选的第二个参数:应用运行时会变化的子句。
ts
// revenue.query.ts — 静态的,每次渲染都完全相同
const orders = schema.tables.orders;
export const RevenueQuery = defineQuery({
source: orders,
aggregations: [aggregations.sum(orders.fields.total)],
breakouts: [
breakout(orders.fields.createdAt, { unit: "month" }),
breakout(orders.fields.plan),
],
});
// 组件仅提供UI会更改的部分
const { data } = useMetabaseQuery(RevenueQuery, {
filters: plan === null ? [] : [filter(orders.fields.plan, "=", plan)],
});即使看起来没有依赖,也要按这种方式拆分:第一个参数必须在每次渲染时都完全相同,只有第二个参数可以随运行时状态变化。
动态子句作为独立的阶段运行,因此它们看到的是静态查询的结果列,而非其源表。这就是为什么上面的是一个分组:对源列进行过滤的控件只有在该列保留到结果中时才能工作。如果没有,请将其添加为分组,或让静态查询不进行聚合。同样,对聚合后的静态查询进行过滤时,应基于/,而非它们背后的字段。
plancountsum分段和度量仅属于静态部分。 它们是针对表定义的,而动态阶段没有表——因此和会在编译时和运行时被拒绝。这是通常的“优先使用精心设计的定义”规则不适用的唯一情况。
filters: [orders.segments.completed]aggregations: [orders.measures.revenue]将精心设计的定义放在静态查询中使其生效,让动态子句作用于输出结果:
ts
export const CompletedOrders = defineQuery({
source: orders,
filters: [orders.segments.completed], // 分段在此处生效
aggregations: [orders.measures.revenue],
breakouts: [breakout(orders.fields.plan)],
});
const { data } = useMetabaseQuery(CompletedOrders, {
// 结果列,而非分段或度量
filters: plan === null ? [] : [filter({ type: "column", name: "PLAN" }, "=", plan)],
});如果控件必须切换某个分段的开关,这是静态查询之间的选择,而非动态子句:为每个状态定义一个查询并选择查询,或将相同条件表达为对结果列的过滤。
如果在查询对象上发现,或在操作定义上发现,请勿删除或手动编辑。两者都是生成的同步状态——参见同步所有查询与操作。
savedQuestionSourceIdcopiedActionIdTable query recipes
表查询示例
For a table query, pass the generated table object as :
sourcets
// queries/records.query.ts
const recordsTable = schema.tables.records;
export const RecordStatuses = defineQuery({
source: recordsTable,
fields: [recordsTable.fields.id, recordsTable.fields.status],
});For grouped table summaries, include at least one aggregation:
ts
export const ActiveAmountByMonth = defineQuery({
source: recordsTable,
filters: [
recordsTable.segments.activeRecords,
filter(recordsTable.fields.amount, ">", 100),
],
aggregations: [recordsTable.measures.totalAmount],
breakouts: [breakout(recordsTable.fields.createdAt, { unit: "month" })],
orderBys: [orderBy(recordsTable.fields.createdAt, "desc", { unit: "month" })],
});For basic aggregations without a curated measure, use the helpers:
aggregationsts
export const AmountByCategory = defineQuery({
source: recordsTable,
aggregations: [
aggregations.count(),
aggregations.sum(recordsTable.fields.amount),
],
breakouts: [breakout(recordsTable.fields.category)],
});When using the same helper more than once, Metabase may return numbered runtime keys such as , , and . TypeScript only models the base helper key today. For custom KPI code that intentionally uses repeated same-kind aggregations, read through or cast the row to before accessing numbered keys. Prefer curated measures or separate queries when that is clearer.
sumsum_2sum_3data.columnsRecord<string, unknown>Table fields, segments, measures, filters, breakouts, and orderBys must come from the queried table. Use when you want TypeScript to validate that ownership at the definition.
defineQuery<RecordsTable>({ ... })对于表查询,将生成的表对象作为传递:
sourcets
// queries/records.query.ts
const recordsTable = schema.tables.records;
export const RecordStatuses = defineQuery({
source: recordsTable,
fields: [recordsTable.fields.id, recordsTable.fields.status],
});对于分组的表汇总,至少包含一个聚合:
ts
export const ActiveAmountByMonth = defineQuery({
source: recordsTable,
filters: [
recordsTable.segments.activeRecords,
filter(recordsTable.fields.amount, ">", 100),
],
aggregations: [recordsTable.measures.totalAmount],
breakouts: [breakout(recordsTable.fields.createdAt, { unit: "month" })],
orderBys: [orderBy(recordsTable.fields.createdAt, "desc", { unit: "month" })],
});对于没有精心设计度量的基本聚合,请使用辅助函数:
aggregationsts
export const AmountByCategory = defineQuery({
source: recordsTable,
aggregations: [
aggregations.count(),
aggregations.sum(recordsTable.fields.amount),
],
breakouts: [breakout(recordsTable.fields.category)],
});当多次使用同一个辅助函数时,Metabase可能会返回带编号的运行时键,例如、和。目前TypeScript仅建模基础辅助函数键。对于有意使用重复同类聚合的自定义KPI代码,请通过读取,或将行断言为后再访问带编号的键。如果更清晰的话,优先使用精心设计的度量或单独的查询。
sumsum_2sum_3data.columnsRecord<string, unknown>表字段、分段、度量、过滤器、分组和排序必须来自被查询的表。当你希望TypeScript在定义处验证所有权时,请使用。
defineQuery<RecordsTable>({ ... })metric aggregation recipes
指标聚合示例
For a metric-backed query, pass the generated table object as and the generated metric object in :
sourceaggregationsts
// queries/revenue.query.ts
const ordersTable = schema.tables.orders;
const revenueMetric = schema.metrics.revenue;
export const Revenue = defineQuery({
source: ordersTable,
aggregations: [revenueMetric],
});Use generated metric dimensions for filters and breakouts in queries that aggregate the owning metric. Dimensions from the metric's source table work directly. Dimensions from related tables also work when the generated field includes ; prefer those related-table dimensions for readable labels instead of grouping by raw foreign key IDs:
sourceFieldIdts
export const PaidRevenueByMonthAndFranchise = defineQuery({
source: ordersTable,
aggregations: [revenueMetric],
filters: [filter(revenueMetric.dimensions.orders.status, "=", "paid")],
breakouts: [
breakout(revenueMetric.dimensions.orders.createdAt, { unit: "month" }),
breakout(revenueMetric.dimensions.franchises.name),
],
orderBys: [
orderBy(revenueMetric.dimensions.orders.createdAt, "desc", {
unit: "month",
}),
],
});
// Prefer readable related-table dimensions when available.
breakout(revenueMetric.dimensions.franchises.name);
// Avoid raw FK IDs when the related-table dimension exists.
breakout(revenueMetric.dimensions.orders.franchiseId);Queries backed by metrics can include helper aggregations over generated metric dimensions. They can also use compatible saved Segments and Measures from the table source when the generated schema exposes them:
ts
export const CompletedRevenueByStatus = defineQuery({
source: ordersTable,
filters: [schema.tables.orders.segments.completed],
aggregations: [
revenueMetric,
schema.tables.orders.measures.totalRevenue,
aggregations.sum(revenueMetric.dimensions.orders.amount),
],
breakouts: [breakout(revenueMetric.dimensions.orders.status)],
});A metric aggregation must belong to the table source. Do not use source-card metrics in table-source queries. Generated metric dimensions are scoped to their owning metric: if a query uses in filters, helper aggregations, breakouts, or orderBys, it must also include in . Do not use metric dimensions as standalone table fields for unrelated or table-measure queries. Generated metric dimensions must also resolve to the table source. Use when you want TypeScript to validate that at the definition.
revenueMetric.dimensions.*revenueMetricaggregationscount()defineQuery<typeof ordersTable>({ ... })对于指标支持的查询,将生成的表对象作为传递,并将生成的指标对象放在中:
sourceaggregationsts
// queries/revenue.query.ts
const ordersTable = schema.tables.orders;
const revenueMetric = schema.metrics.revenue;
export const Revenue = defineQuery({
source: ordersTable,
aggregations: [revenueMetric],
});在聚合所属指标的查询中,使用生成的指标维度进行过滤和分组。指标源表的维度可直接使用。当生成的字段包含时,相关表的维度也可使用;优先使用这些相关表维度以获得可读的标签,而非按原始外键ID分组:
sourceFieldIdts
export const PaidRevenueByMonthAndFranchise = defineQuery({
source: ordersTable,
aggregations: [revenueMetric],
filters: [filter(revenueMetric.dimensions.orders.status, "=", "paid")],
breakouts: [
breakout(revenueMetric.dimensions.orders.createdAt, { unit: "month" }),
breakout(revenueMetric.dimensions.franchises.name),
],
orderBys: [
orderBy(revenueMetric.dimensions.orders.createdAt, "desc", {
unit: "month",
}),
],
});
// 优先使用可读的相关表维度(如果有)。
breakout(revenueMetric.dimensions.franchises.name);
// 当存在相关表维度时,避免使用原始FK ID。
breakout(revenueMetric.dimensions.orders.franchiseId);指标支持的查询可以包含基于生成的指标维度的辅助聚合。当生成的schema暴露了表源中的兼容已保存分段和度量时,也可以使用它们:
ts
export const CompletedRevenueByStatus = defineQuery({
source: ordersTable,
filters: [schema.tables.orders.segments.completed],
aggregations: [
revenueMetric,
schema.tables.orders.measures.totalRevenue,
aggregations.sum(revenueMetric.dimensions.orders.amount),
],
breakouts: [breakout(revenueMetric.dimensions.orders.status)],
});指标聚合必须属于表源。不得在表源查询中使用源卡片指标。生成的指标维度的作用域限定为其所属指标:如果查询在过滤器、辅助聚合、分组或排序中使用,则它还必须在中包含。不得将指标维度用作独立表字段,用于不相关的或表度量查询。生成的指标维度还必须解析到表源。当你希望TypeScript在定义处验证这一点时,请使用。
revenueMetric.dimensions.*aggregationsrevenueMetriccount()defineQuery<typeof ordersTable>({ ... })SDK-rendered views
SDK渲染视图
Table fields, segments, measure aggregations, and metric aggregations must come from the queried table. Generated metric dimensions used in filters, helper aggregations, breakouts, and orderBys must resolve to the queried table and belong to a metric included in the same query's .
When table queries use , , , , or , let infer the shape, or write when ownership validation matters more than precise result-row keys.
aggregationsfieldssegmentsaggregationsbreakoutsorderBysdefineQuerydefineQuery<typeof recordsTable>表字段、分段、度量聚合和指标聚合必须来自被查询的表。在过滤器、辅助聚合、分组和排序中使用的生成指标维度必须解析到被查询的表,且属于同一查询中包含的某个指标。
当表查询使用、、、或时,让推断结构,或者当所有权验证比精确的结果行键更重要时,编写。
aggregationsfieldssegmentsaggregationsbreakoutsorderBysdefineQuerydefineQuery<typeof recordsTable>Interactive Metabase Views
交互式Metabase视图
Whether an element is an SDK question at all — and whether it is or — is decided in the data-app setup skill (Rendering a chart: Metabase first). Once it is: declare the query in , resolve it with , then pass the result through the SDK question component's prop.
StaticQuestionInteractiveQuestionqueries/useMetabaseQueryObject(TheQuery)carduseMetabaseQueryObjectuseMetabaseQueryuseMetabaseQueryObjectuseMetabaseQueryObject{ query, error, isLoading }The examples under Rendering With SDK Components use for minimal loading and error handling. In a real app, render the app's existing loading or error UI there. Passing is safe while is ; do not pass the full hook result as .
return nullcard={{ query }}querynull{ query, error, isLoading }card.queryWhen wrapping in a reusable chart/card component, destructure and render ; do not read only , because query-construction failures otherwise look like endless loading. Calling the hook inside that child component is valid React. Do not call hooks directly inside loops, conditions, or callbacks in the parent component.
useMetabaseQueryObjecterror{ query }Wrong/right pattern:
tsx
const trendQuery = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // wrong
const { query: trendQuery } = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // rightHook typing:
- Both hooks take a export imported from
defineQueryand nothing else; an inline object is a compile error. Write no generics on the hooks.queries/ - infers typed row data from the definition. Put
useMetabaseQuery(...)on the definition when ownership validation matters.defineQuery<typeof table> - returns
useMetabaseQueryObject(...). Pass the{ query, error, isLoading }property toquery.card={{ query }} - Do not use or
as Parameters<typeof useMetabaseQuery>[0]to quiet query typing errors. The first hides invalid table fields, metric aggregations, and breakouts; the second hides an unsynchronized query that fails in production.as DefinedQuery
The basic prop contract is:
- Generated table query, including metric aggregations:
<StaticQuestion card={{ query }} /> - Full interactive question:
<InteractiveQuestion card={{ query }} />
When you need the set of SDK-supported question displays, do not copy a local list. In generated apps, search for the exact declaration and use that tuple as the source of truth. Do not read the whole declaration file into context.
node_modules/@metabase/embedding-sdk-react/dist/index.d.tsdeclare const cardDisplayTypes: readonly [...]Always pass SDK-rendered ad hoc questions with a object. Start with when the user has not asked for a specific chart type and Metabase defaults can infer a reasonable display from the query. Use when the user request or design calls for a specific chart type, such as a pie chart for a distribution, but does not ask for setting-level customization. Add only when the user explicitly asks for a setting-level presentation change, such as hiding or renaming an axis label, showing value labels, stacking bars, adding a goal line, ordering table columns, showing pie totals/labels, or controlling series/slice order. Search for , the relevant type, and any setting key you plan to use. Read the JSDoc comments attached to those declarations, then use the TypeScript declarations as the source of truth for legal and combinations. Build the query with ; do not call internal query resolution helpers, cast through , or hardcode settings from memory.
cardcard={{ query }}card={{ query, visualization }}visualizationSettingsnode_modules/@metabase/embedding-sdk-react/dist/index.d.tsexport declare type MetabaseCard*VisualizationSettingsvisualizationvisualizationSettingsuseMetabaseQueryObjectanyFor lightweight descriptions of the exposed settings and when to use them, read references/visualization-settings.md. Treat that file as guidance only; the installed SDK declaration decides what is legal.
Before writing a , check for the return type. Destructure the returned and use that value in ; for configured cards, type the object with . If TypeScript reports duplicate opaque symbols, do not force a cast; update the SDK package before using .
cardnode_modules/@metabase/embedding-sdk-react/dist/data-app.d.tsuseMetabaseQueryObjectquerycard.querysatisfies MetabaseCardDatasetQuerycardDo not invent alternate prop names for generated queries or visualization settings. If the SDK type says a prop does not exist, believe it and use the documented prop shape.
cardWhen is needed, map typed rows into an explicit local view model using named properties before rendering:
useMetabaseQueryts
const orderedAtKey = ordersTable.fields.orderedAt.name;
const chartRows = (data?.rows ?? []).map((row) => ({
label: String(row[orderedAtKey] ?? "Unknown"),
value: row.count,
}));If the result key only comes from at runtime, use with the matching column position, or narrow the key to a literal before indexing .
data.columnsrawRowsdata.rows一个元素是否为SDK问题——以及它是还是——由数据应用设置技能(渲染图表:优先使用Metabase)决定。确定后:在中声明查询,用解析它,然后通过SDK问题组件的属性传递结果。
StaticQuestionInteractiveQuestionqueries/useMetabaseQueryObject(TheQuery)carduseMetabaseQueryObjectuseMetabaseQueryuseMetabaseQueryObjectuseMetabaseQueryObject{ query, error, isLoading }使用SDK组件渲染下的示例使用进行最小化的加载和错误处理。在实际应用中,请在此处渲染应用现有的加载或错误UI。当为时传递是安全的;不要将完整的钩子结果作为传递。
return nullquerynullcard={{ query }}{ query, error, isLoading }card.query当将包装在可复用的图表/卡片组件中时,请解构并渲染;不要只读取,否则查询构建失败看起来会像无限加载。在子组件内部调用钩子是有效的React做法。不要在父组件的循环、条件或回调中直接调用钩子。
useMetabaseQueryObjecterror{ query }错误/正确模式:
tsx
const trendQuery = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // 错误
const { query: trendQuery } = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // 正确钩子类型:
- 两个钩子都接收从导入的
queries/导出项,不接受其他内容;内联对象会导致编译错误。不要在钩子上写泛型。defineQuery - 会从定义推断类型化的行数据。当所有权验证很重要时,请在定义上使用
useMetabaseQuery(...)。defineQuery<typeof table> - 返回
useMetabaseQueryObject(...)。将{ query, error, isLoading }属性传递给query。card={{ query }} - 不要使用或
as Parameters<typeof useMetabaseQuery>[0]来消除查询类型错误。前者会隐藏无效的表字段、指标聚合和分组;后者会隐藏未同步的查询,这类查询在生产环境中会失败。as DefinedQuery
基本的属性约定是:
- 生成的表查询,包括指标聚合:
<StaticQuestion card={{ query }} /> - 完整的交互式问题:
<InteractiveQuestion card={{ query }} />
当你需要SDK支持的问题显示类型集合时,不要复制本地列表。在生成的应用中,搜索中的精确声明,并将该元组作为事实来源。不要将整个声明文件读入上下文。
node_modules/@metabase/embedding-sdk-react/dist/index.d.tsdeclare const cardDisplayTypes: readonly [...]始终使用对象传递SDK渲染的即席问题。当用户未要求特定图表类型,且Metabase默认值可从查询推断出合理的显示方式时,从开始。当用户请求或设计要求特定图表类型(如用于分布的饼图)但不需要设置级别的自定义时,使用。仅当用户明确要求设置级别的呈现更改时才添加,例如隐藏或重命名轴标签、显示值标签、堆叠条形图、添加目标线、排序列表列、显示饼图总计/标签或控制序列/切片顺序。搜索中的、相关的类型以及你计划使用的任何设置键。阅读这些声明附带的JSDoc注释,然后将TypeScript声明作为合法和组合的事实来源。使用构建查询;不要调用内部查询解析辅助函数、通过断言或凭记忆硬编码设置。
cardcard={{ query }}card={{ query, visualization }}visualizationSettingsnode_modules/@metabase/embedding-sdk-react/dist/index.d.tsexport declare type MetabaseCard*VisualizationSettingsvisualizationvisualizationSettingsuseMetabaseQueryObjectany有关暴露设置的轻量级描述及其使用场景,请阅读references/visualization-settings.md。该文件仅作为指导;已安装的SDK声明决定了合法的内容。
编写之前,请检查中的返回类型。解构返回的并在中使用该值;对于已配置的卡片,请使用为对象添加类型。如果TypeScript报告重复的不透明符号,不要强制断言;使用前请更新SDK包。
cardnode_modules/@metabase/embedding-sdk-react/dist/data-app.d.tsuseMetabaseQueryObjectquerycard.querysatisfies MetabaseCardDatasetQuerycard不要为生成的查询或可视化设置发明替代属性名。如果SDK类型表示某个属性不存在,请相信它,并使用已记录的属性结构。
card当需要使用时,在渲染前将类型化的行映射到具有命名属性的显式本地视图模型:
useMetabaseQueryts
const orderedAtKey = ordersTable.fields.orderedAt.name;
const chartRows = (data?.rows ?? []).map((row) => ({
label: String(row[orderedAtKey] ?? "Unknown"),
value: row.count,
}));如果结果键仅在运行时来自,请将与匹配的列位置配合使用,或在索引前将键缩小为字面量。
data.columnsrawRowsdata.rowsRendering With SDK Components
使用SDK组件渲染
Chart only, without the toolbar:
tsx
import {
InteractiveQuestion,
StaticQuestion,
type MetabaseCard,
} from "@metabase/embedding-sdk-react";
import { useMetabaseQueryObject } from "@metabase/embedding-sdk-react/data-app";
import { AmountByMonth } from "../../queries/events.query";
const { query, isLoading, error } = useMetabaseQueryObject(AmountByMonth);
if (error) {
return null;
}
if (isLoading || !query) {
return null;
}
return (
<InteractiveQuestion card={{ query }}>
<InteractiveQuestion.QuestionVisualization height="500px" />
</InteractiveQuestion>
);Configured SDK visualization:
tsx
// queries/events.query.ts
export const TotalAmountByMonth = defineQuery({
source: eventsTable,
aggregations: [eventsTable.measures.totalAmount],
breakouts: [breakout(eventsTable.fields.occurredAt, { unit: "month" })],
});
// the component
const { query, isLoading, error } = useMetabaseQueryObject(TotalAmountByMonth);
if (error) {
return null;
}
if (isLoading || !query) {
return null;
}
const trendCard = {
query,
visualization: "bar",
visualizationSettings: {
"graph.show_values": true,
"graph.y_axis.title_text": "Total amount",
},
} satisfies MetabaseCard;
return (
<InteractiveQuestion card={trendCard}>
<InteractiveQuestion.QuestionVisualization height="500px" />
</InteractiveQuestion>
);仅图表,不带工具栏:
tsx
import {
InteractiveQuestion,
StaticQuestion,
type MetabaseCard,
} from "@metabase/embedding-sdk-react";
import { useMetabaseQueryObject } from "@metabase/embedding-sdk-react/data-app";
import { AmountByMonth } from "../../queries/events.query";
const { query, isLoading, error } = useMetabaseQueryObject(AmountByMonth);
if (error) {
return null;
}
if (isLoading || !query) {
return null;
}
return (
<InteractiveQuestion card={{ query }}>
<InteractiveQuestion.QuestionVisualization height="500px" />
</InteractiveQuestion>
);配置后的SDK可视化:
tsx
// queries/events.query.ts
export const TotalAmountByMonth = defineQuery({
source: eventsTable,
aggregations: [eventsTable.measures.totalAmount],
breakouts: [breakout(eventsTable.fields.occurredAt, { unit: "month" })],
});
// 组件
const { query, isLoading, error } = useMetabaseQueryObject(TotalAmountByMonth);
if (error) {
return null;
}
if (isLoading || !query) {
return null;
}
const trendCard = {
query,
visualization: "bar",
visualizationSettings: {
"graph.show_values": true,
"graph.y_axis.title_text": "Total amount",
},
} satisfies MetabaseCard;
return (
<InteractiveQuestion card={trendCard}>
<InteractiveQuestion.QuestionVisualization height="500px" />
</InteractiveQuestion>
);Filters And Breakouts
过滤器与分组
Use helpers because they give better autocomplete and shorter errors.
ts
filter(ordersTable.fields.quantity, ">", 0);
filter(ordersTable.fields.status, "contains", "paid");
filter(ordersTable.fields.quantity, "between", [10, 20]);
filter(ordersTable.fields.status, "not-empty");
breakout(ordersTable.fields.createdAt, { unit: "month" });
breakout(ordersTable.fields.amount, {
binning: { strategy: "num-bins", "num-bins": 10 },
});
breakout(ordersTable.fields.state);
orderBy(ordersTable.fields.createdAt, "desc", { unit: "month" });Do not hand-write object literals such as or ; use . When ordering the same date field used by a date breakout, pass the same to both and .
orderBys{ field, direction }{ fieldId, direction }orderBy(...)unitbreakout(...)orderBy(...)For top-N grouped summaries, order by the aggregation result, not the raw source field. Store the aggregation helper in a local constant and pass that same constant to both and :
aggregationsorderBy(...)ts
// queries/inventory.query.ts
const avgQuantity = aggregations.avg(inventoryTable.fields.quantityOnHand);
export const TopIngredientsByQuantity = defineQuery({
source: inventoryTable,
aggregations: [avgQuantity],
breakouts: [breakout(inventoryTable.fields.ingredient)],
orderBys: [orderBy(avgQuantity, "desc")],
limit: 15,
});For user-selectable sorting, the sort is runtime state, so it belongs in the hook's second argument. Build a typed map of allowed generated fields instead of indexing the whole object:
fieldsts
type SortKey = "revenue" | "orders";
type ScorecardTable = typeof scorecardTable;
type ScorecardField = ScorecardTable["fields"][keyof ScorecardTable["fields"]];
const sortFields = {
revenue: scorecardTable.fields.netRevenue,
orders: scorecardTable.fields.orders,
} satisfies Record<SortKey, ScorecardField>;
const { data } = useMetabaseQuery(Scorecard, {
orderBys: [orderBy(sortFields[sortKey], "desc")],
});ScorecarddefineQuery({ source: scorecardTable })queries/For metric queries, pass generated metric dimensions to and :
filter(...)breakout(...)ts
filter(revenueMetric.dimensions.orders.status, "=", "paid");
breakout(revenueMetric.dimensions.orders.createdAt, { unit: "month" });Filter operator rules:
- string: ,
=,!=,contains,does-not-contain,starts-with,ends-with,is-empty,not-empty,is-nullnot-null - number: ,
=,!=,>,>=,<,<=,between,is-nullnot-null - date: ,
=,!=,>,>=,<,<=,between,time-interval,is-nullnot-null - boolean: ,
=,is-nullnot-null
Only date dimensions can use . Non-date dimensions can be used as breakouts without ; numeric dimensions can use .
unitunitbinningSegments are already filters:
ts
filters: [
schema.tables.records.segments.activeRecords,
filter(schema.tables.records.fields.amount, ">", 100),
];Use curated segments first when they exactly match the product intent. Use when the UI needs a threshold, category, date range, text match, boolean condition, or other narrowing that is not already represented by a curated segment.
filter(...)请使用辅助函数,因为它们提供更好的自动补全和更简短的错误信息。
ts
filter(ordersTable.fields.quantity, ">", 0);
filter(ordersTable.fields.status, "contains", "paid");
filter(ordersTable.fields.quantity, "between", [10, 20]);
filter(ordersTable.fields.status, "not-empty");
breakout(ordersTable.fields.createdAt, { unit: "month" });
breakout(ordersTable.fields.amount, {
binning: { strategy: "num-bins", "num-bins": 10 },
});
breakout(ordersTable.fields.state);
orderBy(ordersTable.fields.createdAt, "desc", { unit: "month" });不要手写对象字面量,例如或;请使用。对日期分组使用的同一日期字段排序时,请向和传递相同的。
orderBys{ field, direction }{ fieldId, direction }orderBy(...)breakout(...)orderBy(...)unit对于前N名的分组汇总,请按聚合结果排序,而非原始源字段。将聚合辅助函数存储在局部常量中,并将同一个常量同时传递给和:
aggregationsorderBy(...)ts
// queries/inventory.query.ts
const avgQuantity = aggregations.avg(inventoryTable.fields.quantityOnHand);
export const TopIngredientsByQuantity = defineQuery({
source: inventoryTable,
aggregations: [avgQuantity],
breakouts: [breakout(inventoryTable.fields.ingredient)],
orderBys: [orderBy(avgQuantity, "desc")],
limit: 15,
});对于用户可选的排序,排序是运行时状态,因此它属于钩子的第二个参数。构建一个允许的生成字段的类型化映射,而非索引整个对象:
fieldsts
type SortKey = "revenue" | "orders";
type ScorecardTable = typeof scorecardTable;
type ScorecardField = ScorecardTable["fields"][keyof ScorecardTable["fields"]];
const sortFields = {
revenue: scorecardTable.fields.netRevenue,
orders: scorecardTable.fields.orders,
} satisfies Record<SortKey, ScorecardField>;
const { data } = useMetabaseQuery(Scorecard, {
orderBys: [orderBy(sortFields[sortKey], "desc")],
});Scorecardqueries/defineQuery({ source: scorecardTable })对于指标查询,将生成的指标维度传递给和:
filter(...)breakout(...)ts
filter(revenueMetric.dimensions.orders.status, "=", "paid");
breakout(revenueMetric.dimensions.orders.createdAt, { unit: "month" });过滤器运算符规则:
- 字符串:、
=、!=、contains、does-not-contain、starts-with、ends-with、is-empty、not-empty、is-nullnot-null - 数字:、
=、!=、>、>=、<、<=、between、is-nullnot-null - 日期:、
=、!=、>、>=、<、<=、between、time-interval、is-nullnot-null - 布尔:、
=、is-nullnot-null
仅日期维度可以使用。非日期维度可以不带用作分组;数字维度可以使用。
unitunitbinning分段本身就是过滤器:
ts
filters: [
schema.tables.records.segments.activeRecords,
filter(schema.tables.records.fields.amount, ">", 100),
];当精心设计的分段与产品意图完全匹配时,优先使用它们。当UI需要阈值、类别、日期范围、文本匹配、布尔条件或其他尚未由精心设计的分段表示的缩小条件时,请使用。
filter(...)Filter UI Patterns
过滤器UI模式
When the user asks for custom filters, build normal React controls that feed semantic query filters.
Before implementing filters, create a filter contract for the visible dashboard. At minimum, identify:
- For each filter, name the runtime query that provides its options.
- For each filter, name the raw value used in .
filter(...) - For each card, table, KPI, and trend, name the generated table field or metric dimension that can receive that filter.
- If a filter only applies to one section, keep it section-scoped or omit it from the global filter bar.
- If a page needs a different date field such as , use one visible date control for that page.
snapshotDate - KPI/detail pairs that describe the same concept should use the same relevant filters.
Use the detailed checklist in for filter state rules, runtime categorical options, stale option reset, searchable controls, and custom date-picker implementation.
references/filter-ui-patterns.mdFor the common memoized date/category filter shape:
tsx
type DatePreset = "30d" | "90d" | "custom" | "all";
const [datePreset, setDatePreset] = useState<DatePreset>("all");
const [customRange, setCustomRange] = useState<[string | null, string | null]>([
null,
null,
]);
const [status, setStatus] = useState("all");
const dateRange = useMemo((): readonly [string, string] | null => {
if (datePreset === "all") {
return null;
}
if (datePreset === "custom") {
const [start, end] = customRange;
return start && end ? [start, end] : null;
}
return getPresetDateRange(datePreset);
}, [datePreset, customRange]);
const orderFilters = useMemo(
() => [
...(dateRange
? [filter(ordersTable.fields.createdAt, "between", dateRange)]
: []),
...(status === "all"
? []
: [filter(ordersTable.fields.status, "=", status)]),
],
[dateRange, status],
);customRange<DateRangePopover value={customRange} onChange={setCustomRange}>Date | nullYYYY-MM-DDfilter(..., "between", range)date.toISOString().split("T")[0]当用户要求自定义过滤器时,构建普通的React控件来为语义查询过滤器提供输入。
实现过滤器之前,为可见的仪表盘创建一份过滤器契约。至少要明确:
- 每个过滤器,命名提供其选项的运行时查询。
- 每个过滤器,命名在中使用的原始值。
filter(...) - 每个卡片、表格、KPI和趋势图,命名可以接收该过滤器的生成表字段或指标维度。
- 如果某个过滤器仅应用于一个部分,请将其保持在部分范围内,或从全局过滤器栏中省略。
- 如果页面需要不同的日期字段(如),为该页面使用一个可见的日期控件。
snapshotDate - 描述同一概念的KPI/详情对应使用相同的相关过滤器。
有关过滤器状态规则、运行时分类选项、过期选项重置、可搜索控件和自定义日期选择器实现,请使用中的详细清单。
references/filter-ui-patterns.md对于常见的带记忆的日期/分类过滤器结构:
tsx
type DatePreset = "30d" | "90d" | "custom" | "all";
const [datePreset, setDatePreset] = useState<DatePreset>("all");
const [customRange, setCustomRange] = useState<[string | null, string | null]>([
null,
null,
]);
const [status, setStatus] = useState("all");
const dateRange = useMemo((): readonly [string, string] | null => {
if (datePreset === "all") {
return null;
}
if (datePreset === "custom") {
const [start, end] = customRange;
return start && end ? [start, end] : null;
}
return getPresetDateRange(datePreset);
}, [datePreset, customRange]);
const orderFilters = useMemo(
() => [
...(dateRange
? [filter(ordersTable.fields.createdAt, "between", dateRange)]
: []),
...(status === "all"
? []
: [filter(ordersTable.fields.status, "=", status)]),
],
[dateRange, status],
);上面的就是存储的值,因此无需转换。当备用选择器的状态使用时,请使用本地的格式化函数转换选定的日期,然后再将它们传递给。不要使用处理本地日期过滤器。
customRange<DateRangePopover value={customRange} onChange={setCustomRange}>Date | nullYYYY-MM-DDfilter(..., "between", range)date.toISOString().split("T")[0]Result Shape And Charts
结果结构与图表
- Prefer keyed .
data.rows - Never treat as positional arrays. Do not use
data.rows,row[0],row[1], or tuple casts forDisplayRowrow objects.useMetabaseQuery - Inspect before mapping low-level
data.columns, but do not use arbitraryrawRowsstrings to index typeddata.columns[].name.data.rows - Runtime row objects are keyed by returned Metabase column names, usually such as
column.nameortotal_amount. Do not assume generated schema keys likeaverage_scoreortotalAmountare runtime row keys.averageScore - For generated field references, the React property path and runtime result key can differ: might be
schema.tables.orders.fields.orderedAt.name. Useordered_at,row[ordersTable.fields.orderedAt.name], orrow.countmetadata instead of guessingdata.columns.row.orderedAt - Treat row values as nullable. Guard before calling number/string methods such as ,
toFixed, or string transforms.toLocaleString - Use only for known positional shapes.
rawRows - Aggregation columns may be named ,
count, orsum; match metadata when needed.avg - If a custom visualization needs several helper aggregations with the same output name, such as multiple calls, prefer separate single-aggregation queries so each typed row has the known
aggregations.sum(...)key. If one multi-aggregation query is necessary, readsumby column position after checkingrawRows; do not depend on generated names likedata.columnsunless they are explicitly typed or narrowed in the app code.sum_2 - Grouped queries can include a breakout bucket. Render it as
nullor filter it out deliberately."Unknown" - Time-series charts need multiple ordered buckets. Do not fake sparklines for scalar or one-point results.
- Multi-series charts with different units or magnitudes need separate axes or normalization.
- Format user-facing values: currency to at most 2 decimals, counts as whole numbers, dates as readable labels.
- Do not render ambiguous derived business values unless the semantic layer description or inspected sample values make the meaning and units obvious.
- Empty results are distinct from loading. After is false, render a clear empty state instead of leaving a skeleton or blank KPI.
isLoading
- 优先使用键控的。
data.rows - 切勿将视为位置数组。不要对
data.rows行对象使用useMetabaseQuery、row[0]、row[1]或元组断言。DisplayRow - 在映射低级之前检查
rawRows,但不要使用任意的data.columns字符串索引类型化的data.columns[].name。data.rows - 运行时行对象由返回的Metabase列名键控,通常是,例如
column.name或total_amount。不要假设生成的schema键(如average_score或totalAmount)是运行时行键。averageScore - 对于生成的字段引用,React属性路径和运行时结果键可能不同:可能是
schema.tables.orders.fields.orderedAt.name。请使用ordered_at、row[ordersTable.fields.orderedAt.name]或row.count元数据,而非猜测data.columns。row.orderedAt - 将行值视为可空的。在调用数字/字符串方法(如、
toFixed)或字符串转换前进行防护。toLocaleString - 仅对已知的位置形状使用。
rawRows - 聚合列可能命名为、
count或sum;需要时匹配元数据。avg - 如果自定义可视化需要多个具有相同输出名称的辅助聚合,例如多个调用,优先使用单独的单聚合查询,这样每个类型化的行都有已知的
aggregations.sum(...)键。如果必须使用一个多聚合查询,请在检查sum后按列位置读取data.columns;不要依赖生成的名称(如rawRows),除非它们在应用代码中被显式类型化或缩小范围。sum_2 - 分组查询可能包含分组桶。将其渲染为
null或有意过滤掉。"Unknown" - 时间序列图表需要多个有序的桶。不要为标量或单点结果伪造迷你图。
- 具有不同单位或量级的多序列图表需要单独的轴或归一化。
- 格式化面向用户的值:货币最多保留2位小数,计数为整数,日期为可读标签。
- 不要渲染含义模糊的派生业务值,除非语义层描述或检查过的示例值使其含义和单位明确无误。
- 空结果与加载不同。当为false后,渲染清晰的空状态,而非保留骨架屏或空白KPI。
isLoading
Presentation Guidance
呈现层指南
Prefer Metabase-rendered panels for chart-shaped and table-shaped data. The React app may group, sort, format, and derive display-only values from when a custom panel is justified, but do not make custom panels the default.
data.rowsGood transforms:
- Group rows for summaries.
- Sort and slice rows for ranked lists only when a custom list is clearly better than a Metabase row/bar/table visualization.
- Pick chart types from actual data shape, and prefer Metabase ,
bar,line,area,row,combo, andpivotdisplays before writing custom chart code.table - Show loading, error, and empty states.
- Bound dense result displays. Tables, alert lists, logs, and ranked lists should use a top-N slice, grouping, pagination, or a fixed/max-height scroll area so a large result set cannot stretch the entire page.
When a page feels like a raw table browser, look for schema-backed ways to enrich it:
- Use segments for curated subsets like active, completed, overdue, high-priority, or needs-attention records.
- Use measures for curated aggregations instead of recalculating everything ad hoc in React.
- Use metrics when the schema exposes a curated metric aggregation for the page's core business question.
- Use filters to focus the query on the UI's intent.
- Use breakouts to create trends, category comparisons, and grouped summaries.
- If the enriched result is still a sortable/drillable table, render it with SDK visualization components instead of rebuilding table behavior in React.
Avoid manual classification when the semantic layer already has the concept. Prefer curated segments, fields, or measures over string matching, threshold heuristics, or category reconstruction in React.
If no curated schema entry supports the intended UI, leave the section out or ask for semantic-layer curation. Do not keep mock data or placeholder analytics in the finished app.
对于图表类和表格类数据,优先使用Metabase渲染的面板。当有充分理由使用自定义面板时,React应用可以对进行分组、排序、格式化和派生仅用于显示的值,但不要将自定义面板作为默认选项。
data.rows合适的转换:
- 为汇总对行进行分组。
- 仅当自定义列表明显优于Metabase的行/条形/表格可视化时,才对行进行排序和切片以生成排名列表。
- 根据实际数据形状选择图表类型,在编写自定义图表代码之前,优先使用Metabase的、
bar、line、area、row、combo和pivot显示方式。table - 显示加载、错误和空状态。
- 限制密集结果的显示范围。表格、警报列表、日志和排名列表应使用前N切片、分组、分页或固定/最大高度滚动区域,这样大型结果集不会拉伸整个页面。
当页面感觉像原始表浏览器时,寻找基于schema的方法来丰富它:
- 使用分段来表示精心设计的子集,例如活跃、已完成、逾期、高优先级或需要关注的记录。
- 使用精心设计的聚合度量,而非在React中临时重新计算所有内容。
- 当schema为页面的核心业务问题暴露了精心设计的指标聚合时,使用指标。
- 使用过滤器将查询聚焦于UI的意图。
- 使用分组来创建趋势、类别比较和分组汇总。
- 如果丰富后的结果仍然是可排序/可下钻的表格,请使用SDK可视化组件渲染它,而非在React中重建表格行为。
当语义层已有该概念时,避免手动分类。优先使用精心设计的分段、字段或度量,而非在React中进行字符串匹配、阈值启发式或类别重建。
如果没有精心设计的schema条目支持预期的UI,请省略该部分或请求进行语义层整理。不要在完成的应用中保留模拟数据或占位分析。
Final Checks
最终检查
- Run .
npm run typecheckorProperty 'definedWithDefineQuery' is missingmeans a hook received something other than aProperty 'definedWithDefineAction' is missingorqueries/export; move the object there and import it.actions/ - Search touched files for ,
useMetabaseQuery(, anduseMetabaseQueryObject(. The first argument must be an identifier imported fromuseAction(orqueries/; aactions/, a{, adefineQuery(, or a spread there is wrong even when it compiles. The second argument of the query hooks is the dynamic object and is written inline.defineAction( - Confirm and
queries/sit besideactions/, not underpackage.json, and that every definition the app renders lives there.src/ - Run ; it synchronizes queries before producing the bundle.
npm run build - Keep TypeScript diagnostics compact in the chat or handoff. Use the full output locally to fix the app, but report grouped root causes and only a few representative diagnostics instead of pasting the entire output.
tsc - Verify every rendered value can be traced to a returned row property, schema field, measure, or deterministic transform.
- Search touched files for ,
row[0],row[1],row.orderedAt,row.orderDate,as unknown as,DisplayRow,<select,margin,rate,score,percent,%, and* 100; fix positional rows, result-key guesses, entity.toFixedfilters, and unsupported business-field interpretations.<select> - Verify every date preset bar includes Custom last unless explicitly omitted, every visible date filter affects the current page, and no page shows duplicate date filters for one scope.
- Verify points at the built bundle path and that the bundle path is tracked by git.
data_app.yaml - For every visible filter, verify "All" maps to no filter, selected values come from runtime query results, and each non-All option changes every card it claims to affect.
- 运行。
npm run typecheck或Property 'definedWithDefineQuery' is missing表示钩子接收到的不是Property 'definedWithDefineAction' is missing或queries/的导出项;将对象移至该目录并导入。actions/ - 在修改过的文件中搜索、
useMetabaseQuery(和useMetabaseQueryObject(。第一个参数必须是从useAction(或queries/导入的标识符;即使能编译,那里出现actions/、{、defineQuery(或展开运算符也是错误的。查询钩子的第二个参数是动态对象,应内联编写。defineAction( - 确认和
queries/位于actions/旁,而非package.json下,且应用渲染的每个定义都位于那里。src/ - 运行;它会在生成包之前同步查询。
npm run build - 在聊天或交付中保持TypeScript诊断信息紧凑。在本地使用完整输出来修复应用,但只报告分组后的根本原因和少数代表性诊断,而非粘贴整个输出。
tsc - 验证每个渲染的值都可以追溯到返回的行属性、schema字段、度量或确定性转换。
- 在修改过的文件中搜索、
row[0]、row[1]、row.orderedAt、row.orderDate、as unknown as、DisplayRow、<select、margin、rate、score、percent、%和* 100;修复位置行、结果键猜测、实体.toFixed过滤器以及不受支持的业务字段解释。<select> - 验证每个日期预设栏都将“自定义”置于最后(除非明确省略),每个可见的日期过滤器都影响当前页面,且没有页面为一个作用域显示重复的日期过滤器。
- 验证指向构建后的包路径,且该包路径由git跟踪。
data_app.yaml - 对于每个可见的过滤器,验证“全部”映射为无过滤器,选定的值来自运行时查询结果,且每个非“全部”选项都会改变它声称影响的所有卡片。
Common Mistakes
常见错误
- Creating or searching for Metabase content during app building.
- Writing the query object at the hook call instead of exporting it from with
queries/, or the action atdefineQueryinstead of fromuseActionwithactions/. Both are compile errors now; the fix is the directory, not a cast.defineAction - Wrapping the inline object in or
defineQuery(...)at the call site. It compiles, butdefineAction(...)never sees it, so it is refused in production.sync-resources - Putting definitions under or
src/queries/, wheresrc/actions/never looks.sync-resources - Importing older hooks instead of .
useMetabaseQuery - Copying raw numeric IDs into constants instead of using generated schema objects.
- Inventing ad hoc measure objects such as or
{ name: "count" }.{ name: "sum", field: fieldId } - Passing raw strings for table fields.
- Adding lookup helpers instead of using keyed generated schema objects.
- Inventing SDK component prop names instead of using for generated table queries.
query - Mixing fields, segments, or measures from unrelated tables.
- Passing a segment or measure to the dynamic second argument, where only result columns resolve.
- Adding a filter UI that sends empty values instead of omitting the filter.
- Hardcoding categorical filter values instead of querying the runtime values from Metabase.
- Displaying entity names but filtering by those names when a stable ID is available.
- Applying a dashboard-level filter to only one KPI while related charts and tables ignore it.
- Showing a global Date Range plus a page-specific Snapshot Date where one date filter has no effect.
- Letting a KPI and its detail table use different date or category filters without explaining the difference.
- Rendering /
Margin/Rate/Scorewith inventedHealth, stars, colors, or thresholds.% - Shipping a date preset bar with no Custom range option, or Custom before All time.
- Charting opaque IDs such as when a user-facing name is available.
franchise_id - Rendering an entity filter in a plain , even if the current runtime option list is short.
<select> - Reaching for native or any date picker dependency (
<input type="date">,react-datepicker, a UI suite's picker) for a date range instead ofreact-day-picker, and shipping browser-controlledDateRangePopoverplaceholders or unthemed calendar popovers.mm/dd/yyyy - Labelling a date trigger with instead of
new Date("YYYY-MM-DD").toLocaleDateString(), so the label is a day early for users west of Greenwich.useDateFormatter() - Assuming fully validates value types.
filter(...) - Letting a bucket become the latest time-series point.
null - Hardcoding business values, labels, timestamps, or rankings.
- Creating chart-ready arrays by hand instead of deriving them from queried .
data.rows - Casting typed SDK rows to generic tuple rows such as .
[string, number] - Reading generated object property names such as when Metabase returns column names such as
row.orderedAt.ordered_at - Rendering fields that are not present in the schema or returned query result.
- Rendering while the SDK is still authenticating or loading.
No data - Creating nested instances instead of sharing one provider at the app boundary.
MetabaseProvider
- 在应用构建过程中创建或搜索Metabase内容。
- 在钩子调用处编写查询对象,而非用从
defineQuery导出,或在queries/处编写操作,而非用useAction从defineAction导出。两者现在都是编译错误;修复方法是移动到对应目录,而非类型断言。actions/ - 在调用处用或
defineQuery(...)包裹内联对象。它能编译,但defineAction(...)永远看不到它,因此在生产环境中会被拒绝。sync-resources - 将定义放在或
src/queries/下,而src/actions/永远不会查看那里。sync-resources - 导入旧的钩子而非。
useMetabaseQuery - 将原始数字ID复制到常量中,而非使用生成的schema对象。
- 发明临时度量对象,例如或
{ name: "count" }。{ name: "sum", field: fieldId } - 为表字段传递原始字符串。
- 添加查找辅助函数,而非使用键控的生成schema对象。
- 发明SDK组件属性名,而非对生成的表查询使用。
query - 混合来自不相关表的字段、分段或度量。
- 将分段或度量传递给动态的第二个参数,而那里仅解析结果列。
- 添加发送空值的过滤器UI,而非省略过滤器。
- 硬编码分类过滤器值,而非从Metabase查询运行时值。
- 显示实体名称但按这些名称过滤,而此时有可用的稳定ID。
- 仅将仪表盘级过滤器应用于一个KPI,而相关图表和表格忽略它。
- 显示全局日期范围加上页面特定的快照日期,其中一个日期过滤器无效。
- 让KPI及其详情表格使用不同的日期或分类过滤器,而不解释差异。
- 用发明的、星级、颜色或阈值渲染
%/Margin/Rate/Score。Health - 交付没有自定义范围选项的日期预设栏,或自定义在“全部时间”之前。
- 当有面向用户的名称可用时,仍绘制不透明ID(如)的图表。
franchise_id - 将实体过滤器渲染为普通,即使当前运行时选项列表很短。
<select> - 为日期范围使用原生或任何日期选择器依赖(
<input type="date">、react-datepicker、UI组件库的选择器),而非react-day-picker,并交付浏览器控制的DateRangePopover占位符或无法自定义主题的日历弹出层。mm/dd/yyyy - 用标记日期触发器,而非使用
new Date("YYYY-MM-DD").toLocaleDateString(),导致格林威治以西的用户看到的标签早了一天。useDateFormatter() - 假设完全验证值类型。
filter(...) - 让桶成为最新的时间序列点。
null - 硬编码业务值、标签、时间戳或排名。
- 手动创建可直接用于图表的数组,而非从查询到的派生。
data.rows - 将类型化的SDK行断言为通用元组行,例如。
[string, number] - 当Metabase返回列名(如)时,读取生成的对象属性名(如
ordered_at)。row.orderedAt - 渲染schema中不存在或查询结果未返回的字段。
- 在SDK仍在进行身份验证或加载时渲染“无数据”。
- 创建嵌套的实例,而非在应用边界共享一个提供器。
MetabaseProvider