* initial proof-of-concept for data apps POC 2 POC 3 POC 4 Revert "POC 4" This reverts commit1f62e574dc. POC 5 POC 6 * Expose all SDK components to child realm * Setup app publishing and serving * Refactor distortion helpers * Adjust skill * Add useQuestionQuery SDK hook (#74900) Co-authored-by: Timofei <timofei@metabase.com> * Mock METABASE_EMBEDDING_SDK_BUNDLE for data app sdk hooks/components * Adjust skill for TS project * Update skill to clearly mention TS * Add blocked APIs list to skill * Prototype schema exports and typed hooks for custom data apps (#74978) * add prototype for schema generation * add formatting for generated schemas * add skill file * useQuestionQuery should take only ID based on @sanex3339 feedback * add segments, measures, tables and useMetabaseQuery hook * Add useMetabaseQuery to endowments * Fix queryDataset --------- Co-authored-by: Timofei <timofei@metabase.com> * Add ability to update app bundle * Simplify hook * Add spinner instruction * Mode Data Apps to OSS * Instruct to not not use blocked event handlers * Improve API, add styles isolation, add router components (#75418) * Prepare skills for data app template + adjust router components (#75490) * Fixes after rebase * Move migration to v63 * Move migration to v63 #2 * Fix config.edn * Fix BE tests * Fix team name * Fix duplicated config * Adjust version text in skill * Fix data app bundle uploading after legacy client removal * Remove `actions` from config edn * Fix failing test * Fix failing test #2 * Add custom actions for data apps (#75627) * Fix imports * Fix type annotations * Adjust migration * Adjust permissions * Fixed iframe security * Data Apps publishing via remote repository POC (#75702) * Update gitignore with data-app.html * [PoC] Type safety and modularity improvements for custom data apps (#75381) * allow separate libraries in schemas * add metric type checking * add collections parameter * check that segments and measures belongs to table in useMetabaseQuery * require less type information for measures * require less type information for segments * use metric dataset endpoint when querying measures in metrics * add validation for breakout buckets * fix filter operation type scoping * expose filter type helper for nicer autocompletions * update skill file * add breakout schema helper for better type errors * update breakout schema helper to accept binning and bucket * move fields that are not used in compile and runtime to comments to shorten type errors * enforce data truthfulness in skill * enforce semantic layer richness in skill * handle dimension key collisions mapped from many tables * update skill description * prevent hallucinated measures and breakouts * fix field serialization * add guidance on using filter matcher * add skill guidance on long results and units * accept query object in StaticQuestion and InteractiveQuestion * extract use-metabase-query to multiple files * update skill file to use sdk question by default * update skill file to avoid hallucinated derived calculations * add new functions into sandbox * kondo and cljfmt lint fixes * update CSP rules to allow style tags for data apps * address review feedback on scoping and metric filters * address code review feedback * Add aggregations support for breakouts in query hooks (#75723) * add aggregations support * add additional aggregation helpers * Add metrics support in useMetabaseQueryObject hook (#75728) * add metric queries * indicate metric support in skill file for useMetabaseQueryObject * add new version for release * fix query dataset tests * update release workflow temporarily * update skill description * update skill to use more generic examples * hotfix: allow esm usage in vite dev mode * skill file update for table caveat * update sandbox * fix type errors in useMetabaseQuery * add v0.63.2 to release workflow * update skill file to use the 63-data-apps version * Move data apps to the EE directory and under `data-apps` token feature (#75871) * remove skill file alias * Add error handling for data apps * Add spinner while iframe is loading * Add OSS/no token upsell banner placeholder for data apps (#75893) * ensure a fixed height is set if container has height * Allow including everything in data and metric library in schema endpoint (#75944) * add filter for including library and data collections * address review on skill file * remove id field from rendered comment * remove useQuestionQuery and useMetricQuery hook * Improve env files lookup in skills (#75955) * Allow `unsafe-eval` for data app iframe (#76004) * Fix lint * Fixes after rebase * Fix duplicated test * Improve Providers and typesafety (#75934) * chore: reformat single expression functions to arrow * Use entity id in questionCollections when generating typed schemas (#76230) * allow using entity ids for question collection filter * guide the agent towards using entity ids * add failing test for invalid entity id * Make custom viz work in data apps (#76237) * Allow blob: images in data-app CSP (EMB-1923) SDK custom visualizations load their icon assets via blob: URLs (to avoid the cross-origin auth issue), but the /embed/data-app/ route allowed 'unsafe-eval' without the blob: scheme in img-src, so icons were blocked under a production CSP. Add blob: to img-src scoped to the data-app iframe route only; the main app img-src stays strict. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * EMB-1880: support for `providerProps` in data apps --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Reduce schema bloat in dimensions of generated metrics when many tables are involved (#76003) * Add useQuestionQuery SDK hook (#74900) Co-authored-by: Timofei <timofei@metabase.com> * Prototype schema exports and typed hooks for custom data apps (#74978) * add prototype for schema generation * add formatting for generated schemas * add skill file * useQuestionQuery should take only ID based on @sanex3339 feedback * add segments, measures, tables and useMetabaseQuery hook * Add useMetabaseQuery to endowments * Fix queryDataset --------- Co-authored-by: Timofei <timofei@metabase.com> * [PoC] Type safety and modularity improvements for custom data apps (#75381) * allow separate libraries in schemas * add metric type checking * add collections parameter * check that segments and measures belongs to table in useMetabaseQuery * require less type information for measures * require less type information for segments * use metric dataset endpoint when querying measures in metrics * add validation for breakout buckets * fix filter operation type scoping * expose filter type helper for nicer autocompletions * update skill file * add breakout schema helper for better type errors * update breakout schema helper to accept binning and bucket * move fields that are not used in compile and runtime to comments to shorten type errors * enforce data truthfulness in skill * enforce semantic layer richness in skill * handle dimension key collisions mapped from many tables * update skill description * prevent hallucinated measures and breakouts * fix field serialization * add guidance on using filter matcher * add skill guidance on long results and units * accept query object in StaticQuestion and InteractiveQuestion * extract use-metabase-query to multiple files * update skill file to use sdk question by default * update skill file to avoid hallucinated derived calculations * add new functions into sandbox * kondo and cljfmt lint fixes * update CSP rules to allow style tags for data apps * address review feedback on scoping and metric filters * address code review feedback * Add aggregations support for breakouts in query hooks (#75723) * add aggregations support * add additional aggregation helpers * Add metrics support in useMetabaseQueryObject hook (#75728) * add metric queries * indicate metric support in skill file for useMetabaseQueryObject * add new version for release * fix query dataset tests * remove useQuestionQuery and useMetricQuery hook * Data Apps publishing via remote repository POC (#75702) * avoid duplicating dimension fields namespace dimensions by table add dimension id to mapping pick from table fields for only allowed dimensions fix incorrect type guards allow querying metric using question as source remove createMetabaseQuery from public interface add sourceFieldId to metric * doc generation type fixes --------- Co-authored-by: Timofei <timofei@metabase.com> * Add allowed hosts handling for data apps (#76094) * Update version * Properly include data app changes in `pull changes` modal (#76294) * Move app template into main repo, handle skill versioning flow (#76288) * feat(data-apps): allow passing down a card with viz settings (#75993) support card prop in sdk question component * Rename data app env variables and read them only in dev mode (#76604) * Add custom data apps layout with sliding toolbar (#76600) * Remove unused helper * Dev env Sandboxing, Human Readable Errors and Copy Helper (#76597) * Revert wrong rebase changes * Improve creating filtering interfaces in custom data apps (#76401) * update skill files to guide filter usages * update skill file to prevent filtering mistakes * move filtering ui patterns into a separate file * do not treat mb_replace_me as valid creds * use combobox for filters * pass title false to sdk question * combobox trigger before typing * Add missing exported types * Apply visualizations and visualization settings from SDK package (#76659) * ask the agent to pull possible visualizations * ask the agent to use card prop * teach the agent when to use viz settings * Support query-only SDK cards * Remove cross references between data apps skills (#76661) Remove data app skill cross references * Move Data App Dev Vite App setup into SDK package (#76695) * Don't navigate on ad-hoc question chart clicks (#76751) * Fix sandboxing crashes and update skill file mistakes (#76858) Fix data app sandboxing and skill guidance * Block form submissions in data apps via CSP (#76882) * Fix SDK theming hover text colors in generated data apps (#77083) update guidance on colors * Remove SDK question query prop for data apps use cases (#77080) * Remove SDK question query prop * update tests to use card prop instead * Adopt Lib.createTestQuery for table sources when querying in data apps (#75957) * Add base metabase-lib query implementation * switch to Lib.createTestQuery DSL * Load query metadata at runtime for metabase-lib instead of hard-coding (#76874) * load query metadata dynamically * accept id-only source references * Wait for SDK login before resolving query metadata * refactor: use useAsyncFn to handle states * pass source-name to createTestQuery * Add metric aggregations for useMetabaseQuery via Lib.createTestQuery (#76947) * add metric sources for useMetabaseQuery via Lib.createTestQuery * use MetabaseQueryObject instead of opaque type marker * Fix metric schema and join source validation * add metric library filtering to skill files * make metric an aggregation expression instead of join sources * mark tableId as never in joined fields * export public MetabaseQueryObject type * use camelCase for source name and field id * Add initial data app e2e setup (#77101) * Add sorting and fields to useMetabaseQuery via Lib.createTestQuery (#76966) * add sorting, limiting and fields to useMetabaseQuery * refactor: cleanup sorting and aggregations * Fix metric order by validation * Support saved questions for useMetabaseQuery via Lib.createTestQuery (#77073) * add saved questions as useMetabaseQuery input * remove extra operators for saved cards * Add unit fe/be tests for data apps (#76979) * Fix lint * Use modern api in query-dataset.ts * Fix BG color * Fix execute-action tests * Update SDK-data-apps version * Move migrations to v64 folder * Adjust router components to use metabase/router * Allow opening a data app for any signed user * Rename data app skills (#77266) * Update DataAppLayout.tsx `go back` button logic * Harden data apps skills and action validation errors (#77261) * harden data apps skills and actions error validation * do not display models as questions in data apps * clarify action result types * Agents should report errors when models, actions or questions cannot be schema generated (#77260) * return an error if models or saved questions are invalid * return an error if actions are invalid * update skill file guidance on model generation * fix model boundary * Add data apps admin UI + routing/sandboxing/isolation e2e (#77271) * Rename data-apps paths to /apps (#77298) * Include all action-included models in the typed schema when a mutation is specified (#77291) * add includeModels=true flag to include all models * omit model columns and models without actions * remove extra metadata * Adjust admin UI for Data Apps (#77420) * Add more e2e/unit tests for Data Apps (#77524) * Update bun.lock * Fix errors after rebase * Parallelize dts rollup (#77663) * revert EMB-1840 measures support in metric dataset endpoint * chore: add data app module (#77705) * test ci * apply code review suggestion * Setup separate entries for client and node dev data app entries (#77687) * Fix type casts lint errors (#77696) * Adjust data apps setup + add proper analytics setup (#77604) * Extract common and scope helpers + tests for typed schema module (#77403) * refactor: extract common and query param helpers * refactor: use kebab-case for query params * refactor: cleanup schema mapping ci: fix module config * refactor: rename schema scope helpers to scope.clj * refactor: extract scope_test.clj * Extract typed schema rendering helpers to render.clj (#77703) * extract schema render function * move runtime compact test out * remove javascript and json endpoints from schema generator * refactor: cleanup javascript.clj and render.clj * Extract typed schema generators for tables and questions (#77729) * refactor: extract schema generator modules * remove question-details from model-schemas * fix: batch queries in table and question schema generator * remove N+1 in typed schema field table lookup * Extract typed schema generators for models and actions (#77739) * extract model schema generator * batch model action querying to avoid n+1 queries * Extract typed schema generator for metrics (#77806) * Extract typed schema generators for models and actions (#77739) * extract model schema generator * batch model action querying to avoid n+1 queries * extract metric schema generator * test: simplify fields in metric_test fixtures * refactor: clean up metric schema generation * remove n+1 from metric field dimension read * disable default temporal dimension breakout * surface errors if metric schema cannot be built * revert metrics/api.clj to master to remove outdated comments * Fix routes * Take a data app's slug from its directory name (#77688) * Merge data-apps specific migrations into one * ci: fix failing backend test and cljfmt lint * Add a beta badge in data apps admin page heading (#77877) marks the data app as beta * Revert "Merge data-apps specific migrations into one" This reverts commit383a5fa28d. * Revert "Revert "Merge data-apps specific migrations into one"" This reverts commit948d904b62. * ci: actionlint fix * Fix data apps route * Use getSubpathSafeUrl in attachIframeUrlMirror * Fix comment * Do not send apps with sync error for token check * Add data app listing sidebar section for non-admins (#77977) * show data apps in sidebar for non-admins * add app icon to data apps * use available=true to filter no sync error and enabled apps * fix navbar keys in data apps * test: use non-admin user to assert non-admin response * fix toucan query in data apps listing endpoint * re-export entity detail helpers and result column to metabot core * open data apps in new tab from the sidebar * Prevent race conditions in responses of useMetabaseQuery hook (#78023) prevent stale responses in useMetabaseQuery * ci: remove tools.util from module declaration * Merge migrations into a single one * reset release-embedding-sdk.yml to master --------- Co-authored-by: Phoomparin Mano <poom@metabase.com> Co-authored-by: Phoomparin Mano <phoomparin@gmail.com> Co-authored-by: Nicolò Pretto <info@npretto.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
202 lines
9.9 KiB
Clojure
202 lines
9.9 KiB
Clojure
(ns metabase-enterprise.data-apps.sync
|
|
"Materialize data apps from a synced repository snapshot.
|
|
|
|
Data apps ride the remote-sync import pipeline: whenever remote-sync pulls the
|
|
connected repository (manual \"Pull changes\", auto-import poll, or startup),
|
|
it calls [[sync-from-snapshot!]] with the just-imported snapshot. We discover
|
|
every `data_apps/<dir>/data_app.yaml` and materialize one `data_app` row per app
|
|
(caching the built bundle). A sync only upserts — it never deletes, so an app
|
|
whose directory is gone from the repo is kept until an admin removes it.
|
|
|
|
This namespace does no Git access of its own — the snapshot's `read-file` /
|
|
`list-files` come from remote-sync. The cached `bundle` blob is what serving
|
|
reads, so a failed sync never takes a working app offline, and the admin
|
|
`enabled` toggle is preserved across syncs."
|
|
(:require
|
|
[clojure.string :as str]
|
|
[metabase-enterprise.data-apps.config :as data-app.config]
|
|
[metabase.settings.core :as setting]
|
|
[metabase.util.i18n :refer [tru]]
|
|
[metabase.util.log :as log]
|
|
[toucan2.core :as t2])
|
|
(:import
|
|
(java.security MessageDigest)
|
|
(java.util.regex Pattern)
|
|
(org.apache.commons.codec.binary Hex)))
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
(def ^:const max-bundle-bytes
|
|
"Cap on a synced bundle's size (10 MiB)."
|
|
(* 10 1024 1024))
|
|
|
|
(defn- bytes-hash ^String [^bytes b]
|
|
(let [^MessageDigest md (MessageDigest/getInstance "SHA-256")]
|
|
(Hex/encodeHexString ^bytes (.digest md b))))
|
|
|
|
(defn- ->bytes ^bytes [^String s]
|
|
(.getBytes s "UTF-8"))
|
|
|
|
(defn repo-url
|
|
"The connected remote-sync repository URL, or nil when none is configured. Read
|
|
by keyword so this OSS namespace has no compile-time dependency on the
|
|
enterprise remote-sync module; returns nil when that module (and its setting)
|
|
isn't loaded."
|
|
[]
|
|
(let [url (try (setting/get :remote-sync-url) (catch Throwable _ nil))]
|
|
(when-not (str/blank? url)
|
|
url)))
|
|
|
|
;;; ----------------------------------------------------- Discovery -----------------------------------------------------
|
|
|
|
(def ^:private app-dir-regex
|
|
;; the data_apps/<dir> folder a repo path sits in (dir = a single path segment)
|
|
(re-pattern (format "(%s/[^/]+)/.*" (Pattern/quote data-app.config/apps-dir))))
|
|
|
|
(defn- discover-app-configs
|
|
"Given the snapshot's `list-files` and `read-file` fns (where `read-file`
|
|
returns file text or nil), iterate the folders under `data_apps/` and return one
|
|
entry per folder that is an app — it holds a `data_app.yaml`; a folder without
|
|
one is skipped. Each entry is a parsed app
|
|
`{:slug :display_name :bundle :allowed_hosts}` (with `:bundle` the repo-root
|
|
relative bundle path) or `{:config-error <message>}`. Parse/read failures are
|
|
isolated per app so one bad config can't abort the sync."
|
|
[list-files read-file]
|
|
(let [files (set (list-files))
|
|
app-dirs (distinct (keep #(second (re-matches app-dir-regex %)) files))]
|
|
(for [dir app-dirs
|
|
:let [config-path (files (str dir "/data_app.yaml"))]
|
|
:when config-path]
|
|
(try
|
|
(if-let [content (read-file config-path)]
|
|
(let [{:keys [slug display_name path allowed_hosts]} (data-app.config/parse-app-config (->bytes content) dir)]
|
|
{:slug slug, :display_name display_name, :bundle (str dir "/" path), :allowed_hosts allowed_hosts})
|
|
{:config-error (tru "Could not read {0}." config-path)})
|
|
(catch Throwable e
|
|
{:config-error (ex-message e)})))))
|
|
|
|
;;; ----------------------------------------------------- Materialize -----------------------------------------------------
|
|
|
|
(defn- upsert-by-name!
|
|
"Insert or update by slug. Never writes `:enabled`, so the admin toggle (and the
|
|
DB default of true for new rows) is preserved across syncs."
|
|
[slug row]
|
|
(if (t2/exists? :model/DataApp :name slug)
|
|
(t2/update! :model/DataApp :name slug row)
|
|
(t2/insert! :model/DataApp (assoc row :name slug))))
|
|
|
|
(defn- app-content-changed?
|
|
"Whether the just-synced content differs from the `existing` row (nil = a new
|
|
app). Compares only content-bearing fields — a `last_synced_sha` /
|
|
`last_synced_at` bump on an otherwise-identical app is NOT a change, so callers
|
|
can count real changes (e.g. for the remote-sync pull summary)."
|
|
[existing {:keys [display_name bundle_path bundle_hash allowed_hosts]}]
|
|
(or (nil? existing)
|
|
(some? (:sync_error existing))
|
|
(not= (:display_name existing) display_name)
|
|
(not= (:bundle_path existing) bundle_path)
|
|
(not= (:bundle_hash existing) bundle_hash)
|
|
(not= (vec (or (:allowed_hosts existing) []))
|
|
(vec (or allowed_hosts [])))))
|
|
|
|
(defn- sync-app!
|
|
"Materialize one app. On bundle failure, the row's metadata is still upserted
|
|
with `sync_error` set so the app appears in the list with its failure; the
|
|
previously cached bundle (if any) is kept. `existing` is the app's pre-sync row
|
|
(or nil); returns true when this sync actually changed the app's content (a new
|
|
app, differing bundle/metadata, or a new failure) so callers can count changes."
|
|
[existing {:keys [slug display_name bundle sha read-file allowed_hosts]}]
|
|
(try
|
|
(let [content (read-file bundle)
|
|
_ (when-not content
|
|
(throw (ex-info (tru "Bundle file \"{0}\" not found in the repository." bundle)
|
|
{:status-code 400})))
|
|
^bytes bytes (->bytes content)]
|
|
(when (> (alength bytes) max-bundle-bytes)
|
|
(throw (ex-info (tru "Bundle for \"{0}\" must be less than {1} MiB."
|
|
slug (quot max-bundle-bytes (* 1024 1024)))
|
|
{:status-code 413})))
|
|
(let [fields {:display_name display_name
|
|
:allowed_hosts allowed_hosts
|
|
:bundle_path bundle
|
|
:bundle_hash (bytes-hash bytes)}]
|
|
(upsert-by-name! slug (assoc fields
|
|
:bundle bytes
|
|
:last_synced_sha sha
|
|
:last_synced_at :%now
|
|
:sync_error nil))
|
|
(app-content-changed? existing fields)))
|
|
(catch Throwable e
|
|
(upsert-by-name! slug {:display_name display_name
|
|
:allowed_hosts allowed_hosts
|
|
:bundle_path bundle
|
|
:sync_error (ex-message e)})
|
|
(log/warnf e "[data-app] failed to sync app %s" slug)
|
|
;; a failing app counts as a change unless it was already failing identically
|
|
(or (nil? existing) (not= (:sync_error existing) (ex-message e))))))
|
|
|
|
(defn import-from-snapshot!
|
|
"Materialize data apps from a synced repo `snapshot`:
|
|
|
|
{:read-file (fn [path] -> <file-text-string> | nil)
|
|
:list-files (fn [] -> [<path-string> ...])
|
|
:sha <commit-sha-string>}
|
|
|
|
Discovers every `data_apps/<dir>/data_app.yaml` and upserts a row per app in a
|
|
single transaction. Apps not present in the snapshot are left untouched (a
|
|
sync never deletes — removal is an explicit admin action). Returns
|
|
`{:synced <n>, :changed <n>, :sha <sha>, :config-errors [<message> ...]}`,
|
|
where `:changed` is how many apps this sync actually created/updated
|
|
(a `last_synced_sha` bump on an unchanged app does not count). A malformed
|
|
`data_app.yaml` is isolated (collected into `:config-errors`, that app skipped)
|
|
rather than aborting the others; per-app bundle failures are recorded on the
|
|
row.
|
|
|
|
Two apps can't collide on a slug here: a slug *is* an app's directory name (see
|
|
the config namespace), a repo can't hold two `data_apps/<slug>` directories, and
|
|
discovery takes one config per directory (see [[discover-app-configs]])."
|
|
[{:keys [read-file list-files sha]}]
|
|
;; realize discovery once (it calls read-file/parse per config); `results` is
|
|
;; then walked several times below
|
|
(let [results (vec (discover-app-configs list-files read-file))
|
|
good (filter :slug results)
|
|
errors (vec (keep :config-error results))
|
|
;; pre-sync rows, so we can tell a real change from a sha/timestamp bump
|
|
existing (into {} (map (juxt :name identity))
|
|
(t2/select [:model/DataApp :name :display_name :allowed_hosts
|
|
:bundle_path :bundle_hash :sync_error]))
|
|
changed
|
|
;; Upsert-only: a sync never deletes. Apps materialized from a previous
|
|
;; repo are kept (they survive unlinking, and switching repos), and an
|
|
;; app whose directory name matches an existing app's is overridden in
|
|
;; place by `upsert-by-name!`. Rows are removed only by an explicit admin
|
|
;; action (see the API's DELETE).
|
|
(t2/with-transaction [_conn]
|
|
(reduce (fn [n cfg]
|
|
(cond-> n
|
|
(sync-app! (get existing (:slug cfg))
|
|
(assoc cfg :sha sha :read-file read-file))
|
|
inc))
|
|
0 good))]
|
|
(log/infof "[data-app] synced sha=%s apps=%d changed=%d errors=%d"
|
|
sha (count good) changed (count errors))
|
|
{:synced (count good), :changed changed, :sha sha, :config-errors errors}))
|
|
|
|
(defn sync-from-snapshot!
|
|
"Entry point for the remote-sync import pipeline. Materializes data apps from
|
|
the just-imported `snapshot` (see [[import-from-snapshot!]] for its shape).
|
|
Never throws, so it can't break the surrounding remote-sync import: a thrown
|
|
failure is logged and swallowed, and a malformed `data_app.yaml` is logged and
|
|
its app simply doesn't appear. Returns the [[import-from-snapshot!]] result, or
|
|
nil if the sync threw."
|
|
[snapshot]
|
|
(try
|
|
(let [{:keys [config-errors] :as result} (import-from-snapshot! snapshot)]
|
|
(when (seq config-errors)
|
|
(log/warnf "[data-app] %d data_app.yaml config(s) skipped: %s"
|
|
(count config-errors) (str/join "; " config-errors)))
|
|
result)
|
|
(catch Throwable e
|
|
(log/warn e "[data-app] sync from remote-sync snapshot failed")
|
|
nil)))
|