metabase-data-app-semantic-layer について
React で Metabase Data App を構築する開発者向けのセマンティックレイヤーガイドです。生成スキーマ、定義済みクエリとアクション、SDK コンポーネントを使い、クエリと表示を分離して推測データを避ける方法を示します。
主な機能
- 01生成スキーマをデータコンテキストとして利用
- 02`defineQuery` と `defineAction` でクエリとアクションを定義
- 03セマンティックオブジェクトと SDK ヘルパーでクエリを構築
- 04実行時フィルターを基本クエリから分離
- 05Metabase のクエリ結果から可視化を表示
活用シーン
- 01セマンティックレイヤーに基づく Data App の構築
- 02フィルターコントロールと動的クエリの接続
- 03Metabase のデータを使ったチャートや指標の表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ metabase/ metabase --skill metabase-data-app-semantic-layerインストール後、Claude Code で/metabase-data-app-semantic-layerを入力して読み込まれたか確認します。
同梱ファイルreferences/filter-ui-patterns.mdreferences/visualization-settings.md
詳細
Metabase データアプリ セマンティックレイヤー
基本的なルール
セマンティックレイヤーとプレゼンテーションレイヤーは分離してください。
- Metabase のコンテキストはすべて、生成されたスキーマファイル(通常は
src/metabase.data.tsまたはsrc/*.metabase.data.ts。そのランタイムオブジェクトと、コンテキストを与える情報がある場合にエントリが冒頭に記述する/* metadata: {...} */ブロックを含む)と、アプリのコレクションのファイルが何から書かれているかはnpm run print-resourcesから取得しなければなりません。リポジトリ内の Metabase YAML から取得してはならず、また自身の API 呼び出しから取得してはなりません。 - React UI を構築している間は、MCP ツールを通じてデータを探索したり、Metabase コンテンツを作成したり、テーブルを作成したり、セマンティックレイヤーを編集したりしないでください。
- データアプリのクエリヘルパーは
@metabase/embedding-sdk-react/data-appからインポートしてください。 - すべてのクエリは、ルートレベルの
queries/ディレクトリ内のdefineQuery(...)という名前付きエクスポートであり、すべてのアクションは、ルートレベルのactions/ディレクトリ内のdefineAction(...)という名前付きエクスポートで、どちらもpackage.jsonの隣に配置します。最初のフック呼び出しを書く前に両方のディレクトリを作成してください。テンプレートにはそれぞれ README 付きで含まれています。フックはこれをコンパイル時に強制します。useMetabaseQuery、useMetabaseQueryObject、useActionは、インラインオブジェクト、satisfies MetabaseQueryOptionsオブジェクト、および定義のスプレッドコピーを拒否します。エラーはProperty 'definedWithDefineQuery' is missing(または'definedWithDefineAction')と表示されます。修正は常に、そのオブジェクトを定義としてqueries/またはactions/に移動してインポートすることであり、キャストは決して行いません。 - すべての
savedQuestionEntityIdとcopiedActionEntityIdは、アプリのコレクション内のファイルを指します(Write every query and action into the app's collection を参照)。リファクタリングや名前変更の間もそれを保持し、決して別の定義にコピーせず、そのファイルが存在する間は決して削除しないでください。 - 生の ID や文字列よりも、生成されたスキーマオブジェクトを優先してください。トップレベルのテーブルオブジェクトにはローカル定数を抽出してください。
- アプリのコード内で
DatasetQuery/MBQL オブジェクトを手書きしないでください。{ type: "query", query: { "source-table": table.id } }のようなインラインクエリオブジェクト、生のsource-table句、生のフィールド ID、裸のテーブル ID、メトリック ID を、SDK コンポーネント、useMetabaseQuery、またはuseMetabaseQueryObjectに渡さないでください。生成されたテーブルおよびメトリックのスキーマオブジェクトを優先してください。単純なテーブルソースのクエリでは、{ type: "table", id: table.id }のような明示的なソース参照も有効です。 - クエリは
source: schema.tables.<name>、生成されたfields、生成されたsegments、生成されたmeasures、aggregations内の生成されたメトリック、生成されたメトリックのdimensions、filter(...)、breakout(...)、orderBy(...)、およびaggregations.count()やaggregations.sum(...)などのaggregationsヘルパーで構築してください。source: schema.metrics.<name>は使用しないでください。メトリックは集計式であり、クエリソースではありません。 - 既存の保存済みクエスチョンを
useMetabaseQueryまたはuseMetabaseQueryObjectのソースとして使用しないでください。型付きスキーマはschema.questionsを公開せず、question-collectionsもサポートしません。これは、アプリのリソースが既存の保存済みクエスチョンをコピーできないためです。 - 浅いテーブルダンプよりも、意味的に豊かなテーブルクエリを優先してください。生成されたアプリをより有用にする場合は、キュレーションされたテーブルのメジャー、セグメント、フィルター、ブレイクアウトを使用してください。
- React 側の推論よりも、セマンティックレイヤーの定義を優先してください。ある概念に対するセグメントやメジャーがスキーマにある場合は、生の行からその概念を再作成するのではなく、それを使用してください。どちらも静的クエリにのみ属します。動的な第 2 引数はそれらを受け取れません。Static and dynamic query parts を参照してください。
- フィルター UI はデフォルトでデータを表示しなければなりません。空のコントロール、「All」オプション、および不完全なカスタム範囲は、クエリをブロックしたり空白のダッシュボードを表示したりするのではなく、フィルターなしを生成すべきです。
- カテゴリカルフィルターのオプション値をハードコードしないでください。生成されたスキーマフィールドは、そのフィールドが存在することを証明するだけで、どの値が存在するかは証明しません。フィルターが適用するのと同じ生成されたスキーマフィールドを使用して、実行時に Metabase からオプションをクエリしてください。
- ダッシュボードレベルのフィルターは、すべての互換性のあるカード、テーブル、KPI、トレンドに目に見えて影響を与えるべきです。フィルターが 1 つのクエリにしか適用できない場合は、その適用範囲を UI で明確にしてください。重複した、または効果のない日付コントロールを表示しないでください。
- 保存された値が id/キーであり、UI がラベルを表示するエンティティフィルターは、単一の検索可能なコンボボックスを使用しなければなりません。クリック/フォーカスは、入力前にすぐにオプションリストを開く必要があります。実行時にオプションをクエリし、ラベルを検索し、生の値を保存してください。エンティティフィルターを決して
<select>として描画しないでください。プレーンなセレクトは、ユーザーが明示的に提供した短い閉じた列挙型にのみ使用します。 - カスタム日付範囲には
@metabase/embedding-sdk-react/data-appのDateRangePopoverを使用してください。これはアプリ自身のトリガー要素をラップし、その下に Metabase スタイルの範囲カレンダーを開きます。トリガーはアプリのままです。他のフィルターコントロールと同じようにスタイルを設定してください。同じエントリのuseDateFormatter()がそのラベルを生成します。これは SDK に同梱されており、依存関係も CSS インポートも不要です。DateRangeCalendarは同じカレンダーのインライン版で、アプリにすでにコンテナがある場合に使用します。範囲のために日付ピッカーライブラリをインストールしないでください。react-datepicker、react-day-picker、flatpickr、または UI スイートのピッカー(@mui/x-date-pickers、antd、rsuiteなど)は使用しないでください。ネイティブの<input type="date">も使用しないでください。そのプレースホルダーとカレンダーポップオーバーはブラウザーによって制御され、しばしばmm/dd/yyyyを表示し、確実にテーマを適用できません。 new Date("YYYY-MM-DD")で日付ラベルを構築しないでください。日付のみの文字列は UTC として解析され、グリニッジより西では前日を表示します。ローカル時間で解析し、インスタンスのロケールでフォーマットするuseDateFormatter()のformatDateRange/formatDateを使用してください。- サードパーティの日付ピッカーには、SDK カレンダーがカバーしないもの、たとえば単一日付や日時の選択にのみ頼ってください。
react-datepickerがデフォルトの選択です。その場合は、そのスタイルシート(react-datepicker/dist/react-datepicker.css)をインポートし、必要に応じてアプリの視覚スタイルに合わせて小さな CSS オーバーライドを追加し、Date | nullを渡してください。不完全な範囲に対してnew Date("")やその他の無効な日付を決して渡さないでください。厳密なコールバックパラメーターは、onChange={(date: Date | null) => ...}のように明示的に型付けしてください。 - 日付バーはデフォルトで Custom を最後に含めなければなりません。期間プリセット、All time、次に Custom です。Custom を省略するのは、ユーザーが固定プリセットのみ、または日付範囲コントロールなしを明示的に求めた場合だけです。
{ name: "count" }や{ name: "sum", field: ... }のような集計オブジェクトやメジャーオブジェクトを決してでっち上げないでください。生成されたテーブルメジャーまたはエクスポートされた集計ヘルパーを使用してください。- Metabase から返された値、または返された値の決定論的な変換のみを描画してください。KPI 値、トレンド、ラベル、ステータス、評価、タイムスタンプ、ランキング、インサイト、セグメント、チャートシリーズをでっち上げないでください。
margin、rate、score、percent、health、risk、efficiencyのような曖昧なビジネスフィールドをカスタム描画しないでください。セマンティックレイヤーの単位が明示的にサポートしていない限り、%を追加したり、100 を掛けたり、色分けしたり、星を描画したりしないでください。SDK のテーブル/チャートを使用するか、そのフィールドを省略するか、キュレーションを依頼してください。- 可視化データは、
InteractiveQuestion/StaticQuestionを伴うuseMetabaseQueryまたはuseMetabaseQueryObjectを通じて Metabase から取得しなければなりません。チャート用の配列、サンプルデータ、デモ値、スキーマ形状のモック値をハードコードしないでください。 - チャートは
InteractiveQuestion/StaticQuestionで描画してください。代わりにカスタム可視化が許可される場合、およびその 2 つのどちらを使用するかは、セットアップスキルの Rendering a chart: Metabase first によって決定されます。 useMetabaseQueryObject(...)は{ query, error, isLoading }を返します。queryプロパティのみをcard={{ query }}としてInteractiveQuestionまたはStaticQuestionに渡してください。フックの結果全体をcard.queryとして決して渡さないでください。useMetabaseQuery().rowsはタプル配列ではなく、キー付きオブジェクトです。row[0]/row[1]を決して読まないでください。また、これをas unknown as [string, number][]、DisplayRow、または別のタプルキャストで黙らせないでください。TypeScript がプロパティ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の第 2 引数に属します。「Static and dynamic query parts」を参照してください。aggregationsとbreakoutsを含むクエリにfieldsを含めないでください。ブレイクアウトがグループ化された結果列を決定します。fieldsは行選択クエリにのみ使用してください。- フィールドを描画する前に、それが生成されたスキーマオブジェクトに存在し、クエリによって返されることを確認してください。Metabase API、ビジネス上の直感、または古いモックデータからテーブルキー、フィールドキー、列名を推測しないでください。
src/metabase.data.tsに実際に出力されたエントリのみを使用してください。 - 返されたデータまたはキュレーションされたセマンティックレイヤーの定義がサポートしていない限り、「リアルタイム」、「ライブ」、「人員不足」、「リスク」などのサポートされていない鮮度や運用上の主張を避けてください。
- 作業が完了したと主張する前、または最終的な引き継ぎを準備する前に、TypeScript の型のみのチェックを実行し、コマンド/結果を報告してください。チェックが失敗した場合は、最終的な要約の前に型エラーを修正してください。
スキーマの生成
スキーマファイルがすでに存在する場合は、それを使用します。存在しない場合や古い場合は、スキーマ生成をこのデータアプリのセマンティックレイヤーのキュレーションとして扱ってください。機械的なエクスポートではありません。
生成する前にエクスポート範囲を選択します:
- 明示的な範囲が指定されている場合はそれに従います。そうでない場合は、アプリの目的から必要なコンテンツタイプ(テーブル、キュレーションされたメトリクス、アクション)を推測します。たとえば「注文を表示する」にはテーブルが必要です。行数や合計にはテーブルの集計も使用できます。
- それらのニーズをカバーする最も狭いサポート対象の範囲を、リクエストまたはプロジェクトのコンテキストにあるコレクション ID とデータベース名または ID を使って選択します。ライブラリの種類は明確でも、より狭いコレクションが不明な場合は、そのライブラリのツリー全体を使用します。
- 範囲の選択に必要なコンテキストのみを尋ねます。たとえば、リクエストがデータベース範囲を必要とするが特定していない場合に、どのデータベースを使用するかなどです。範囲が決まったら、それを簡潔に示し、確認を待たずに生成します。
範囲パラメータ:
include-data-library=trueはLibrary / Dataツリー全体を対象とします。include-metric-library=trueはLibrary / metricsツリー全体を対象とします。library-collections=<id-or-entity-id>[,<id-or-entity-id>]は特定の Data または metrics ライブラリのサブコレクションを対象とします。include-actions=trueはアプリが実行できるアクションをschema.actionsの下に含めます。database=<name-or-id>と組み合わせると、そのデータベースのアクションのみを含めます。database=<name-or-id>は、アプリが 1 つのデータベースのテーブルを使用する場合に指定します。ライブラリ範囲とは別に使用してください。API はその組み合わせを拒否します。
アプリがテーブルとキュレーションされたメトリクスの両方を必要とする場合は、ライブラリ範囲を組み合わせてください。
アプリが schema.actions の下の保存済みアクションを必要とする場合は、include-actions=true を使用します。database で 1 つのデータベースに範囲を絞らない限り、読み取り可能なすべてのアクションを含みます。library-collections、include-data-library、または include-metric-library と組み合わせて、1 つのスキーマに選択したテーブル/メトリクスと読み取り可能なすべてのアクションを含めることができます。
ユーザーが、作成、更新、削除、送信、承認、アクションの実行、書き込み操作の実行など、変更を伴うフローを求めた場合は、型付きスキーマの URL に include-actions=true を含めてください。ユーザーが特定のアクションを 1 つ指定した場合でもこれを行ってください。アクションは生成されたスキーマを通じてのみ検出できるためです。
Metabase の URL と API キーは リポジトリルート の .env.local に
DATA_APP_MB_URL と DATA_APP_MB_API_KEY としてあります(リポジトリごとに 1 ファイル、
通常はアプリディレクトリから 2 階層上にあり、アプリディレクトリ内ではありません)。キーは
Administrators グループのものである必要があります: 型付きスキーマは管理者にのみ応答し、
それ以外のキーでは 403 が返ります。以下のコマンドはそのファイルを source するので、
シェルが値をそのまま curl に代入します — 認証情報を自分で読み取ったり、抽出したり、
扱ったりすることはありません。
ユーザーに API キーをチャットに貼り付けるよう決して求めず、
.env.localをcat/echoしないでください — これは git で無視されており、他のシークレットを 含む可能性があるため、その内容は会話から外に出す必要があります。sourceして、 シェルが値を公開せずに使用できるようにしてください。sourceした後に$DATA_APP_MB_API_KEYまたは$DATA_APP_MB_URLが空であるか、デフォルトのmb_replace_meプレースホルダーのままである場合は、ユーザー自身に実際の値を追加するよう 依頼してから続行してください。
リポジトリルートの .env.local から認証情報を source し、範囲を指定した
スキーマを生成します。以下の例は Data ライブラリからテーブルデータをエクスポートします。
そのクエリパラメータを上で選択した範囲に置き換えてください:
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"
)
生成されたファイル内のエントリは、アプリを書くために提供すべきコンテキストがある場合、
そのランタイムプロパティの前に、JSON の /* metadata: {...} */ ブロックで始まります:
たとえば、フィールドの表示名、説明、セマンティック型、テーブルのデータベース、スキーマ、
実際の名前、メトリクスの説明、フィルター、および検証済みかどうかなどです。これはコメント
であるため、バンドルに到達することはなく、アプリがインポートすることもありません。読み取り、
ファイルを編集したり再フォーマットしたりしないでください。
エクスポートが成功したら、スキーマにリクエストされたアプリに必要なすべてのエンティティが含まれていることを確認します。不足している場合は、利用可能なコンテキストを使ってスコープを修正するか、UI を構築する前に不足しているコンテキストを確認してください。
スキーマ生成が失敗した場合は、エラーを隠したり、言い換えて曖昧にしたり、リトライでやり過ごしたりしないでください。型付きスキーマのエラーをユーザーに提示し、存在する場合は失敗した id、名前、メッセージを含めてください。
すべてのクエリとアクションをアプリのコレクションに書き込む
データアプリが実行するすべてのクエリとアクションは、アプリ自身のコレクション内のコピーを経由します。そのコレクションへの読み取りアクセスがあれば、閲覧者はアプリのカードを実行できますが、表示されるのはすでに読み取り可能なテーブルのデータだけです。アプリが付与するのはコレクションであり、テーブルではないためです。それぞれを、package.json の隣にあるルート直下のディレクトリ内で名前付きエクスポートとして宣言します。defineQuery(...) は queries/、defineAction(...) は actions/ です。CLI がスキャンするのはこれら 2 つのディレクトリだけなので、src/queries/、src/actions/、その他のソースディレクトリにある定義は決して認識されません。検出の対象は .js、.jsx、.ts、.tsx、.cjs、.cts、.mjs、.mts です。
import { defineAction, defineQuery } from "@metabase/embedding-sdk-react/data-app";
import schema from "../src/metabase.data";
// queries/revenue.query.ts
export const RevenueQuery = defineQuery({
savedQuestionEntityId: "<entity ID of its saved question>",
source: schema.tables.orders,
});
// actions/orders.action.ts
export const CreateOrder = defineAction({
copiedActionEntityId: "<entity ID of its copied action>",
action: schema.actions.createOrder,
});
各定義は、リポジトリの collections/data_apps/ ディレクトリに書き込むシリアライズされた Metabase YAML によって支えられます。そこは data-apps 名前空間のコレクションが Metabase のエクスポートとして存在する場所です。リポジトリが pull されるまで Metabase 側では何も変わりません。pull は、リポジトリが保持する他のすべてと同様に、ファイルをシリアライズされたコンテンツとしてアプリのコレクションに読み込みます。そのため、そのコレクション内に手作業で何かを作成するのではなく、ファイルを書き込んでください。アプリのディレクトリ data_apps/<slug>/ から見ると、そのディレクトリは ../../collections/data_apps/ です。
data_apps/<slug>/data_app.yaml collection: <entity ID of the app's collection>
collections/data_apps/<collection>.yaml the app's collection, a root collection of the data-apps namespace
collections/data_apps/<collection>/<name>.yaml a saved question per query, copies of the metrics they use, and a copy of each action the app runs
<collection> は、Metabase がスラッグ化したコレクション名です。Data App: Sales なら data_app__sales になります。pull と CLI は、パスではなくコンテンツによってアプリのファイルを見つけます。つまり、マニフェストが entity_id を指定するコレクションと、その collection_id を持つカードとアクションです。
YAML は Metabase 表現形式で書いてください。collections/data_apps/ 配下のファイルを書き込む、または編集する前に、Metabase 表現 YAML を読み書きするためのスキルをロードし (スキルディスカバリを使用)、そこにあるすべてのファイル (コレクション、カード、アクション) に対してそのスキルとそのフォーマット仕様に従ってください。そのようなスキルが利用できない場合は、npx skills add metabase/agent-skills/skills --skill metabase-representation-format でインストールしてください。それでもロードできない場合は、ファイルを書き込まずに停止し、ユーザーに伝えてください。以下のいずれもその代わりにはなりません。以前のアプリの YAML (git 履歴内のファイルを含む)、仕様内の 1 つのフィールドを検索すること、または npm run check-resources が通ること。そのスキーマ検証はスキーマをチェックするものであり、フォーマットの慣習をチェックするものではありません。すべてのカードとコピーは、以下に示すように npm run print-resources が出力する内容から書かれます。それをどう書くかがフォーマットスキルです。フィールドの形を決して推測しないでください。新しいエンティティ ID はすべて、アプリ自身の dev 依存関係から npx representations generate-entity-id で生成してください (複数の場合は --count <n>)。決してでっち上げたり、別のアプリのものを再利用したり、コピー元のエンティティの ID をそのコピーに残したりしないでください。
-
コレクション。 アプリごとに 1 回、そして
collections/data_apps/配下の何よりも前に、collections/data_apps/<collection>.yamlをnamespace: data-appsを持つData App: <app name>という名前のコレクションとして書き込み、そのエンティティ ID をdata_app.yamlにcollection: <id>として記述します。通常、スキャフォールディングはマニフェストとともにこれを書き込みます。data_app.yamlがすでにコレクションを指定していて、そのファイルが存在する場合はこの手順をスキップしてください。print-resourcesは、マニフェストがコレクションを指定するまで実行を拒否します。保存された question をそこに出力するためです。それはその名前空間のルートコレクションでなければなりません。parent_id、personal_owner_id、type、authority_level、archive_operation_idを持たず、is_remote_synced、is_sample、archivedでもありません。pull は、collection:のないマニフェスト、およびそのコレクションがリポジトリ内にファイルを持たないマニフェストを拒否します。 -
リソースが何から書かれているかを出力します。 すべての
defineQueryはそのsavedQuestionEntityIdを、すべてのdefineActionはそのcopiedActionEntityIdを、出力する前に保持しており、これらはnpx representations generate-entity-idで生成されます。保存された質問は定義の ID とともに出力されるため、print-resourcesは ID のないクエリを拒否し、その名前を報告します。次にnpm run print-resources(オプションで 1 つのファイルを指定、npm run print-resources -- queries/revenue.query.ts)が、リポジトリルートの.env.localにあるDATA_APP_MB_URLとDATA_APP_MB_API_KEYを使ってアプリの定義を Metabase に送信し、シリアライズが書き出すとおりにすべてを JSON として出力します。すべての参照は数値 ID ではなく名前またはエンティティ ID によるものです:queries: 各定義のexport、file、savedQuestionEntityId、Metabase がそれに対してentityとして書き出す保存された質問(export にちなんで命名され、アプリのコレクション内にあり、そのエンティティ ID を持ち、開発プレビューが実行するクエリを保持します)、およびそれが集計するmetricsのエンティティ ID。actions: 各定義のexport、file、copiedActionEntityId、およびソースアクションのentity。metrics: クエリが集計するメトリクス。それぞれentityとして。
アクションまたはメトリクスの隣にある数値の
idは、それがどのソースであるかを示すだけです。アプリのコレクションのどのファイルもそれを保持しません。このエクスポートに応答できるのは管理者だけなので、API キーは Administrators グループ内のものでなければなりません。それ以外のキーではコマンドは
403で失敗します。停止してユーザーに伝えてください。決して別の方法でファイルを書かないでください。ビルドまたはコピーできない項目には、代わりに
errorがあります(たとえば、クエリが名前を挙げる存在しないフィールド、モデルに属するアクション、または別のカードを読み取るメトリクスやアクション)。停止してユーザーに伝えてください。定義を変更した後は再度実行し、影響を受けるファイルを更新してください。 -
クエリごとに 1 つの保存された質問。 出力された
entityをそのままcollections/data_apps/<collection>/<name>.yamlに書き込みます。その名前、type、display、creator_id、collection_id、エンティティ ID、およびserdes/metaはすでに設定されています。何も追加しないでください。唯一の変更はそのdataset_queryにあります。各メトリクス参照([metric, {}, <entity ID>])は、ソースメトリクスのエンティティ ID ではなく、そのメトリクスのアプリのコピー(ステップ 4)を指します。スクリプトで出力された JSON をシリアライズするのではなく、フォーマットスキルのレイアウト(MBQL 句のレイアウト方法)で、出力されたキー順を保ったまま自分で書いてください。スクリプトはフォーマットスキルをスキップし、その出力はフォーマットに対してチェックされません。クエリは開発プレビューが実行したものであるため、その中の違いは本番で実行されるものを変えてしまいます。ファイルを書いた後、それをパースし直して出力された entity と比較し、メトリクス参照以外のすべての違いを修正してください。 -
メトリクスごとに 1 つのコピー。 クエリが集計する各メトリクスもコピーする必要があります。なぜなら、閲覧者はアプリのコレクションしか読めないからです。メトリクスの出力された
entityを、新しいエンティティ ID とアプリのコレクションに設定されたcollection_idを持つカードとしてcollections/data_apps/<collection>/に書き込み、そのメトリクスを使用するすべての質問からコピーのエンティティ ID を参照します。1 つのコピーがそれらすべてに使えます。 -
アクションごとに 1 つのコピー。 アクションの出力された
entityを、新しいエンティティ ID とともにcollections/data_apps/<collection>/に書き込み、定義のcopiedActionEntityIdとして設定し、collection_idをアプリのコレクションに設定します。アプリはモデルに属さないクエリアクションのみを実行するため、コピーがmodel_idを持つことは決してありません。 すべてのカードとコピーは、フォーマットスキルに従って、印刷されたエクスポートから自分で書いてください。決して YAML から書かないでください(リポジトリのエクスポートされたコレクションやそのトップレベルのactions/、別のアプリのコレクション、git 履歴のいずれからも)。また、自分の API 呼び出しからも、印刷された JSON を YAML としてダンプするスクリプトからも書かないでください。その参照はすでに YAML が使用する形式になっているので、そのままコピーしてください。コピーは、そのエクスポートが印刷するものを正確に保持します。ただし、それをコピーたらしめるものは除きます。すなわち、新しいentity_id(および対応するserdes/metaのid、コピーの名前からのラベル付き)とcollection_idです。印刷は、シリアライゼーションが未設定またはデフォルトのままにするもの(description、archived、enable_embedding、public_uuid、フォームフィールドの未設定の設定項目など)をすでに省略しています。フォーマットもそれらを省略するため、印刷にないキーを追加せず、null値のキーを保持せず、印刷されたキーの順序を保ってください。
すべてのカードとアクションは、印刷が与える creator_id を持ちます。Metabase はアプリをロードするとき、それを内部ユーザーに置き換えます。
アプリが必要とするソースが src/metabase.data.ts にない場合、またはそのエクスポートに error がある場合は、停止してユーザーに伝えてください。決してゼロから代替を書かないでください。Metabase でソースが変更された後は、npm run print-resources を再度実行し、そのコピーを再度書いてください。コピーのエンティティ ID は保持してください。
npm run check-resources を実行し、それが報告するすべてのファイルを修正してください。これは、定義とアプリのコレクションのファイルが一致しないときに失敗し、すべての問題を列挙します。すなわち、コレクションを名指ししていない、またはファイルのないコレクションを名指ししているマニフェスト、エンティティ ID がない、またはどのファイルも保持していないエンティティ ID を名指ししている定義、質問でないカードを名指ししているクエリ、どの定義も名指ししていない質問またはアクションです。次に、リポジトリ内のすべての Metabase YAML ファイル(アプリのコレクションファイルを含む)をフォーマットのスキーマに対して検証します。npm run build は最初のチェックを実行します。メトリクスのコピーは、何もそれを使用しなくなったら自分で削除してください。どちらのコマンドも Metabase を呼び出しません。次のプルはすべてを再度検証し、アプリのリソースを拒否する場合は、ファイル名を挙げたメッセージを Data apps 管理ページに表示します。そのファイルを修正して再度プッシュしてください。
定義自体をフックに渡し、何が実行されるかを SDK に解決させてください。本番ビルドはコピーを実行し、開発プレビューは作成されたテーブルまたはアクションを実行するため、アプリはそのリソースが存在する前でも動作します。
const { data } = useMetabaseQuery(RevenueQuery, {
filters: [filter(RevenueQuery.source.fields.status, "=", selectedStatus)],
});
const { execute, isExecuting, error } = useAction(CreateOrder);
インラインのテーブルソースクエリ(読み取り専用、フィルターオプション、ヘルパークエリであっても)や、生のアクション ID、savedQuestionEntityId、copiedActionEntityId、手で組み立てた { source: { type: "card", id } } を決して渡さず、定義を新しいオブジェクトにスプレッドしないでください。それぞれがスワップを無効にします。作成された ID はパーミッション境界もバイパスします。TypeScript はこれらのほとんどを拒否します。フックは defineQuery/defineAction が返したものだけを受け入れるため、インラインオブジェクト、satisfies 型のオブジェクト、スプレッドコピー、および schema.actions.<action> はすべてコンパイルに失敗します。tsc が Property 'definedWithDefineQuery' is missing または Property 'definedWithDefineAction' is missing を報告する場合、その引数は定義ではありません。それを queries/ または actions/ に移動し、そのエクスポートをインポートしてください。キャストで、または呼び出し側でインラインオブジェクトを defineQuery(...) でラップすることで黙らせないでください。それはコンパイルは通りますが、保存された質問に裏付けられることは決してありません。固定のパーミッション境界フィルター、集計、およびブレイクアウトは defineQuery 内に保ってください。それらは保存された質問に焼き込まれるため、その外で再度適用せず、実行時の句はフックの第 2 引数に置いてください(静的なクエリ部分と動的なクエリ部分 を参照)。useAction にジェネリクスは不要です。定義が execute のパラメーターと result を型付けします。
package.json を "print-resources": "embedding-sdk-react data-apps print-resources"、"check-resources": "embedding-sdk-react data-apps check-resources && representations validate-schema --folder ../.."(@metabase/representations を dev 依存として使用)、および "build": "vite build" で設定します。定義を追加、変更、名前変更、または削除した後は、アプリのコレクションのファイルを更新してから、npm run check-resources と npm run build を実行します。npm run build が成功するまで、アプリのテストや引き渡しを行わないでください。data_app.yaml、collections/data_apps/ 配下のアプリのファイル、および定義をまとめてコミットします。Metabase がリポジトリをプルすると、アプリがそれらを読み込みます。
標準パターン
クエリごとに 2 つのファイル: queries/ 内の定義と、コンポーネント内のフック呼び出しです。ここから先の例では簡潔にするため savedQuestionEntityId を省略していますが、実際のすべての定義にはこれが含まれます(すべてのクエリとアクションをアプリのコレクションに書き込む を参照)。
// 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,
});
// 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.rows から正確な行キーが必要な場合は、選択フィールドのクエリでは省略します。以下のレシピは defineQuery に渡すオブジェクトを示しています。それぞれは queries/ 内のエクスポートであり、フックに直接記述する引数ではありません。
各スキーマエントリはレンダーツリーごとに最大 1 回だけ呼び出してください。 同じ questionId(または同じ tableId と同一のフィルター/メジャー/ブレイクアウト)に対する複数の useMetabaseQuery 呼び出しは、独立したサブスクリプションをマウントし、重複したクエリを発行し、読み込み中にコンシューマー間でデータが食い違う原因になります。呼び出しはデータを必要とする最も上位のコンポーネントに引き上げ、data / isLoading / error を props として下位に渡してください。異なる id、または同じ id でもフィルター / ブレイクアウトが異なる場合は、異なるデータソースです。個別に呼び出してください。
キー付きスキーマオブジェクトを使用します:
- テーブル:
source: schema.tables.<table> - メトリクス:
aggregations内のschema.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" のような生のディメンション文字列を渡さないでください。
静的なクエリ部分と動的なクエリ部分
どちらのクエリフックも、省略可能な第2引数を取ります。これはアプリの実行中に変化する句です。
// 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)],
});
何も依存していないように見えても、このように分割してください。第1引数はすべてのレンダーで同一でなければならず、実行時の状態によって変化してよいのは第2引数だけです。
動的な句は独自のステージとして実行されるため、静的なクエリの結果カラムを参照し、そのソーステーブルは参照しません。だからこそ plan は上記のブレイクアウトなのです。ソースカラムでフィルタリングするコントロールは、そのカラムが結果に残っている場合にのみ機能します。残っていない場合は、それをブレイクアウトとして追加するか、静的なクエリを集約せずにそのままにしてください。同様に、集約された静的なクエリは、その背後にあるフィールドではなく count/sum でフィルタリングしてください。
セグメントとメジャーは静的な部分にのみ属します。 これらはテーブルに対して定義され、動的なステージにはテーブルがありません。したがって、filters: [orders.segments.completed] と aggregations: [orders.measures.revenue] は、コンパイル時と実行時の両方で拒否されます。これは、通常の「キュレーションされた定義を優先する」というルールが適用されない唯一の箇所です。
キュレーションされた定義は、それが解決される静的なクエリに置き、動的な句はそこから出てきたものに対して動作させてください。
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)],
});
コントロールでセグメントをオンとオフに切り替える必要がある場合、それは動的な句ではなく静的なクエリ間の選択です。状態ごとに1つのクエリを定義してそのクエリを選ぶか、同じ条件を結果カラムに対するフィルターとして表現してください。
クエリオブジェクトに savedQuestionEntityId が見つかっても、アクション定義に copiedActionEntityId が見つかっても、それらを削除しないでください。それぞれが、アプリのコレクション内にあるその定義のファイルを指し示しています。詳しくはすべてのクエリとアクションをアプリのコレクションに書き込むを参照してください。
テーブルクエリのレシピ
テーブルクエリでは、生成されたテーブルオブジェクトを source として渡します。
// queries/records.query.ts
const recordsTable = schema.tables.records;
export const RecordStatuses = defineQuery({
source: recordsTable,
fields: [recordsTable.fields.id, recordsTable.fields.status],
});
グループ化されたテーブルサマリーでは、少なくとも1つの集約を含めてください。
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" })],
});
キュレーションされたメジャーを使わない基本的な集約には、aggregations ヘルパーを使用します。
export const AmountByCategory = defineQuery({
source: recordsTable,
aggregations: [
aggregations.count(),
aggregations.sum(recordsTable.fields.amount),
],
breakouts: [breakout(recordsTable.fields.category)],
});
クエリで同じヘルパーを複数回使用する場合は、それぞれに name を付けてください。その名前は結果カラムの名前と行キーになり、型付けされ、orderBy(...) や実行時の句がその集約を参照する際に使われます。名前を付けない場合、カラムは sum、sum_2 などの形で返り、そのいずれかによる並べ替えやフィルタリングは失敗します。
const totalAmount = aggregations.sum(recordsTable.fields.amount, {
name: "total_amount",
});
const totalTax = aggregations.sum(recordsTable.fields.tax, {
name: "total_tax",
});
export const TaxByCategory = defineQuery({
source: recordsTable,
aggregations: [totalAmount, totalTax],
breakouts: [breakout(recordsTable.fields.category)],
orderBys: [orderBy(totalTax, "desc")],
});
// Rows are keyed `total_amount` and `total_tax`.
テーブルのフィールド、セグメント、メジャー、フィルター、ブレイクアウト、orderBy は、クエリ対象のテーブルから取得する必要があります。定義時に TypeScript でその所有関係を検証したい場合は、defineQuery<RecordsTable>({ ... }) を使用してください。
metric 集約のレシピ
metric に基づくクエリでは、生成されたテーブルオブジェクトを source として渡し、生成された metric オブジェクトを aggregations に渡します。
// queries/revenue.query.ts
const ordersTable = schema.tables.orders;
const revenueMetric = schema.metrics.revenue;
export const Revenue = defineQuery({
source: ordersTable,
aggregations: [revenueMetric],
});
所有する metric を集約するクエリでは、フィルターとブレイクアウトに生成された metric ディメンションを使用してください。metric のソーステーブルのディメンションは直接機能します。関連テーブルのディメンションも、生成されたフィールドに sourceFieldId が含まれていれば機能します。生の外部キー ID でグループ化するのではなく、読みやすいラベルのためにこれらの関連テーブルのディメンションを優先してください。
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);
metric に基づくクエリは、生成された metric ディメンションに対するヘルパー集約を含めることができます。また、生成されたスキーマが公開している場合は、テーブルソースの互換性のある保存済みセグメントとメジャーも使用できます。
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)],
});
metric 集約はテーブルソースに属している必要があります。テーブルソースのクエリでソースカードの metric を使用しないでください。生成された metric ディメンションは、それを所有する metric にスコープされます。クエリがフィルター、ヘルパー集約、ブレイクアウト、orderBy で revenueMetric.dimensions.* を使用する場合、aggregations にも revenueMetric を含める必要があります。無関係な count() やテーブルメジャーのクエリで、metric ディメンションを単独のテーブルフィールドとして使用しないでください。生成された metric ディメンションも、テーブルソースに解決される必要があります。定義時に TypeScript でそれを検証したい場合は、defineQuery<typeof ordersTable>({ ... }) を使用してください。
SDK がレンダリングするビュー
テーブルフィールド、セグメント、メジャー集計、メトリック集計は、クエリ対象のテーブルから取得する必要があります。フィルター、ヘルパー集計、ブレイクアウト、orderBys で使用される生成メトリックディメンションは、クエリ対象のテーブルに解決され、同じクエリの aggregations に含まれるメトリックに属している必要があります。
テーブルクエリで fields、segments、aggregations、breakouts、orderBys を使用する場合は、defineQuery に形状を推論させるか、所有権の検証が正確な結果行のキーよりも重要な場合は defineQuery<typeof recordsTable> と記述します。
インタラクティブな Metabase ビュー
ある要素がそもそも SDK の質問であるかどうか — そしてそれが StaticQuestion なのか InteractiveQuestion なのか — は、データアプリのセットアップスキル(チャートのレンダリング: Metabase を優先)で決定されます。決定されたら、queries/ でクエリを宣言し、useMetabaseQueryObject(TheQuery) で解決し、その結果を SDK の質問コンポーネントの card プロパティに渡します。
useMetabaseQueryObject は、メトリック集計を含む生成テーブルクエリをサポートします。カスタム React で直接行データが必要な場合は useMetabaseQuery を使用し、Metabase に可視化をレンダリングまたは管理させたい場合は useMetabaseQueryObject を使用します。useMetabaseQueryObject にジェネリクスを渡さないでください。これはクエリ結果の行ではなく { query, error, isLoading } を返します。
SDK コンポーネントでのレンダリング の下の例では、最小限のローディングとエラー処理に return null を使用しています。実際のアプリでは、そこにアプリの既存のローディング UI またはエラー UI をレンダリングしてください。query が null の間は card={{ query }} を渡しても安全です。フックの結果 { query, error, isLoading } 全体を card.query として渡さないでください。
useMetabaseQueryObject を再利用可能なチャート/カードコンポーネントでラップする場合は、error を分割代入してレンダリングしてください。{ query } だけを読まないでください。そうしないと、クエリ構築の失敗が無限ローディングのように見えてしまいます。その子コンポーネント内でフックを呼び出すことは有効な React です。親コンポーネントのループ、条件、コールバック内でフックを直接呼び出さないでください。
誤り/正しいパターン:
const trendQuery = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // wrong
const { query: trendQuery } = useMetabaseQueryObject(TrendQuery);
<InteractiveQuestion card={{ query: trendQuery }} />; // right
フックの型付け:
- どちらのフックも、
queries/からインポートしたdefineエクスポートだけを引数に取り、それ以外は取りません。インラインオブジェクトはコンパイルエラーになります。フックにジェネリクスを記述しないでください。 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.ts で正確な宣言 declare const cardDisplayTypes: readonly [...] を検索し、そのタプルを信頼できる情報源として使用してください。宣言ファイル全体をコンテキストに読み込まないでください。
SDK でレンダリングされるアドホックな質問には、常に card オブジェクトを渡してください。ユーザーが特定のチャートタイプを求めておらず、Metabase のデフォルトでクエリから妥当な表示を推測できる場合は、card={{ query }} から始めます。ユーザーのリクエストや設計上、特定のチャートタイプ(たとえば分布を表す円グラフなど)が必要でも、設定レベルのカスタマイズを求めていない場合は、card={{ query, visualization }} を使用します。visualizationSettings を追加するのは、ユーザーが設定レベルの表示変更(軸ラベルの非表示や名前変更、値ラベルの表示、棒の積み上げ、目標線の追加、テーブル列の並べ替え、円グラフの合計やラベルの表示、系列やスライスの順序の制御など)を明示的に求めた場合だけです。node_modules/@metabase/embedding-sdk-react/dist/index.d.ts で export declare type MetabaseCard、該当する *VisualizationSettings 型、および使用予定の設定キーを検索してください。それらの宣言に付随する JSDoc コメントを読み、正当な visualization と visualizationSettings の組み合わせについては TypeScript の宣言を信頼できる情報源としてください。クエリは useMetabaseQueryObject で構築してください。内部のクエリ解決ヘルパーを呼び出したり、any でキャストしたり、記憶にある設定をハードコードしたりしないでください。
公開されている設定の概要とその使用場面については、references/visualization-settings.md をお読みください。このファイルはあくまでガイダンスとして扱ってください。何が正当かは、インストールされている SDK の宣言が判断します。
card を書く前に、node_modules/@metabase/embedding-sdk-react/dist/data-app.d.ts で useMetabaseQueryObject の戻り値の型を確認してください。返された query を分割代入し、その値を card.query で使用します。設定済みのカードでは、そのオブジェクトを satisfies MetabaseCard で型付けします。TypeScript が重複した不透明な DatasetQuery シンボルを報告する場合は、キャストを強制しないでください。card を使用する前に SDK パッケージを更新してください。
生成されたクエリや可視化設定に対して、代わりの prop 名を勝手に考案しないでください。SDK の型で prop が存在しないと示されている場合は、それを信じ、ドキュメント化されている card prop の形式を使用してください。
useMetabaseQuery が必要な場合は、レンダリングの前に、型付きの行を名前付きプロパティを使った明示的なローカルのビューモデルにマッピングしてください:
const orderedAtKey = ordersTable.fields.orderedAt.name;
const chartRows = (data?.rows ?? []).map((row) => ({
label: String(row[orderedAtKey] ?? "Unknown"),
value: row.count,
}));
結果のキーが実行時に data.columns からのみ得られる場合は、一致する列位置を指定して rawRows を使用するか、data.rows にインデックスを付ける前にキーをリテラル型に絞り込んでください。
SDK コンポーネントによるレンダリング
ツールバーなしのチャートのみ:
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 可視化:
// 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>
);
フィルターとブレイクアウト
ヘルパーを使用してください。オートコンプリートが向上し、エラーが簡潔になります。
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", numBins: 10 },
});
breakout(ordersTable.fields.state);
orderBy(ordersTable.fields.createdAt, "desc", { unit: "month" });
{ field, direction } や { fieldId, direction } のような orderBys オブジェクトリテラルを手書きしないでください。orderBy(...) を使用してください。日付ブレイクアウトで使用されるのと同じ日付フィールドを並べ替える場合は、同じ unit を breakout(...) と orderBy(...) の両方に渡してください。
上位 N 件のグループ化された集計では、生のソースフィールドではなく、集計結果で並べ替えてください。集計ヘルパーをローカル定数に格納し、その同じ定数を aggregations と orderBy(...) の両方に渡してください:
// 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,
});
ユーザーが選択可能な並べ替えの場合、並べ替えは実行時の状態であるため、フックの第 2 引数に属します。fields オブジェクト全体をインデックスするのではなく、許可された生成フィールドの型付きマップを構築してください:
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")],
});
Scorecard は queries/ 内の集計されていない defineQuery({ source: scorecardTable }) エクスポートです。そのソースフィールドは結果に引き継がれるため、動的ステージでそれらによって並べ替えることができます。
メトリクスクエリでは、生成されたメトリクスディメンションを filter(...) と breakout(...) に渡してください:
filter(revenueMetric.dimensions.orders.status, "=", "paid");
breakout(revenueMetric.dimensions.orders.createdAt, { unit: "month" });
フィルター演算子のルール:
- string:
=,!=,contains,does-not-contain,starts-with,ends-with,is-empty,not-empty,is-null,not-null - number:
=,!=,>,>=,<,<=,between,is-null,not-null - date:
=,!=,>,>=,<,<=,between,time-interval,is-null,not-null - boolean:
=,is-null,not-null
unit を使用できるのは日付ディメンションのみです。日付以外のディメンションは unit なしでブレイクアウトとして使用できます。数値ディメンションは binning を使用できます。
セグメントはすでにフィルターです:
filters: [
schema.tables.records.segments.activeRecords,
filter(schema.tables.records.fields.amount, ">", 100),
];
キュレーションされたセグメントが製品の意図に正確に一致する場合は、まずそれらを使用してください。UI がしきい値、カテゴリ、日付範囲、テキスト一致、ブール条件、またはキュレーションされたセグメントではすでに表現されていないその他の絞り込みを必要とする場合は、filter(...) を使用してください。
フィルター UI パターン
ユーザーがカスタムフィルターを要求した場合は、セマンティッククエリフィルターに供給する通常の React コントロールを構築してください。
フィルターを実装する前に、表示されるダッシュボードのフィルター契約を作成してください。少なくとも以下を特定してください:
- 各フィルターについて、そのオプションを提供するランタイムクエリを指定してください。
- 各フィルターについて、
filter(...)で使用される生の値を指定してください。 - 各カード、テーブル、KPI、トレンドについて、そのフィルターを受け取ることができる生成テーブルフィールドまたはメトリクスディメンションを指定してください。
- フィルターが 1 つのセクションにのみ適用される場合は、セクションスコープに保つか、グローバルフィルターバーから省略してください。
- ページが
snapshotDateのような異なる日付フィールドを必要とする場合は、そのページに 1 つの表示される日付コントロールを使用してください。 - 同じ概念を表す KPI/詳細のペアは、同じ関連フィルターを使用する必要があります。
フィルター状態のルール、ランタイムカテゴリオプション、古いオプションのリセット、検索可能なコントロール、およびカスタム日付ピッカーの実装については、references/filter-ui-patterns.md の詳細なチェックリストを使用してください。
一般的なメモ化された日付/カテゴリフィルターの形式:
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 | null を使用する場合、選択された日付を filter(..., "between", range) に渡す前に、ローカルの YYYY-MM-DD フォーマッターで変換してください。ローカルの日付フィルターには date.toISOString().split("T")[0] を使用しないでください。
結果の形状とチャート
- キー付きの
data.rowsを優先してください。 data.rowsを位置ベースの配列として扱わないでください。useMetabaseQueryの行オブジェクトに対してrow[0]、row[1]、DisplayRow、タプルへのキャストを使用しないでください。- 低レベルの
rawRowsをマッピングする前にdata.columnsを確認してください。ただし、型付きのdata.rowsをインデックスするために任意のdata.columns[].name文字列を使用しないでください。 - 実行時の行オブジェクトは、返された Metabase のカラム名(通常は
total_amountやaverage_scoreのようなcolumn.name)でキー付けされます。totalAmountやaverageScoreのような生成されたスキーマのキーが実行時の行のキーであると想定しないでください。 - 生成されたフィールド参照では、React のプロパティパスと実行時の結果キーが異なる場合があります。
schema.tables.orders.fields.orderedAt.nameがordered_atである可能性があります。row.orderedAtを推測するのではなく、row[ordersTable.fields.orderedAt.name]、row.count、またはdata.columnsのメタデータを使用してください。 - 行の値は null 許容として扱ってください。
toFixed、toLocaleString、文字列変換などの数値/文字列メソッドを呼び出す前にガードしてください。 rawRowsは既知の位置ベースの形状にのみ使用してください。- 集約カラムは
count、sum、avgと命名されている場合があります。必要に応じてメタデータと照合してください。 - クエリに同じ種類のヘルパー集約が複数ある場合(たとえば複数の
aggregations.sum(...)呼び出し)、それぞれに名前を付け({ name: "..." })、その名前で行を読み取ってください。sum_2のような生成された名前に依存しないでください。 - グループ化されたクエリには
nullのブレイクアウトバケットが含まれる場合があります。それを"Unknown"としてレンダリングするか、意図的に除外してください。 - 時系列チャートには複数の順序付けられたバケットが必要です。スカラーまたは 1 点のみの結果に対してスパークラインを偽装しないでください。
- 単位や規模が異なる複数系列のチャートには、別々の軸または正規化が必要です。
- ユーザーに表示する値はフォーマットしてください。通貨は最大 2 桁の小数、カウントは整数、日付は読みやすいラベルとしてください。
- セマンティックレイヤーの説明または検査したサンプル値が意味と単位を明確にしない限り、曖昧な派生ビジネス値をレンダリングしないでください。
- 空の結果は読み込み中とは異なります。
isLoadingが false になったら、スケルトンや空白の KPI を残すのではなく、明確な空の状態をレンダリングしてください。
表示のガイドライン
チャート形状およびテーブル形状のデータには、Metabase がレンダリングするパネルを優先してください。カスタムパネルが正当化される場合、React アプリは data.rows から表示専用の値をグループ化、並べ替え、フォーマット、導出してもかまいませんが、カスタムパネルをデフォルトにしないでください。
適切な変換:
- サマリーのために行をグループ化します。
- カスタムリストが Metabase の行/棒/テーブル可視化よりも明らかに優れている場合にのみ、ランキングリストのために行を並べ替え、スライスします。
- 実際のデータ形状からチャートタイプを選択し、カスタムチャートコードを書く前に Metabase の
bar、line、area、row、combo、pivot、table表示を優先してください。 - 読み込み、エラー、空の状態を表示します。
- 高密度の結果表示を制限します。テーブル、アラートリスト、ログ、ランキングリストは、top-N スライス、グループ化、ページネーション、または固定/最大高さのスクロール領域を使用して、大きな結果セットがページ全体を引き伸ばせないようにしてください。
ページが生のテーブルブラウザのように感じられる場合は、スキーマに基づく方法でそれを豊かにする方法を探してください:
- アクティブ、完了、期限超過、高優先度、要注意レコードなどの厳選されたサブセットにはセグメントを使用します。
- React でその場で再計算するのではなく、厳選された集約にはメジャーを使用します。
- スキーマがページの核心的なビジネス課題に対する厳選されたメトリクス集約を公開している場合は、メトリクスを使用します。
- UI の意図にクエリを集中させるためにフィルターを使用します。
- トレンド、カテゴリ比較、グループ化されたサマリーを作成するためにブレイクアウトを使用します。
- 豊かにされた結果が依然として並べ替え可能/ドリル可能なテーブルである場合は、React でテーブル動作を再構築するのではなく、SDK の可視化コンポーネントでレンダリングしてください。
セマンティックレイヤーにすでに概念がある場合は、手動の分類を避けてください。React での文字列マッチング、閾値ヒューリスティック、カテゴリの再構築よりも、厳選されたセグメント、フィールド、メジャーを優先してください。
意図された UI をサポートする厳選されたスキーマエントリがない場合は、そのセクションを省略するか、セマンティックレイヤーのキュレーションを依頼してください。完成したアプリにモックデータやプレースホルダーの分析情報を残さないでください。
最終チェック
npm run typecheckを実行します。Property 'definedWithDefineQuery' is missingまたはProperty 'definedWithDefineAction' is missingは、フックがqueries/またはactions/のエクスポート以外を受け取ったことを意味します。そのオブジェクトをそこへ移動してインポートしてください。- 変更したファイルで
useMetabaseQuery(、useMetabaseQueryObject(、useAction(を検索します。最初の引数はqueries/またはactions/からインポートされた識別子でなければなりません。そこに{、defineQuery(、defineAction(、またはスプレッドがあるのは、コンパイルが通っていても誤りです。クエリフックの 2 番目の引数は動的オブジェクトであり、インラインで記述します。 queries/とactions/がsrc/の下ではなくpackage.jsonの隣にあり、アプリがレンダリングするすべての定義がそこに存在することを確認します。- 表現形式スキルを読み込み、アプリのコレクションのすべてのファイルをそれに対してチェックしたことを確認します。引き継ぎでその旨を述べるか、実施していない旨を述べてください。
- すべての定義にエンティティ ID とアプリのコレクション内のファイルがあり、各保存済みクエスチョンがその定義と一致することを確認します。
npm run check-resourcesを実行し、続いてnpm run buildを実行します。アプリのコレクションファイルが定義を裏付けなくなると、両方とも失敗します。 - TypeScript の診断はチャットまたは引き継ぎでは簡潔に保ちます。アプリを修正するためにローカルで完全な出力を使用しますが、
tscの出力全体を貼り付けるのではなく、グループ化した根本原因と少数の代表的な診断のみを報告してください。 - レンダリングされるすべての値が、返された行のプロパティ、スキーマフィールド、メジャー、または決定論的な変換にたどれることを検証します。
- 変更したファイルで
row[0]、row[1]、row.orderedAt、row.orderDate、as unknown as、DisplayRow、<select、margin、rate、score、percent、%、* 100、.toFixedを検索します。位置指定の行、結果キーの推測、エンティティの<select>フィルター、サポートされていないビジネスフィールドの解釈を修正してください。 - すべての日付プリセットバーに、明示的に省略されていない限り Custom last が含まれていること、表示されているすべての日付フィルターが現在のページに影響すること、1 つのスコープに対して重複した日付フィルターを表示するページがないことを検証します。
data_app.yamlがビルド済みバンドルのパスを指しており、そのバンドルパスが git で追跡されていることを確認します。- 表示されているすべてのフィルターについて、「All」がフィルターなしにマッピングされること、選択された値が実行時のクエリ結果から取得されること、非 All の各オプションが影響を与えると主張するすべてのカードを変更することを検証します。
よくある間違い
- アプリ構築中に Metabase のコンテンツを作成したり検索したりすること。アプリのコレクションのファイルは
npm run print-resourcesから書き出されるものであり、Metabase 内で作成されることはありません。 - リポジトリ内の Metabase YAML(そのエクスポート、トップレベルの
actions/、または git 履歴)や、自身の API 呼び出しからメトリクスやアクションをコピーすること。npm run print-resourcesからコピーしてください。 - 保存済みクエリを自分で書いたり、print に存在しないキーを追加したり、出力されたエンティティ内でアプリのコピーへのメトリクス参照以外の何かを変更したりすること。
- クエリオブジェクトを
queries/からdefineQueryでエクスポートするのではなくフック呼び出しの箇所で書いたり、アクションをactions/からdefineActionで取得するのではなくuseActionの箇所で書いたりすること。どちらも現在はコンパイルエラーです。修正すべきはディレクトリであり、キャストではありません。 - 呼び出し箇所でインラインオブジェクトを
defineQuery(...)やdefineAction(...)でラップすること。コンパイルは通りますが、保存済みクエリに支えられていないため、本番環境では拒否されます。 - 定義を
src/queries/やsrc/actions/の下に置くこと。CLI はそこを決して参照しません。 useMetabaseQueryではなく古いフックをインポートすること。- 生成されたスキーマオブジェクトを使う代わりに、生の数値 ID を定数にコピーすること。
{ name: "count" }や{ name: "sum", field: fieldId }のようなアドホックなメジャーオブジェクトをでっち上げること。- テーブルフィールドに生の文字列を渡すこと。
- キー付きの生成スキーマオブジェクトを使う代わりに、ルックアップヘルパーを追加すること。
- 生成されたテーブルクエリに
queryを使う代わりに、SDK コンポーネントの prop 名をでっち上げること。 - 無関係なテーブルのフィールド、セグメント、メジャーを混在させること。
- 結果カラムのみが解決される動的な第 2 引数に、セグメントやメジャーを渡すこと。
- フィルターを省略する代わりに空の値を送信するフィルター UI を追加すること。
- Metabase から実行時の値をクエリする代わりに、カテゴリフィルターの値をハードコードすること。
- 安定した ID が利用可能なのに、エンティティ名を表示しつつその名前でフィルタリングすること。
- 関連するチャートやテーブルが無視する中で、ダッシュボードレベルのフィルターを 1 つの KPI にのみ適用すること。
- 一方の日付フィルターが効果を持たないのに、グローバルの Date Range とページ固有の Snapshot Date を併せて表示すること。
- 違いを説明することなく、KPI とその詳細テーブルに異なる日付またはカテゴリのフィルターを使わせること。
Margin/Rate/Score/Healthを、でっち上げた%、星、色、しきい値でレンダリングすること。- Custom 範囲オプションのない日付プリセットバーを出すこと、または All time より前に Custom を置くこと。
- ユーザー向けの名前が利用可能なのに、
franchise_idのような不透明な ID をチャート化すること。 - 現在の実行時オプションリストが短くても、エンティティフィルターを素の
<select>でレンダリングすること。 - 日付範囲に
DateRangePopoverではなくネイティブの<input type="date">や任意の日付ピッカーの依存関係(react-datepicker、react-day-picker、UI スイートのピッカー)に手を伸ばし、ブラウザ制御のmm/dd/yyyyプレースホルダーやテーマ未適用のカレンダーポップオーバーを出荷すること。 - 日付トリガーのラベルを
useDateFormatter()ではなくnew Date("YYYY-MM-DD").toLocaleDateString()で付け、その結果グリニッジより西のユーザーにとってラベルが 1 日早くなること。 filter(...)が値の型を完全に検証すると仮定すること。nullのバケットを最新の時系列ポイントにすること。- ビジネス上の値、ラベル、タイムスタンプ、ランキングをハードコードすること。
- クエリされた
data.rowsから導出する代わりに、チャート用の配列を手作業で作成すること。 - 型付きの SDK 行を
[string, number]のような汎用タプル行にキャストすること。 - Metabase が
ordered_atのようなカラム名を返すのに、row.orderedAtのような生成オブジェクトのプロパティ名を参照すること。 - スキーマに存在しない、またはクエリ結果で返されないフィールドをレンダリングすること。
- SDK がまだ認証中または読み込み中なのに
No dataをレンダリングすること。 - アプリの境界で 1 つのプロバイダーを共有する代わりに、ネストされた
MetabaseProviderインスタンスを作成すること。