Compare commits

..

154 Commits

Author SHA1 Message Date
ChrisVandoo
f958779a56 TESTING 2026-07-27 08:16:39 -04:00
ChrisVandoo
ccf9960b97 set backport pr to draft if there are conflicts 2026-07-24 12:36:23 -04:00
Ben Grabow
1c2e735ab0 GHY-4103 Repair stale template tags on serdes import (#77897) 2026-07-24 11:15:35 -05:00
Mahatthana (Kelvin) Nomsawadi
bb6977debd feat: simplify SDK release process and checklist maintenance (#77537)
* scaffold: two-step SDK release process (EMB-1518)

Replaces the hardcoded version/dist-tag computation in
release-embedding-sdk.yml with a new sdkRelease metadata block in
package.template.json (distTag, tagAsLatest), plus a new
sdk-version-bump-pr.yml workflow that bumps the version via `npm version`
and opens a PR labeled sdk-release-trigger for review. Merging that PR
triggers the release workflow directly.

Unverified: scaffolded per EMB-1518 ahead of Nicolo/Roman's review, since
this can only really be exercised by triggering a real SDK release. See
.claude/kelvin/2026-07-09-emb-1518-simplify-sdk-release-process-and-checklist-maintenance/
for the plan and open questions (particularly around distTag semantics for
stable/patch/minor releases).

* fix: correct distTag comment, <major>-stable is real (EMB-1518)

npm dist-tag ls @metabase/embedding-sdk-react confirms 51-stable
through 62-stable are already live, actively-used tags - not a new
concept as the prior comment assumed. Resolves the one open question
this scaffold couldn't settle from the proposal text alone.

* fix: address review cycle 1 findings (EMB-1518)

Fixes release-breaking and correctness issues found in review cycle 1
of the SDK release process scaffold:

- beta release_type now defaults to <major>-beta dist-tag instead of
  the literal (non-existent) "beta" tag
- validate branch/release_type combinations that the old
  getNextSdkVersion used to hard-error on (master requires alpha,
  release branches reject alpha)
- pull_request trigger on release-embedding-sdk.yml now scoped to
  package.template.json via a paths filter, and the concurrency group
  no longer cancels an in-flight release on unrelated PR closures
- readme-PR follow-up steps now skip cleanly when the readme was
  already up to date, instead of failing and firing a false alert
- tagAsLatest now always resets to false rather than carrying forward
  a prior true, and the actual value is shown in the summary/PR body
- version-bump branch name includes the run id to avoid collisions on
  retry
- renamed a step whose name didn't match what it does

See the "Review cycle 1 (fable)" section and the new "Fixes applied
for review cycle 1" subsection in the plan file for full detail on
each finding and its disposition.

* fix: address review cycle 2 findings + simplify version bump (EMB-1518)

Cycle-2 findings: add a preminor release_type so master's next-major
alpha bump no longer needs a hand-made PR (#9); make the Slack
failure notification independent of an artifact upload that may
never have run (#10); guard against two open version-bump PRs racing
each other (#11); stop interpolating the free-text branch input
directly into shell scripts (#12).

Also replaces the npm-version rename dance (rename to package.json,
bump, rename back, plus a .bak copy) with direct calls to the semver
package the bump logic already wraps, writing the result via jq
instead - no functional change, removes the dead .bak file (#13).

* fix: address review cycle 3 findings + rename resolve-release job (EMB-1518)

Fixes findings #14-#16 from the third fable review cycle:
- #14: the failure Slack alert's messageId interpolation was unquoted,
  rendering a JS syntax error whenever send-slack-notification-start was
  skipped (the exact scenario cycle-2's fix #10 was meant to cover). Now
  quoted, and falls back to sendSlackMessage (new top-level message)
  instead of sendSlackReply when there's no thread to reply to.
- #15: finished the cycle-2 injection-hardening pass by routing the two
  remaining inline branch-value usages (workflow-summary's heredoc,
  publish-npm's gh pr create --base) through env: instead of ${{ }}.
- #16: added a semver validity pre-check before the bump step, so an old
  release branch's 0.N.x placeholder version fails with a clear ::error::
  instead of a silent exit 1 from the semver CLI.

#17 (minor's version-collision caveat on old release branches) is left as
an open question for Nicolo/Roman rather than removed - documented with a
comment and Judgment call #6. #18 (merge-commit-sha race) is deferred -
it fails loudly rather than silently, lower priority than the others.

Also renames the resolve-release job to prepare-release throughout
release-embedding-sdk.yml, per explicit request (not a review finding).

See the plan file's "Fixes applied for review cycle 3" section for the
full verification detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* feat: auto-compute tagAsLatest from vars.LATEST_VERSION (EMB-1518)

Resolves Judgment call #2: the SDK version-bump workflow previously
always defaulted tagAsLatest to false because there was no reliable
way to know if a branch is the current "latest" major. The repo
already exposes vars.LATEST_VERSION (major version of the most recent
gold/stable release) for exactly this, so patch/minor bumps now set
tagAsLatest=true when MAJOR_VERSION matches it, false otherwise
(including when the variable is unset). alpha/beta/preminor stay
false. The PR review gate still allows overriding the computed value
before merging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* refactor: use github.ref_name instead of inputs.branch (EMB-1518)

github.ref_name is already set by GitHub's "Use workflow from" ref
picker on every workflow_dispatch run, so the custom inputs.branch
free-text field was duplicating a selection that was already made -
and could silently diverge from it. Switch every functional read of
inputs.branch to github.ref_name in both release workflows; keep the
branch input field itself, but make it explicitly non-functional
(required: false, no default, description points at the ref picker).

type: choice was considered and rejected again here: it would close
the typo risk but can't accept an unregistered branch, breaking
one-off custom-branch releases without editing the workflow file per
release - exactly the maintenance burden EMB-1518 removes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* fix: remove dead readme-update mechanism (EMB-1518)

update_readme's two sed patterns match zero occurrences in the current
README (restructured by 15f7d3d20e, no version-pinned content remains),
so publish-npm's readme-follow-up-PR step has been a silent no-op on
every release, not just re-runs. Deleted the entire dead block (7 steps,
the env vars and pull-requests:write permission it alone used) and the
now-unused update_readme function.

* fix: drop minor release_type option (EMB-1518)

`semver -i minor` on a release branch jumps to the next major's
version line, colliding with that major's own branch. `patch` already
covers graduation and `preminor` already covers the legitimate
next-major jump (scoped to master). Drop `minor` outright rather than
add compensating validation - resolves review-cycle-3 finding #17 /
Judgment call #6.

* fix: add explicit reviewer request for embedding team on bump PR (EMB-1518)

CODEOWNERS is deliberately stripped on release branches (a repo-wide
practice to cut auto-backport review noise, unrelated to this
workflow), so the bump PR's "code owner review is automatic" comment
was false there - it got zero reviewers requested on release
branches. EMB-1000 documented the identical gap in the old system's
follow-up PR. Fix targets the metabase/embedding team (not a named
individual - the old system's hardcoded reviewer has since left).

* fix: restrict release types by branch, add custom releases (EMB-1518)

A one-off branch could publish as the stable release. Validation only
guarded master and release-x.*, so `patch` on e.g. sdk-data-apps stripped
the prerelease id (0.62.5-data-apps.0 -> 0.62.5), computed the
<major>-stable dist-tag, and - when the major matched vars.LATEST_VERSION
- set tagAsLatest, shipping a feature branch to npm as the stable release.
Only check-git-tag stood in the way, and only if that version had already
shipped.

Each branch class now gets only the release types that make sense for it:
master takes alpha/preminor, release branches take beta/patch, any other
branch takes custom. tagAsLatest is additionally gated on a release-x.*
branch, so nothing published from master or a one-off branch can take the
npm `latest` tag.

The custom type keeps one-off branches (esbuild, data-apps) releasable
under that restriction: prerelease_id is set on the first cut and becomes
both the version's prerelease id and the <major>-<id> dist-tag, matching
the shape already live on npm (56-esbuild, 63-data-apps). Later bumps
leave it empty and semver reuses the id from the committed version.

Also drops the dead release_utils.bash (zero functions, zero callers),
replaces both free-text dispatch inputs with single-option choices that
cannot be typed into, points the release workflow's form at the bump PR,
and removes the scaffold banner comments.

* fix: read vars.LATEST_MAJOR_VERSION, not LATEST_VERSION (EMB-1518)

The variable is LATEST_MAJOR_VERSION - it is what release.yml already
reads to decide Docker's `latest` tag, and what the release checklist has
a github admin bump each cycle. `vars.LATEST_VERSION` does not exist, so
tagAsLatest would have computed false on every bump, including at gold,
without any error - the fallback for an unset variable is false.

* refactor: make the release concurrency group readable (EMB-1518)

Same three cases as before, but each arm now names what it produces
(sdk-release-<branch> for a dispatch or a merged bump PR, sdk-release-noop-<run id>
for a closed PR this workflow ignores), and the comment above maps them in
the order the expression tries them.

The noop arm is not decoration: concurrency is evaluated before any job's
`if:` runs, so a closed PR that prepare-release skips would still enter the
release group without it. cancel-in-progress stays false so a release is
never interrupted mid-publish.

Groups are prefixed rather than bare because concurrency groups are
repo-wide, not per-workflow.

* refactor: build the release scripts once, in their own job (EMB-1518)

The Slack jobs each got at release/dist/index.cjs a different way:
send-slack-notification-start checked out and built it (then uploaded it as
an artifact), send-slack-notification-failure built its own second copy, and
send-slack-notification-successful downloaded the artifact. The duplicate
build existed because the failure job has to fire even when the metadata read
fails - in which case the start job never ran, so its artifact never existed.

A build-release-scripts job now builds it once and uploads it, and all three
Slack jobs download it. The job hangs off prepare-release only, so the
artifact is there no matter how early the release fails - which removes the
reason the second build existed.

* fix: quote the Slack messageId in the success notification (EMB-1518)

A Slack ts is a string. Interpolated bare it becomes a JS number literal and
drops trailing zeros - 1783602196.214000 renders as 1783602196.214, and the
reply targets a thread that does not exist. Pre-existing on master; the
failure job already quoted it, so this makes the two consistent.

* refactor: resolve the release branch in one expression (EMB-1518)

The env: BRANCH indirection was injection hardening from when the value was
the free-text `inputs.branch`. It is github.ref_name now - a real branch
name, same as github.event.pull_request.base.ref, which the next line already
inlined - so it guarded nothing and made the step asymmetric.

base.ref is empty on a dispatch, so `base.ref || ref_name` picks the right one
without branching on github.event_name.

* refactor: drop the duplicate-bump-PR pre-flight (EMB-1518)

It guarded a case the design already handles safely. Two open bump PRs
against the same branch both compute from the branch's committed version, so
both propose the same next version: the first to merge publishes and tags it,
and the second dies at check-git-tag with "Tag ... already exists" (npm would
reject the republish too). Nothing is published twice, and the error says
exactly what happened.

Getting there needs someone to dispatch the bump twice and leave the first PR
open, with both PRs sitting labelled in the list. That is not worth an API
call and 16 lines on every bump.

* chore: trim the bump step's comments (EMB-1518)

* refactor: drop the semver validity pre-check (EMB-1518)

Every supported release branch (57 through 63) already carries a valid
semver, and the new process never writes a `0.N.x` placeholder - gold is a
patch bump (0.63.0-beta.3 -> 0.63.0). The placeholder only ever existed
transiently under the old checklist, which rewrote it back to a concrete
version after each release.

Also saves an npx invocation on every bump.

* chore: spell out PRERELEASE_ID in the bump workflow (EMB-1518)

* refactor: spell out the semver call in each release type (EMB-1518)

alpha and beta shared an arm that passed --preid="$RELEASE_TYPE", and patch
passed -i "$RELEASE_TYPE" - you had to know the variable's value to know what
command ran. Each arm now names its own call.

The release_type description now leads with the phase (before gold, at gold,
after gold), which is what the person dispatching actually knows. It also
corrects preminor: master moves to the next major *after gold*, not at branch
cut - the 62 cycle did it in the after-gold checklist (#75487, 0.62.0-alpha ->
0.63.0-alpha), and 61 before it (#74279).

* docs: stop calling the bump PR a review gate (EMB-1518)

Nothing blocks on approval: the bump PR is not auto-merged but it is not
gated on a reviewer either, and workflow_dispatch can publish with no PR at
all. The computed distTag/tagAsLatest are defaults made visible and editable
- whoever dispatched the bump has already decided.

* docs: note EMB-2095 (auto-merge the bump PR) in the workflow (EMB-1518)

* docs: tighten the tagAsLatest comment (EMB-1518)

* refactor: collapse the tagAsLatest case into one condition (EMB-1518)

* docs: say what the sdkRelease write does (EMB-1518)

* refactor: drop the label-creation step (EMB-1518)

The sdk-release-trigger label is repo-level, so it only ever needed creating
once - it now exists. Re-running `gh label create ... || true` on every bump
swallowed every error (including auth) to no purpose.

* fix: fail on a leftover bump branch instead of side-stepping it (EMB-1518)

The run-id suffix made every retry a fresh branch, so an abandoned bump left
a branch on origin forever and the name was unpredictable. The branch is now
named after the version, and an existing one is an error: an earlier bump to
the same version was never merged, which is worth knowing before opening a
second PR alongside it.

* refactor: drop the no-backport label step (EMB-1518)

Backporting is opt-in - auto-backport.yml only fires on a `backport` label -
and backport-reminder.yml only demands a decision label when a PR touches
docs/**. The bump PR touches one JSON file, so no-backport guarded nothing.

Keeps the reviewer rationale, moved next to the --reviewer flag it explains.

* feat: show the target branch and computed values in the bump PR body (EMB-1518)

The PR body did not say which branch it targeted, which is the first thing
you want to know when a release PR shows up. Branch, release type, version,
dist-tag and tagAsLatest are now a table at the top.

* fix: pin semver and validate prerelease_id (EMB-1518)

npx --yes semver resolved whatever the registry served at dispatch time, on
a job that installs no dependencies - so release-critical version math could
change under us while package.json already pins ^7.6.3. Pinned to semver@7.

prerelease_id was passed to semver unchecked. An id like `DataApps` bumped
fine and then died three steps later in the dist-tag read-back, whose regex
only matches [a-z][a-z0-9-]*, with an error blaming the computed version
instead of the input. It is now rejected up front, with a message naming the
rule.

* fix: take semver's version from package.json (EMB-1518)

Pinning the workflow to semver@7 fixed the drift but created a second
version to keep in step. Read the range the repo already pins instead
(devDependencies.semver, currently ^7.6.3) and hand it to npx, so bumping
semver in package.json is the only place it happens.

* refactor: rename bump workflow input to read_first (EMB-1518)

* feat: reject prerelease_id on non-custom release types (EMB-1518)

* refactor: drop the preminor bump-only warning from the PR body (EMB-1518)

* refactor: drop the duplicate rerun wording in tag-exists error (EMB-1518)

* fix: reject reserved alpha/beta as a custom prerelease_id (EMB-1518)

* refactor: tidy and clarify comments in the SDK bump workflow (EMB-1518)

* fix: satisfy actionlint in the SDK release workflow (EMB-1518)

* feat: extract embedding SDK release logic into a tested helper (EMB-1518)

* refactor: remove dead embedding SDK helpers from version-helpers (EMB-1518)

* fix: drop trailing blank line in the SDK bump workflow (EMB-1518)

* refactor: run the SDK version bump from a tested helper, not inline bash (EMB-1518)

* docs: note the env block guards github-script against injection (EMB-1518)

* refactor: drop the unused LATEST_MAJOR_VERSION fallback in the bump script (EMB-1518)

* feat: show the target branch in the bump PR title (EMB-1518)

* docs: drop the manual-edit invitation from the bump PR body (EMB-1518)

* refactor: tidy the SDK release helper and its tests (EMB-1518)

* refactor: read the SDK release metadata via the tested helper (EMB-1518)

* refactor: env-route the branch name in the bump PR title (EMB-1518)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_0156DAaukfiU6xceXd3brbZN

* refactor: simplify SDK release concurrency group (EMB-1518)

Collapse the three-case concurrency expression to one per-branch group, and
switch every multi-item needs array to a block sequence.

The dropped noop group guarded a compound case: GitHub keeps one pending run
per group and cancels any earlier one, so a closed PR touching the template
could evict a release waiting to start. That needs two same-branch releases
in flight at once, so we take the simpler expression. The clean fix is
queue: max, which actionlint cannot parse yet.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:34:19 +01:00
Riley Thompson
a08f7ff918 Show confirmation modal when uploading CSV to model with duplicates (#77636)
* show warning modal

* address pr comments
2026-07-24 13:37:47 +00:00
github-automation-metabase
fdace56d22 Deflake transform revision history spec (#78272)
* Deflake transform revision history spec

The "view and revert transform revisions" test changed the transform
name twice, each firing a "Transform name updated" success toast that
lives for the default 5s undo timeout. Unlike every other update flow in
this spec, these toasts were never dismissed. On a fast runner the test
reached the failed-revert error toast within that window, so a lingering
success toast coexisted with it and H.undoToast() (a single-element
findByTestId on [data-testid="toast-undo"]) failed with "Found multiple
elements".

Dismiss each success toast right after it appears, matching the existing
convention used elsewhere in this file, so exactly one toast is present
when the error toast is asserted.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Nemanja <31325167+nemanjaglumac@users.noreply.github.com>
2026-07-24 14:55:37 +02:00
github-automation-metabase
8b52336966 Auto-update documentation for master (#78517)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-24 07:54:47 -04:00
Sébastien
0795d60b21 Extract a shared AdminDataTable for admin list tables (sortable + paginated) (#78136) 2026-07-24 12:35:17 +02:00
Timofey Kachalov
5fe045c2ab Add an endpoint that outputs Data App Dev diagnostic reports for an agent (#78358) 2026-07-24 13:23:00 +04:00
Chris Truter
1ba3a7c050 Add semantic search health checks (#78299)
* Add semantic search health checks

* Split HNSW recovery from semantic health

* Use HoneySQL literals for repair queries

* Use dynamic redefs in semantic search test
2026-07-24 10:24:15 +02:00
Ngoc Khuat
36fa06a4bd Serialize card last_used_at so imports don't reset it (#78338) 2026-07-24 06:44:37 +00:00
Chris Truter
a323e1c883 Add NLQ retrieval health checks (#78298) 2026-07-24 08:31:00 +02:00
Egor Grushin
a9731f6e65 style(sdk-bundle): Alert component redesign (#78260)
* redesign Alert, remove deprecated Alert component

* fix styling so far

* update alert stories

* additional changes

* small fix

* refactor Alert config

* address PR's comments

* chores

* Update Loki Snapshots

* addressing the designing feedback in places

* addressing the designing feedback in the Alert itself

* add loki tests

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-07-24 08:08:28 +02:00
Romeo Van Snick
a3e82fb537 Replace TreeTable getRowHref with a renderRowLink render prop (#78523) 2026-07-24 14:36:03 +10:00
Romeo Van Snick
14a65406f1 Fix dashcard #scrollTo auto-scroll (#78522)
* Auto-scroll to a dashcard whenever it becomes the scrollTo target, and keep the scroll until the grid settles

* Drop a stray brace from the auto-scroll spec's first visit URL

* Simplify the auto-scroll correction to a fixed frame window
2026-07-24 14:15:46 +10:00
Fraser
1591e74409 Fix module stats counting (#78184) 2026-07-24 15:53:41 +12:00
Romeo Van Snick
dee31666b9 Migrate transforms pages off withRouteProps (#78422)
* Migrate monitor tools off withRouteProps

* Migrate metric pages off withRouteProps

* Migrate JobInfoApp off withRouteProps

* Migrate transforms pages off withRouteProps
2026-07-24 13:51:17 +10:00
Romeo Van Snick
bd7af8355e Migrate monitor and metric pages off withRouteProps (#78421)
* Migrate monitor tools off withRouteProps

* Migrate metric pages off withRouteProps

* Migrate JobInfoApp off withRouteProps
2026-07-24 12:48:12 +10:00
Romeo Van Snick
24220fe191 Delete the react-router v3 engine (#78305)
* Delete the react-router v3 engine

* Sever the react-router package dependency

* Give the SDK data-app router its own browser history on v7

* Rename the react-router-v7 alias to react-router

* Rename getDataAppHistory to getRawBrowserHistory, drop dead getCurrentHistory
2026-07-24 08:56:17 +10:00
dpsutton
f35f7450a6 Scope migration existence preconditions to their table (~46s off every full pg migration) (#78403)
Scope existence preconditions to their table; lint against name-only checks

A foreignKeyConstraintExists precondition with only foreignKeyName (or
indexExists with only indexName) forces Liquibase to snapshot the entire
schema to answer the check -- ~2s per precondition on a large postgres
appdb. 27 such preconditions (24 FK, 3 index; 21 in one v59 workspace
block) added ~46s to every full migration: 50.2s of total changeset time,
of which 24 changesets cost 45.9s and the other 1,182 cost 3.7s.

Add the scoping key to all 27. Measured on a fresh postgres migration:
total changeset time drops 50.2s -> 5.1s, with zero changesets over 1s.

Editing released changesets' preconditions is safe: Liquibase checksums
cover the changes, not preConditions. Verified two ways -- ChangeSet
.generateCheckSum is byte-identical before/after for all 27 (v9
checksums), and a clone of an already-migrated appdb re-validates
cleanly against the edited files.

lint-migrations-file: generalize require-primary-key-exists-has-table-name
into require-table-scoped-existence-preconditions, covering
primaryKeyExists/indexExists/uniqueConstraintExists (tableName) and
foreignKeyConstraintExists (foreignKeyTableName), walking the full
precondition tree in both its flat-map and list forms.


Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:11:37 -05:00
Maksym Yakubych
089b82338c Make AI auditing Conversations table horizontally scrollable (#78378) 2026-07-23 16:06:15 -05:00
Oleksandr Yakushev
f63af2b18c [perf] Implement containsKey for SnakeHatingMap (#78097) 2026-07-24 00:04:13 +03:00
Riley Thompson
a37f29e737 Match remapped fields with ID or name based fields refs (#78246)
* apply fix

* add tests

* keep key structure the same

* update test

* improve fix

* fix bad field id refs test

* add sandboxing test
2026-07-23 21:01:26 +00:00
Chris Truter
8112036b95 Add shared search index health metrics (#78297)
* Add embedder circuit breaker and recovery

* Add embedder breaker edge-case tests

* Avoid noisy circuit-open embedding errors

* Add shared search index health metrics
2026-07-23 22:57:15 +02:00
Ethan McCue
dec394cf5e Allow config files to be named with a .yaml extension if no .yml file is found. (#77823)
* Fall back to .yaml if .yml not found

Update docs to mention `.yaml` support

* Fall back to .yaml if .yml not found

Update docs to mention `.yaml` support

* Update file.clj

* Fix tests

* Address feedback
2026-07-23 20:30:32 +00:00
Chris Truter
020b02932d Add embedder circuit breaker and recovery (#78296)
* Add embedder circuit breaker and recovery

* Add embedder breaker edge-case tests

* Avoid noisy circuit-open embedding errors
2026-07-23 22:04:03 +02:00
Alexander Polyankin
9fc9f53a1f entity_id is always present; drop the identity-hash machinery (#78452) 2026-07-23 19:29:42 +00:00
technomancy
9ccac4efed Drop field usage table (#78408)
* Drop field_usage table as it is no longer used.

* Stop saving preprocessed_query in metadata since it's not used any more.

This used to be used for saving off field usage, but that hasn't been
a thing for a very long time.
2026-07-23 18:46:00 +00:00
Ngoc Khuat
7b60d66387 Limit body-only dashboard subscription results (#78431) 2026-07-23 17:37:14 +00:00
AlessioLaiso
3360008053 Make model-illustration.svg work well in both light and dark mode (#68072)
* Make model-illustration.svg work well in both light and dark mode

* whitelabel svg

---------

Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
2026-07-23 14:36:37 -03:00
Andrei Chernykh
f24e4bed1f Migrate PinMap to a typed function component and extract a pure getPoints (#78067)
* Migrate PinMap to a typed function component and extract a pure getPoints

* Type the pin metric as nullable, derive MapSettings from VisualizationSettings, and add createMockVisualizationProps

* Trim comments, replace the map ref cast with an instanceof guard, and cover instance clearing
2026-07-23 10:13:44 -07:00
John Swanson
07b9ec8ffa Don't block dashboard saves on pre-existing foreign internal cards (#78186)
* Don't block dashboard saves on pre-existing foreign internal cards

UXW-4870: the guard added in #77236 (reject assigning a dashcard's card
to a question internal to another dashboard) checked updated dashcards
unconditionally, so any dashboard with a pre-existing foreign internal
card association became unsavable — every edit, even a resize, returned
400 "Invalid Request.".

Scope the guard to newly-introduced card ids: card ids the dashboard
already references (as a dashcard's card or in a series) are
grandfathered, so existing associations never block saving — including
moving such a card to a different dashcard in the same save. Newly
assigning a question internal to another dashboard is still rejected.

* Clarify arg names in internal-dashcard guard

Rename the guard's params to existing-dashboard/new-dashcards to make
clear the grandfather set comes from stored state while the checked
dashcards are the incoming save.
2026-07-23 10:08:33 -07:00
Timofey Kachalov
8ee3e5213c Update @slack/web-api to version 8.0.0 (#78438) 2026-07-23 20:53:32 +04:00
Timofey Kachalov
a2d673e0fe Enumerate data apps by reading git tree objects (#78114) 2026-07-23 20:05:12 +04:00
Alex Yarosh
4ea4064511 docs: sample database (#78390)
sample database
2026-07-23 10:37:38 -04:00
Michael Eby
55598bf0bd docs: update screenshots (#78363)
* docs: update screenshots

* Optimized 7 images, saved 357.6 KB (51.0% smaller)

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-07-23 10:08:53 -04:00
John Swanson
c5b6a38acb Guest embed filter dropdowns should use the router database (UXW-4881) (#78235)
* Guest embed filter dropdowns should use the router database (UXW-4881)

PR #68952 made guest-embed query execution bypass DB routing by wrapping it
in with-database-routing-off, but the parameter-values endpoints (dropdown
values, search, remapping) didn't get the same treatment: chain-filter runs
through the QP, where attach-destination-db-middleware throws "Anonymous
users cannot access a database with routing enabled."

- Wrap the query-running sections of the four param helpers in
  metabase.embedding-rest.api.common in with-database-routing-off, fixing
  all six /embed/{card,dashboard}/:token/params/* endpoints plus their
  preview_embed equivalents.
- Wrap the guest-embed tiles endpoints, which have the same anonymous-user
  bug.
- Wrap preview_embed's query-execution endpoints (query, dashcard, pivot,
  tiles) so the admin's embed preview shows exactly what the published
  embed will show (router DB data) instead of routing via the previewing
  admin's user attribute.
- Replace the manual *current-user-permissions-set*/*is-superuser?*
  bindings in the param helpers with the equivalent request/as-admin.

Public sharing has the same gap but whether routed DBs should work over
public links at all is an open product question, so it is intentionally
left out.

* Consolidate embed routing-off wraps into the shared execution helpers

Review feedback: rather than a macro that each endpoint must remember to
use, move with-database-routing-off inside the shared execution helpers in
metabase.embedding-rest.api.common (process-query-for-card-with-params,
process-query-for-dashcard, process-tiles-query-for-dashcard, and a new
process-tiles-query-for-card wrapper), which every /api/embed and
/api/preview_embed endpoint funnels through. Endpoints built on these
helpers now bypass routing automatically, so a future endpoint can't
reintroduce this class of bug.

Removes all per-endpoint wraps from embed.clj (including the two original
ones from #68952) and preview_embed.clj; the invariant now lives in one
file with one explanatory comment.
2026-07-23 06:33:37 -07:00
Nick Fitzpatrick
0669d80f2b Scope measurement container css vars (#78225) 2026-07-23 10:27:32 -03:00
Oleh K
b7d519320b Prevent scrollbar flicker from cell tooltips in TreeTable (#78337)
* Fix tooltip scrollbar flicker in TreeTable (GDGT-2822)

Hovering a cell tooltip in a virtualized TreeTable while scrolling made a
page-level vertical scrollbar flash on and off. The tooltip is portaled to
<body> and positioned with real top/left insets, and with the document root
sized to the exact viewport height, a tooltip positioned past the fold grew
the document and flashed a scrollbar.

Portal cell tooltips into the table's own scroll container instead of <body>,
so a tooltip positioned past the fold is clipped by the (already-scrolling)
table rather than stretching the page. This is wired through a small
TooltipPortalTargetProvider context so the metabase/ui Tooltip wrapper picks
up the target automatically — no per-tooltip wiring, and any TreeTable with
cell tooltips benefits (e.g. the notifications admin table).

* Address PR comment

* Fix Tooltip wrapper not forwarding ref
2026-07-23 15:16:47 +02:00
Chris Truter
26c003f858 Harden health inspector check persistence (#78295) 2026-07-23 15:06:00 +02:00
Jakub Chodorowicz
be2a5e4ea1 GDGT-2890: Fix custom-viz template .gitignore: ignore dist/ and *.tgz​ (#78355)
add dist and tgz to gitignored files
2026-07-23 14:57:19 +02:00
Nick Fitzpatrick
528ae0a381 Update table loading skeleton to use patterns (#78361) 2026-07-23 09:43:59 -03:00
Nick Fitzpatrick
4323943627 Filter out other users personal collections from search in DataStep (#78353) 2026-07-23 09:43:48 -03:00
Stas Gavrylov
b5849c9944 Monitor (#76806)
* Add monitor dir in metabase-enterprise

* GDGT-2676: Migrate Dependency Diagnostics under Monitor space

* GDGT-2677: Migrate Admin -> Tools under Monitor space

* GDGT-2676: Fix paths

* GDGT-2676: Create separate high-level directories for migrated features

* GDGT-2657, GDGT-2686: Monitor space + migrate Dependency Diagnostics (#76527)

* GDGT-2657: Introduce Monitor space layout and routing

* GDGT-2686: Migrate Dependency Diagnostics to Monitor

* Monitor: Add Monitor plugin. Cleanup + fixes

* Monitor: Update graphy ownership path for Dependency Diagnostics

* Monitor: Cleanup PR

* GDGT-2697: Update hotkeys hints for Toggle navbar

* GDGT-2684: Address Copilot review comments

* GDGT-2684: Address PR comments

* GDGT-2684: Rename SpaceLayout to AreaLayout. Split to separate files

* GDGT-2684: Refactor route guards + add tests

* GDGT-2684: Move Admin Tools pages to the Monitor space (#76606)

* GDGT-2684: Move Admin Tools to Monitor space

* Monitor: Extract diagnostics sidebar into shared Monitor sidebar outlet

Add a useMonitorSidebar context to MonitorContent so Monitor pages can
render sidebars in a dedicated right-hand region outside the padded,
scrollable main content area. Rewire Dependency Diagnostics to render its
detail sidebar through this outlet, and promote the resizable shell
(SidebarResizableBox) into shared monitor/components.

* Monitor: Add MonitorHeaderTitle component for consistent page headers

Introduce a shared MonitorHeaderTitle matching the small secondary text
style used by Data Studio breadcrumbs, and apply it across Monitor pages:
Dependency Diagnostics header, Jobs, Logs, Model cache log, Task list/runs,
Task and task-run details, Erroring questions, and Alerts management.

* GDGT-2684: Cleanup

* GDGT-2684: Update monitoring tools access permissions

* GDGT-2684: Add handling for not-found routes

* GDGT-2684: Fix not-found routes handling in Data Studio

* GDGT-2684: Extend route tests. Minor styling refactoring

* GDGT-2684: Refactor Alerts Management sidebar

* GDGT-2684: Move old SidebarResizableBox to ModelSibar

* GDGT-2684: Cleanup

* GDGT-2684: Refactor Sidebar solution to use Portal

* GDGT-2684: Fix Monitor Tools E2E specs (#76886)

* Format monitor router files after the master merge

* Import Route from metabase/router facade in AreaTab spec

Fixes the no-restricted-imports lint error banning direct react-router imports.

* GDGT-2713: Update Tasks page styling (#77088)

* GDGT-2713: Update Tasks page styling

Restyle the Monitor Tasks and Runs pages to match Dependency diagnostics:

- Replace the SettingsSection card + default tabs shell with the Monitor
  frame (fixed-height page, pills tabs; only the table scrolls)
- Rebuild TasksTable and TaskRunsTable on the TreeTable data-grid
  (sticky header, virtualized rows, skeleton loading, a11y labels)
- Move filters into a Group bar and pagination below the table with a
  total results count
- Keep the 2-state sort contract (always sorted, desc/asc) on top of
  TanStack's 3-state cycle; asc-first on all sortable columns
- Add MonitorHeaderTabs (Monitor-owned pills tabs, no data-studio
  dependency) and extract the shared LinkTab router-link tab to
  metabase/common/components

* GDGT-2713: Fix e2e test

* Migrate LinkTab to the metabase/router facade

* Remove unused Link import from PaneHeader

The Tasks styling switched the tabs to LinkTab, leaving Link unused.

* Address PR review comments and fix entity_name typing

Use strict undefined comparisons instead of loose null checks where
values can only be undefined (optional props, RTK Query error,
destructured SortingState), and strict null comparisons where the
value can only be null (Task.db_id).

Also fix TaskRun/RunEntity entity_name typing: the backend always
assocs this key (nil when the entity lookup misses), so it's
string | null on the wire, not optional.

* GDGT-2712: Update Jobs page styling (#77100)

* GDGT-2712: Update Jobs page styling

Restyle the Monitor Jobs page to match Dependency diagnostics:

- Rebuild the page on the Monitor frame (fixed-height page; only the
  jobs table scrolls, with a sticky header)
- Render the scheduler info in a Code block instead of a bare pre
  inside a settings card
- Rebuild the jobs table on the TreeTable data-grid (skeleton loading,
  empty state, a11y label, keyboard row activation); row click still
  opens the job triggers modal
- Move JobInfoApp into a folder and add unit coverage; add
  TaskInfo/Job API mocks and a task-info server mock

* Make the Monitor sidebar outlet resizable

Move the resizing behavior from the Dependency diagnostics sidebar into
the shared Monitor Sidebar so every Monitor view gets it without
duplication:

- Relocate SidebarResizableBox from metabase-enterprise/monitor into
  MonitorLayout and wrap the Sidebar portal content with it
- Sidebar API: optional resizable (default true) with containerWidth
  required while resizable, optional defaultWidth (512 by default);
  consumers measure their content area with useElementSize and pass it
- Protect the Monitor content area from text selection while dragging
  inside Sidebar instead of per-view
- Simplify DiagnosticsSidebar/DependencyDiagnostics accordingly and
  drop the now-empty metabase-enterprise/monitor/components tree
- Make the Alerts management detail sidebar resizable (fills the
  resizable box, keeps its 560px default width)

* GDGT-2712: Jobs page styling fixes

* Replace the job triggers modal with a Monitor sidebar

Clicking a job on the Jobs page now opens the job's triggers in the
resizable Monitor sidebar outlet instead of a modal:

- New JobTriggersSidebar renders each trigger as a vertical key/value
  block (all fields the modal table had), with a divider between
  triggers and a close action that returns to /monitor/jobs
- The /monitor/jobs/:jobKey route stays, so deep links and back/forward
  keep working; JobInfoApp reads the jobKey param and renders the
  sidebar, measuring its content area for the resize constraint
- Delete JobTriggersModal; add a Trigger API mock
- Give the scheduler info Code block the old card padding and keep it
  from shrinking

* Apply cross-review fixes for the jobs sidebar work

- Render trigger details as a dl/dt/dd list for proper label/value
  semantics (no visual change)
- Cover the shared Sidebar resize side effect (user-select class on the
  Monitor container across drag start/stop/unmount)
- Cover keyboard row activation, all trigger attributes with null
  placeholders and the may-fire-again 'No' branch, and the no-triggers
  empty state

* GDGT-2712: Cleanup

* GDGT-2712: Add distinct error message when job is not found

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Migrate JobInfoApp to the metabase/router facade

* Disable text selection on body while resizing the sidebar

Address review comment: dragging the resize handle otherwise causes
the browser to continuously select/deselect page text, since the
sidebar is portaled and doesn't share a DOM ancestor with the main
content to scope a CSS class to.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* GDGT-2711: Update Logs page styling (#77160)

* GDGT-2711: Extract LogsViewer to common Monitor components

* GDGT-2711: Update Logs page styling

* GDGT-2711: Cleanup

* GDGT-2711: Address PR comments

* GDGT-2711: Address PR review comments

Rename LogsViewer's process prop to processUUID, drop the unused
logs-content id, and remove a few comments flagged as redundant.

* GDGT-2661: Update Erroring questions page (styling + refactoring) (#77167)

* GDGT-2661: Restyle Erroring questions page

Rewrite the EE /monitor/errors page off the legacy audit_app viz onto the
Monitor recipe used by the other Monitor tools pages, keeping the same
backend audit query:

- TreeTable with the 11 audit columns, 2-state sortable headers, and native
  row selection; "Rerun Selected" moved to the standard bottom BulkActionBar
- Dependency-diagnostics-style debounced filter inputs
- Page no longer scrolls; the table body is the sole scroll region
- Pagination outside the table with a total count (new bad-table-count audit
  query), hidden until the data has loaded, and synced to a ?page URL param
- Requests abort the previous in-flight one on paging/sorting/filtering

Replaces the AuditParameters/AuditTable/ErrorMode stack; deletes the old
ErrorOverview and its click-mode.

* Remove dead legacy audit-table v1 code

The Erroring questions page was the last consumer of the Audit v1 table
stack. With it rebuilt on TreeTable, these are unreferenced and removable:
AuditTable, QuestionLoadAndDisplay, AuditTableVisualization (the last
"audit-table" display), AuditParameters, the bad_table card def, and
AuditMode. The rest of audit_app (unsubscribe UI, InsightsLink,
metabot-analytics) is untouched.

* Fix duplicate/incorrect cards in the erroring-questions audit query

The latest_qe CTE joined query_execution to the per-card MAX(started_at)
subquery on started_at alone, so executions across cards that shared a
timestamp fanned out — surfacing duplicate rows (and cards whose latest run
didn't actually error). Join on card_id as well. Regression test covers a
shared-timestamp collision.

* GDGT-2661: Address review feedback

- Surface count-query failures in the page error state so a failing count
  can't silently hide pagination
- Filter bar reflows (wrap) instead of clipping at narrow widths / zoom
- Skeleton renders the checkbox column to avoid a layout shift on load
- Centralize the internal-dataset-query cast to a single boundary
- Add tests: count db/collection filter clauses (backend), 202-body
  data.error path, real filter-reset assertion, rerun failure recovery

* GDGT-2661: Fold pagination total into the rows query via COUNT(*) OVER()

The page ran two /api/dataset requests per view — the bad-table rows and a
separate bad-table-count — each recomputing the expensive latest_qe scan over
all query executions. Add a COUNT(*) OVER () total_count column to bad-table so
the full filtered total rides on the rows response, and drop the count query.

Halves the round-trips and eliminates the second latest_qe scan (~2.1s -> ~1.4s
of DB work on the Stats dump); the window count adds no measurable overhead
since it runs over the already-materialized result set before LIMIT.

* Migrate ErrorOverview to the metabase/router facade

* GDGT-2661: Return erroring-questions total_count at the response root

The bad-table query computes COUNT(*) OVER () as a column, which repeated the
total on every row. Strip that column via an xform and surface the value once
as a top-level total_count on the response; the FE reads it from there.

* GDGT-2661: Non-blocking per-row rerun with loader in erroring questions

Rerun now runs in the background per card instead of blocking on the whole
batch: clicking Rerun Selected closes the bulk bar immediately and fires each
request independently, so more questions can be queued while slow ones run.
A rerunning row shows a loader in place of its checkbox, is excluded from
Select all, and its loader clears when the request settles; the list refetches
once the last rerun finishes.

TreeTable gains a generic isRowLoading prop and makes the whole checkbox cell
a click target so a near-miss no longer navigates to the row.

Also hoist total_count onto the Dataset type and drop the local helper.

* Extract shared generic sorting helpers for monitor tables

The monitor tables (erroring questions, tasks, dependency diagnostics) each
duplicated getSortingState/getSorting/isSortColumn. Extract them into a generic
metabase/utils/sorting module keyed by the column type. getSorting is overloaded
so the un-sort behaviour is type-driven: pass the current sort for always-sorted
tables (direction flips) or omit it to allow an unsorted result.

Refactor the three monitor tables onto the shared helpers.

* Update erroring questions E2E selectors for the new table

Point the tests at the TreeTable-based ErrorOverview: erroring-questions-table
container, erroring-question rows, and the always-enabled filters / bulk bar
that closes on rerun instead of a disabled button.

* Bind *driver* when reducing audit internal queries

The MySQL timestamp reader calls results-timezone-id, whose driver arg is
malli-validated as a keyword in tests. reduce-results* bound the timezone
override but not *driver*, so it was nil and audit queries returning timestamps
errored on MySQL/MariaDB. Bind *driver* to the app-db driver alongside the
override.

* GDGT-2661: Address PR comments

* GDGT-2661: Merge erroring questions input into one, extend to support name search

* GDGT-2661: Fix checkbox cell width in rows

* GDGT-2661: Fix e2e test

* GDGT-2661: Address PR comments

* GDGT-2710: Update Model cache log page styling (#77277)

* GDGT-2713: Update Model cache log page styling

Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Replace the hand-rolled AdminS.ContentTable
with a metabase/ui TreeTable (skeleton while loading, empty state, refresh
action), and drop the emotion-styled ModelCacheRefreshJobs.styled.tsx in
favor of a CSS module.

Model and Collection columns are the two stretching columns (fill remaining
width) and support client-side sorting, since the underlying /api/persist
endpoint has no sort param.

* Unify monitor tables on 3-state sortable headers

Erroring questions and Tasks forced a 2-state flip-forever cycle by passing
the current sort back into getSorting(); drop that so a 3rd click clears the
header's sort arrow and falls back to the page's default sort, matching
Dependency diagnostics and Notifications.

The default-sorted column on each page needs sortDescFirst matching its
default direction (desc), otherwise the very first click on that column is a
no-op: TanStack's per-column cycle treats desc as the removable/last state
when sortDescFirst is false, so a column that starts at desc goes straight to
removed on click one.

* Make all applicable columns sortable in Model cache log and Jobs

Both are fully-loaded, unpaginated lists, so enabling client-side sorting on
the remaining columns is safe. Model cache log's action column stays
unsortable (no header text or data). Tasks and Task runs are left as-is:
their backends only support a fixed set of sort columns (or none), and
sorting the rest client-side would silently reorder only the current page.

* GDGT-2710: Address PR comments

* GDGT-2709: Update Alerts management page styling (#77364)

* GDGT-2709: Update Alerts management page styling

Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Move pagination outside the table into a
plain footer and show the total result count via PaginationControls'
showTotal.

* Restrict Alerts management to admins only

The /api/notification/admin* endpoints are superuser-gated on the backend,
but the frontend route and nav link were reachable by anyone with the
broader can_access_monitoring permission (shared with the other Monitor
Tools pages), so a non-admin holding it could reach the page while every
API call inside it 403'd. Add a dedicated canAccessAlertsManagement
selector and route guard so the route/nav link match the backend's
admin-only restriction.

* Fix misleading test name in MonitorLayout spec

The test asserts the link is hidden for a non-admin analyst, not that it's
shown for an admin.

* GDGT-2794: Update Task/Run details page styling

Extract the "Back to Tasks"/"Back to Runs" breadcrumb into a shared
MonitorBackLink component, styled and positioned like other Monitor
page headers. Wrap both detail pages in the same non-scrolling shell
used elsewhere in Monitor so only the content area scrolls internally,
instead of the whole page scrolling behind the floating AppSwitcher.

* Extract MonitorPageContent to replace SettingsSection in Monitor

SettingsSection carries admin-page title/description logic that Monitor
pages don't use. MonitorPageContent keeps the same card styling with none
of that logic, dropping the last two /monitor call sites still importing
from metabase/admin.

* GDGT-2709: Fix layout in Tasks Details page

* GDGT-2709: Refactor NotificationsTable row selection logic. Fix selection with spacebar

* GDGT-2791: Abort superseded in-flight list requests in Monitor (#77694)

* GDGT-2791: Abort superseded in-flight list requests in Monitor

Changing page/sort/filter on a Monitor list page left the previous, often
expensive, list request running to completion on the server. Subscribed RTK
useXQuery hooks only abort when their last subscriber unmounts, so introduce
a shared useAbortableQuery hook built on lazy triggers (whose promise exposes
.abort()) that cancels the superseded request on arg change, unmount, and
skip transitions, and suppresses the abort error so cancellations never flash
the error view.

Caching mirrors useQuery: cache-first by default, refetchOnMountOrArgChange
to opt into always-fetch; manual refetch() always hits the network. Every
converted call site keeps its pre-migration caching semantics.

Converted: TaskListPage, TaskRunsPage, TaskRunEntityPicker,
ModelCacheRefreshJobs, NotificationsAdminPage (main list),
DependencyDiagnostics, BrokenDependentsSection, and ErrorOverview (its
one-line useAbortableAdhocQuery wrapper is inlined and hooks.ts deleted).

* GDGT-2791: Cleanup

* GDGT-2791: Fix type narrowing in TaskRunEntityPicker

* GDGT-2791: Clamp pagination range to total

* GDGT-2791: Test that tag invalidation does not refetch superseded args

* GDGT-2791: Scope query aborts per hook instance via injected cache key

* GDGT-2791: Migrate _refetchDeps to the shared __rtkCacheKey mechanism

* GDGT-2791: Refine baseQuery typings. Cleanup

* GDGT-2791: Replace interface in params declarations with type. Refine BaseQuery params type

* GDGT-2832: Group Monitor navbar items into sub-menus (#77706)

* GDGT-2832: Move AreaContent to AreaLayout and normalize Monitor page spacing

* GDGT-2832: Group Monitor navbar items into sub-menus

* GDGT-2832: Cleanup. Fix AreaTab outline overflow

* GDGT-2832: Match Alerts Management tab spacing to Dependency Diagnostics

* GDGT-2832: Replace collapsible navbar groups with static labelled sections

* GDGT-2832: Update Monitor views headings to match navbar. Fix e2e test

* Monitor section design tweaks (#78007)

* icon and spacing updates

* remove info icon from background tasks page heading

* improve empty states

* GDGT-2810: Enable sorting in all Monitor views (#77842)

* GDGT-2810: Enable sorting on all Background tasks and Task runs columns

* GDGT-2810: Remove bold styling from first Monitor table columns

* GDGT-2810: Fix reflection lint warnings in task history API test

* GDGT-2810: Qualify task_run/task_history filter columns

The entity_name and task_count sorts add joins (report_card and
report_dashboard both have their own entity_id column), so unqualified
filter columns become ambiguous: /api/task/runs?entity-id=N&sort_column=entity_name
returned a 500. Qualify the filter columns and add a regression test
combining filters with the derived-column sorts.

* GDGT-2810: Cleanup

* GDGT-2810: Use kebab-case sort params on the task runs endpoint

* GDGT-2810: Extract shared useSortingStateChange hook for TreeTable sorting

* GDGT-2810: Extract shared page/sort URL param parsers

* GDGT-2810: Fix e2e tests

---------

Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>

* GDGT-2843: Integrate Monitor upsell pages (#77948)

* GDGT-2843: Migrate Monitor upsell pages into monitor scope, link to /upgrade/monitor

* GDGT-2843: Tighten monitor module boundaries to standard shared tier

* GDGT-2843: Change Monitor app icon from gauge to pulse

* GDGT-2843: Update pulse icon and Monitor logo

* GDGT-2843: Address PR comments

* GDGT-2843: Fix e2e tests

* Fix Monitor code review findings (#78106)

* Fix Monitor code review findings

* Remove obsolete dependencies redirects

* Monitor review: Cleanup

* Address review comments

* Cleanup

* Remove Dependency diagnostics link from Data Studio nav

It already lives under Monitor (/monitor/dependency-diagnostics); the
Data Studio route just redirects there now.

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: YB Tan <tybmeta@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Maz Ameli <maz@metabase.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
2026-07-23 13:48:39 +03:00
Ngoc Khuat
d3456cc4d1 Only execute attached cards for attachment-only dashboard subscriptions (#78352) 2026-07-23 10:25:35 +00:00
Fraser
32e4c39378 Remove video recording (#78321) 2026-07-23 22:08:04 +12:00
YB
3bef91fb42 [GDGT-2145] map more dedicated fields and leave them out of additional opts (#78412)
map dedicated fields and leave them out of additional opts
2026-07-23 16:20:40 +08:00
Romeo Van Snick
5461b53d55 Flip the unit-test harness to the v7 router engine (#78306)
* WIP: start flipping unit-test harness to v7

Default routerEngine to v7 and render bare (non-Route) children under a
catch-all route so v7's <Routes> renders them. role=link nav failures still
to diagnose. Suite not yet green.

* Give v7 tests a history handle and drop DOM-wrapped route config

The harness now owns the v7 memory history and hands specs a v3-shaped adapter
(getCurrentLocation/push/replace/goBack/listen), so the ~42 specs that drive and
assert navigation through the returned history keep working on v7.

Route config wrapped in a <div> is a v3 artifact that v7's <Routes> cannot read;
swap those two for fragments.

* Pin the v3-vs-v7 conformance suites to the v3 engine

They exist to prove the facade on v3 matches real v7 and are built from raw v3
route config, so they must not follow the harness default now that it is v7.

* Resolve a relative initial route in the v7 test history

* Buffer navigations dispatched before the v7 router registers

* Carry route props onto the v7 matched-route stubs

* Fix the v48 /browse/:dbId-:slug legacy redirect on v7

* Render facade routes in renderHookWithProviders on v7

* Migrate unit specs off v3-only routing behavior for the v7 flip

* Drop buffered v7 navigations when the router unmounts

* Default use-v7-router to true
2026-07-23 12:25:13 +10:00
Romeo Van Snick
dbeb963735 Remove use-v7-router flag from docs (#78389)
* Remove use-v7-router flag from docs

* Turn the flag of by default
2026-07-23 09:58:06 +10:00
Alexander Polyankin
8f92d99055 Replace timeout-prone fk_target_field_id cleanup migrations with NOT EXISTS (#78382) 2026-07-22 18:56:49 -04:00
Romeo Van Snick
3ee16f6e79 Re-home history@3 types onto the metabase/router facade (#78190)
* Re-home history@3 types onto the metabase/router facade

* Format the re-homed import blocks

* Merge duplicate router imports and drop unused lint directives

* Re-home the router-listen spec's history Location type
2026-07-23 07:44:27 +10:00
Maksym Yakubych
8ff4bd113d feat: remove navigate_to (#75646) 2026-07-22 20:14:25 +00:00
github-automation-metabase
6af601cba0 Stabilize treemap breadcrumb drill test (#78303)
Wait for the DebouncedFrame to settle via ensureChartIsActive() before
clicking a group tile to drill in. While the frame transitions it carries
pointer-events: none, so the drill click was silently swallowed and the
breadcrumb button never rendered.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:31:30 -06:00
Riley Thompson
cdac81f0cc Dont add previous stage sql as source query for cached query sql (#78252)
* dont add extra select for persisted query

* add repro tests

* refactor test

* pull our add-source-query helper

* remove unneeded mt/dataset

* test clean up

* add comment
2026-07-22 18:59:23 +00:00
Andrei Chernykh
628eee6db4 Migrate AuditTableVisualization to a typed function component (#78196)
* Migrate AuditTableVisualization to a typed function component

* Fix Erroring Questions column sorting by forwarding sorting props through Visualization

* Extract typed viz definition to drop casts per review
2026-07-22 09:55:39 -07:00
dpsutton
6482737957 GHY-4016: Replace Field YAMLs with FieldUserSettings in git sync (PR … (#76843)
* GHY-4016: Replace Field YAMLs with FieldUserSettings in git sync (PR 1/3)

Git sync currently exports one Field YAML per database column. For a
typical customer this is ~6396 files — 84% of the entire repo. Most of
that data (active, database_type, position, dimensions, ...) is sync
metadata that git sync has no business tracking; the user-curated part
lives in FieldUserSettings.

This PR switches git sync to export FieldUserSettings instead of Fields.
Only fields with user-curated metadata get a YAML at all.

Measured against the stats-remote-sync repo (196 published tables,
~20 columns each):

  Import:  43s → 14s  (3x faster, 7659 → 1263 files)
  Export:  6.3s → 2.0s  (3x faster, 2155 → 239 entities)
  Files:   7659 → 1263  (84% reduction)

For a large customer (563k files, 522k Fields), ingest time alone drops
from 116s to 7s.

Full Field serdes is unchanged — non-git-sync export/import is
unaffected. FieldUserSettings already had complete serdes support
(generate-path, storage-path, load-find-local, make-spec); this PR
wires it into the git sync paths.

Changes:
- `serdes/descendants "Table"`: branches on `:user-edits-only` opts.
  When set, emits `["FieldUserSettings" field_id]` only for fields with
  a FUS row; otherwise emits `["Field" field_id]` as before.
- `spec.clj`: named `git-sync-extract-opts` constant (with
  `:user-edits-only true`); public `pk-col` helper for the non-standard
  FieldUserSettings PK (`:field_id` not `:id`).
- `extract.clj` (OSS): local `pk-col` for `:where [:in pk-col ids]`
  (module boundary prevents depending on EE spec).
- `impl.clj`: `extract-chunk` uses `spec/pk-col` for FieldUserSettings.
- `models.clj`: adds `"FieldUserSettings"` to `data-model-in-collection`.
- `events.clj`: removes `:model/Field` from `register-events-for-spec!`
  so field-update events no longer create Field RSOs. A dedicated
  `::field-update-event` handler creates FieldUserSettings RSOs only
  when a FUS row exists. This completes the incremental path: full
  export uses `descendants` + `:user-edits-only`; incremental export
  uses FUS RSO rows.

Tests assert on the full RSO table so stray Field RSOs fail immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address PR review: defensive get for dynamic pk-col, fix content-hash test for FieldUserSettings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* don't need maps, k/v pairs work

* Declare :model/FieldUserSettings module boundary export/import

remote-sync references :model/FieldUserSettings in events.clj, so it must
be exported by warehouse-schema and imported by enterprise/remote-sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove unused f1-id binding in extract test (kondo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Test that Field and FieldUserSettings are serdes descendants leaf nodes

Cements the design discussed in review: Table's descendants method is the
only source of field-level entries, and the :user-edits-only branch is
exclusive — you get Field refs or FieldUserSettings refs, never both via
a transitive Field -> FieldUserSettings edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 10:09:52 -05:00
Nicola Mometto
30021f1abd Transform run dag be (#76974)
* WIP: dag run BE

* split into new table

* run utils

* remove dag changes on job runs

* new dag run model

* abstract over parent-run

* separate dag model/api

* factor common job/dag code in a coordinated-run primitive

* fixes

* dag test

* fix migration

* kondo

* missing resolution

* exclude model

* kondo config

* fix test

* fix: exclude for deser

* fixes

* fix: exclude for deser

* fix: missing fk

* random ids

* chore: rename

* feat: add unioned runs

* remove dead endpoints

* api tests

* extend filter semantics

* trim docstrings

* kondo

* gap: allow canceling job runs

* share code

* fix: job start should return proper job id

* fix: mig to 64

* simplify

* simplify further

* simplify

* dead code

* add unique constraint on dag runs

* tighten schemas

* tighten permissions

* fix test

* relax schema

* fix: set null on delete

* handle deleted jobs/transforms for dag runs

* appease the sql linter

* fix test

* simplify rollbacks

* simplify callsite

* fix tests from bad rebase

* fix: fallback when transform is deleted

* add transform_count

* fix tests

* log with context

* varchar 32 for status

* Add UI for running transform with upstream or downstream (#77367)

* GDGT-2625 wip: reprocess DAG UI for transforms

- Split "Run now" button (upstream/downstream) + confirmation modal
- runDag / dag-transforms / cancel-dag API + analytics event
- Tabbed Runs page: transform-graph runs (default) + individual transform runs
- Graph runs table (Run/Type cols), filter bar (Type/Transform/Started/Status),
  sidebar with member transforms; shapes aligned to bronsa's /api/transform/runs
- List + members still served from a mock (transform-graph-runs.mock) via queryFn

* GDGT-2625 wip: align graph runs to /api/transform/runs, sidebar + cancel

- Reshape TransformGraphRun to backend RunSummaryResponse (run_type/entity_id/name)
- Filters mirror Transform runs: Type/Transform/Started/Ended/Status/Trigger
- Sidebar: title = Run name, Type as first metadata row, cancel section below
  metadata (dag/job/transform), confirm modal; job-run cancel endpoint wired
- Mock covers job/dag/transform in all states (succeeded/started/failed/timeout)
- Reorder columns: Status/Trigger after Duration

* GDGT-2625 wip: sidebar header entity links

Add "View this job" / "View this transform" (+ dependency graph for
transforms) action links to the graph-run sidebar header, mirroring the
individual transform-runs sidebar.

* GDGT-2625 wip: migrate graph runs to real /api/transform/runs

- Swap list + member queryFn mocks for real queries (list endpoint, dag/job
  member drill-down; standalone runs synthesized). Delete the mock fixture.
- Rewrite page spec against real endpoints via fetch-mock + new mock factories.
- Reword DAG run confirm modal (drop "in this order", bullet list) since members
  can run in parallel.

* Adjust wording

* GDGT-2625 wip: scheduled state + cleanup

- Show a "scheduled" run state on the transform page after a DAG reprocess is
  triggered, so the queued transform gives feedback instead of looking idle
- Drop dead DAG-run types/tag-providers/mock helpers left from earlier iterations

* Address code-review findings for reprocess DAG UI

Correctness:
- Migrate transforms.cy.spec.ts to /runs/individual (the /runs route now
  renders the graph-runs page, not the individual run table)
- Gate the "Run all" DAG confirm button on the mutation, preventing a
  double-click from triggering two DAG runs
- Toast when run-dag returns a null dag_run_id instead of silently no-op
- Stop the "Scheduled" banner from polling forever when the member-runs
  request errors
- Keep the graph-run sidebar open when the selected run scrolls off the
  current page during polling
- Invalidate transform state on cancelDagRun/cancelJobRun so member
  transforms' last_run refreshes after a cancel

Cleanup:
- Translate whole phrases (with context) for DAG run names
- Replace a typecast with a type guard in TypeFilterWidget
- Memoize parsed params and filter options on the graph-runs page
- Drop the unused direction field from the scheduled-DAG-run state
- Convert the sidebar's IIFE switch to ts-pattern match

* Handle deleted entities in graph runs; fix run-list cache invalidation

- Graph runs table: grey a deleted job/transform's run name (italic +
  "has been deleted" tooltip), keeping the row clickable
- Sidebar: keep showing member transforms for deleted DAG and standalone
  runs (fetched by dag-run id / synthesized from the run); hide the section
  only for deleted job runs, whose members can't be fetched without the
  (now-null) job id
- api/transform.ts: invalidate the run lists on deleteTransform (a
  transform's runs survive its deletion) and cancelCurrentTransformRun, so
  the Runs pages reflect the change instead of relying on polling
- Trim narrating comments from the transform API types

* Cleanup

* Fix transforms cancel e2e: click renamed "Cancel run" confirm button

The transform-page cancel confirmation button was renamed from "Yes" to
"Cancel run", but two cancelation tests still clicked "Yes". The cancel was
never confirmed, so the slow (pg_sleep) transform kept running and held a lock
on the writable DB, hanging the next test's resetWritableDb and failing the
suite. Point those two clicks at the new label (the individual-runs-page cancel
still uses "Yes" and is left unchanged).

* Avoid type cast in RunSection spec to satisfy no-unjustified-type-casts

Replace `body as string` with a typeof guard when reading the run-dag request
body, which the new lint rule flags.

* Show dependency count in the graph-run label

Use bronsa's new transform_count to label DAG runs as
"{n} dependencies › {name}" (upstream) / "{name} › {n} dependents"
(downstream), n = transform_count - 1, joined by a small chevron icon.
Bold the job/transform name; fall back to the plain name when the count is
absent (job/standalone runs, or DAG runs predating the column).

* Replace runs tabs with Detailed view switch; polish graph-run name rendering

- Swap the two runs tabs for a Detailed view switch that toggles grouped
  (/runs) and detailed (/runs/individual) views in place; add RunsPage shell
  and DetailedViewSwitch, remove RunTabs
- Render the sidebar header run name like the table (dimmed deps count +
  chevron), with wider gap and aligned font size/weight
- Keep the Detailed view switch label on one line
- Bump graph-runs page size to 25 to match the individual runs page
- Fix stale react-router-redux import

* Preserve shared filters across Detailed-view toggle; drop deprecated CSS util

Toggling the Detailed-view switch previously called the run-list URL helpers
with no arguments, discarding the active filters. Forward the params common to
both views (statuses, transform ids, start/end time, run methods) via a new
CommonRunListParams/pickCommonRunListParams helper; page and sort are dropped
since the two views paginate differently and use incompatible sort columns.

Also replace the deprecated CS.overflowHidden global utility class in the new
graph-run table with an inline overflow style prop.

* Track Detailed-view toggle in transform runs analytics

Emit a transform_runs_view_toggled simple event from the DetailedViewSwitch
toggle handler with the resulting view mode (detailed/grouped), so both the
grouped and individual run-list call sites are covered.

* Fix reprocess run status: don't re-show Scheduled after a member completes

- Derive the DAG scheduled state from this transform's own member run:
  a member run row only exists once the transform starts, so its presence
  means the transform already ran this cycle. Prevents isScheduled from
  re-arming (and masking the completed status) after this transform's run
  finishes while sibling DAG members are still running
- Keep the graph-run sidebar polling while the top-level run is canceling,
  mirroring isPollingNeeded

* Cleanup

* Address PR review nits from Uladzimir

- Extract isActiveRunStatus helper and use it for the duplicated
  started/canceling checks (RunButton, both isPollingNeeded, sidebar
  shouldPoll, RunSection dagFinished)
- DetailedViewSwitch: replace styles prop with a CSS module class
- Convert renderRunName/renderRunEntityName to RunName/RunEntityName
  components
- Drop redundant String() around an already-string cell value
- Cover RunButton onRun click and the run-options menu show/hide

* Adapt runs routes to the react-router migration on the rebased base

The base migrated transforms routes to element=/withRouteProps with
Outlet-based layouts. Make RunsPage an Outlet layout and render the runs
list pages via element in the specs.

* wip

---------

Co-authored-by: Oleh K <oleh@metabase.com>
2026-07-22 15:24:52 +01:00
Chris Vandoodewaard
76c36ef709 stop actionlint from linting deleted workflow files (#78342)
update file-paths filter for actionlint to ignore deleted files
2026-07-22 10:07:37 -04:00
Kamil Mielnik
70abeae733 GDGT-1933 Dashboard actions prefetch sends parameter values in URL query string (#77939) 2026-07-22 13:53:56 +00:00
Sébastien
6b1809dca1 Resolve tenant/personal collection sentinels in the question save path (#78104) 2026-07-22 15:22:07 +02:00
Timofey Kachalov
35444ee8a5 Add ability to report near membrane sandboxing blocks to a listener (#78344) 2026-07-22 17:04:12 +04:00
Timofey Kachalov
1152608228 refactor(sdk): Restructure the data-app dev server (#78251) 2026-07-22 15:04:52 +04:00
Timofey Kachalov
226175c492 Match CSP port semantics in the data-app sandbox allowlist (#78249) 2026-07-22 14:42:32 +04:00
Timofey Kachalov
7519d07ec5 refactor(sdk): Export SdkThemeProvider from the SDK bundle (#78247) 2026-07-22 14:38:53 +04:00
Romeo Van Snick
8adef47af6 Enable the react-router v7 engine by default, with the fixes it needs (#78199)
* Treat a v7 Link with an empty `to` as a button, not a navigation

* Make a v7 replace to the current URL a no-op so URL-sync effects settle

* Provide router.listen on the v7 engine and keep the router shim stable

* Serialize a v3 location query into the search string on v7

* Fix lint and type issues in the v7 router shim

* Keep the v7 matched-route branch stable across renders

* Republish the v7 splat param under v3's `splat` name

* Enable the react-router v7 engine by default

* Commit the v7 location update synchronously instead of in a transition

* Mirror the v7 location into redux from the history subscription

* Format the v7 router provider

* Sort query keys when serializing a v3 location query

* Match v3 query encoding and resolve bare link paths against the root

* Keep v3 query encoding and leave absolute link urls alone

* Always push when a v7 link targets the current url
2026-07-22 20:02:24 +10:00
Benoit Vinay
075482040b Updates to the license page (#78036)
* Updates to the license page

* Misc

* Unit tests updated
2026-07-22 10:55:11 +02:00
Nicola Mometto
fad1ba4fd7 test: bump transform-run timeouts for slow drivers (#78202)
* test: bump transform-run timeouts for slow drivers

BigQuery transform executions routinely take 50-70s in CI

* share timeout
2026-07-22 08:45:47 +00:00
Fraser
ce14a4a511 Clear old CodeQL caches (#78108) 2026-07-22 20:29:20 +12:00
Romeo Van Snick
001485bb4b Keep async vendor chunks out of the main app's initial bundle (#78304) 2026-07-22 13:32:12 +10:00
Chris Truter
73c6fa1778 Fix migration-version linter for the new backport label semantics (#78281) 2026-07-22 00:42:36 +00:00
da
5cfbe5423c Update pdfbox version from 3.0.5 to 3.0.8 (#78230) 2026-07-21 19:38:26 -03:00
Nathan Voxland
074f2a6e93 Additional transform gates (#76630) 2026-07-21 17:17:40 -05:00
Jeff Bruemmer
3e2c3661be docs - remove 63 workflow (#78265)
* remove 63 workflow

* update latest to target 63
2026-07-21 17:16:25 -04:00
Sloan Sparger
428c6be070 [Metabot] Animated markdown streaming (#78046)
* working solution

* finish

* code comment changes

* small animation adjustment

* respect reduced motion, fix tests
2026-07-21 20:15:26 +00:00
Nick Fitzpatrick
d188001d3a Loading Skeletons in Entity Picker (#78160)
* Loading Skeletons in Entity Picker

* update recent and search panels too

* renaming "LoadingSpinner" and adjusting imports
2026-07-21 16:40:01 -03:00
Uladzimir Havenchyk
c8f693f5da feat: use id-slug url for table row details (#78126)
* feat: use id-slug url for table row details

* update e2e test
2026-07-21 21:57:00 +03:00
Sloan Sparger
f8ae33f741 [Metabot] User conversation history + resumable conversations (#77549)
* conversation list endpoint

* add rtkquery endpoints

* generate conversation title

* better title generation logic, poll for title if not recieved before end of first message

* render conversation title in ui and show conversation list

* allow resuming historical conversations

* show exploration conversation history

* refresh current conversation ui for 409 case

* surface conversation titles in usage analytics

* add /metabot/conversation/:id route

* remove pending title data part, assume from lack of title data part we need to poll, fix loading a conversation that is mid-response

* title backfill job

* rename title event

* FE prevent polling multiple times for conversation title

* improve title generation prompt

* ci fixes

* automated review feedback

* e2e test fixes

* reduce diff size

* self-review

* Address PR review feedback

- api: pass profile-id through valid-usage-profile-id when ensuring a
  conversation title, matching the backfill job
- title backfill: gate readiness on check-usage-limits! (instance-level)
  so the job pauses and reschedules cleanly when a usage limit is hit
- title backfill: bound the title future .get with a 60s timeout so a
  stuck generation can't block the backfill loop indefinitely
2026-07-21 18:37:22 +00:00
Robert Kosara
830717374c Delete auditing tools page, redirect to usage analytics (#78241) 2026-07-21 17:12:23 +00:00
Maksym Yakubych
ccfc4fe280 feat: improve saving the charts (#76757) 2026-07-21 17:05:31 +00:00
Oleh K
bf0d55aac5 Add Metabot option to the Transforms New menu (#78134)
* Add Metabot option to the Transforms New menu (GDGT-1773)

Adds a "Metabot" entry as the first option in the Transforms "New" menu.
Clicking it opens the data-studio Metabot sidebar with a pre-seeded
"Create a transform that " prompt (not submitted), gated on Metabot access.

Also fixes the tiptap prompt input so a programmatically seeded prompt
places the caret at the end instead of the start (autofocus "end" plus a
caret-to-end after external setContent), which also improves the retry path.

* Cleanup

* Cleanup unnecessary code

* Test CreateTransformMenu Metabot item without mocking

Render the real CreateTransformMenu + Metabot via the shared metabot test
setup so the menu item, permission gating, and prompt seeding are exercised
end to end instead of through stubbed hooks.
2026-07-21 16:42:10 +00:00
Chris Vandoodewaard
916a3b8231 update the backport announcement (#77819) 2026-07-21 12:13:44 -04:00
Oleh K
8e45662740 Restyle Pill component to match Figma design (#77086)
* wip: Restyle Pill to Figma capsule spec

- Full-round 999px capsule, background-primary fill + shadow-xs ring
- 12px/500 text (text-primary), disabled -> text-disabled
- Remove button 12px, opacity 0.5 -> 1 on pill hover
- size 'sm' default + label slot override
- Add Storybook showcase matrix story
- Remove dead transforms components/TagList (unused)

* Adjust styles for existing Pill usages

* Add md size to Pill; center label/remove slots

- New md size (14px text, 6/12px padding, 28px tall) via .root[data-size=md]
- Flex-center the label and remove slots
- Adopt md in AddMemberRow and ReorderableTagsInput; drop dead radius=xl

* Apply design feedback to Pill padding and shadow

- sm vertical padding 2px (20px tall)
- sm removable right padding 6px; md removable right padding 8px
- Update shadow-xs token to match Figma (0.5px ring + 1px/3px drop)
2026-07-21 16:32:13 +02:00
Nicola Mometto
40e185b47f fix: use explicit ids when inserting test products on Snowflake (#78201)
Reuse the insert path added for ClickHouse in #77921
2026-07-21 14:38:23 +01:00
Chris Truter
e3a24b8db9 Bump bb pgsql pod to 0.1.6 to remove Rosetta requirement (#78200)
Bump babashka postgresql pod to 0.1.6 so mage runs on Apple Silicon

The 0.1.0 pod manifest has no macos-aarch64 entry -- only macos-amd64.
Babashka downloads that x86_64 artifact and caches it under the aarch64 path
anyway, so on an Apple Silicon machine without Rosetta every mage command dies
before doing any work:

  Cannot run program ".../mac_os_x/aarch64/pod-babashka-postgresql":
  Exec failed, error: 86 (Bad CPU type in executable)

Clearing the pod cache does not help -- the re-download is the same x86_64
binary. 0.1.5 is the first release with a real macos-aarch64 artifact; this
bumps to the current 0.1.6, which resolves to a native arm64 executable.

0.1.6 keeps every platform 0.1.0 shipped (macos-amd64, linux-amd64,
windows-amd64) and adds macos-aarch64 and linux-aarch64, so no one loses
coverage. `pg/execute!` is the only pod fn used anywhere in the repo, and its
signature is unchanged.

Verified on arm64: the cached binary is now `Mach-O 64-bit executable arm64`,
`mage.bot.setup-worktree` loads, and `./bin/mage fix-kondo-ratchets` runs to
completion.

Claude-Session: https://claude.ai/code/session_01X5dVcVX59aLonqqsYsqU65

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:17:08 +00:00
Romeo Van Snick
07909c841f Restore navigation blocking on the v7 engine and fix v7 Link navigation (#78103)
* Restore navigation blocking on the v7 engine via a blocking history

* Render the v7 Link on the v7 engine so links navigate in modals

* Scope v7 leave hooks to their route so within-route navigation is allowed

* Don't let a v7 Link with no destination navigate on click

* Type the app Link's `to` as optional to match the runtime contract

* Apply activeClassName on the v7 engine via NavLink
2026-07-21 19:09:17 +10:00
Kamil Mielnik
a3f5a5bf8a GDGT-2845 Fix Table to map dropdown search with more than 2 letters (#77946) 2026-07-21 09:00:20 +00:00
Oleh K
ec0a9ceaea Fix crashes and broken behavior in the native transform variables sidebar (#78047)
* Hide settings tab in TagEditorSidebar if there is no tags

* Don't crash query metadata fetch on un-normalizable queries

batch-fetch-query-metadata filtered empty queries before normalization,
but normalize-query returns {} when a query can't be normalized (e.g. a
native query with an incomplete field-filter/table template tag). That {}
then failed batch-fetch-query-metadata*'s strict ::query schema, 400ing the
whole request and tripping the query editor error boundary. Filter empties
after normalization too so an unnormalizable query just yields no metadata.

* Hide template-tag 'Try it' examples in transforms editor

The template-tag help sidebar's 'Try it' buttons load an example query
against the Sample Database. Transforms can't target the Sample Database,
so clicking 'Try it' switched the transform to an invalid database and left
the editor in a broken state. Add a canUseSampleDatabase QueryEditor option
(default on) that transforms set to false, which withholds the sample
database id so TagEditorHelp hides the 'Try it' buttons.

* Add regression tests for transform variables sidebar fixes

- batch-fetch-query-metadata skips queries that normalize to {} instead of
  400ing the whole request (the P1 crash)
- TagEditorHelp hides the 'Try it' examples when no sample database is available
- TagEditorSidebar hides the Settings/Help tabs until the query has a variable

* Address PR review feedback

- Simplify batch-fetch-query-metadata with (keep (comp not-empty normalize-query))
- Replace the hidden-tabs settings behavior with an empty state in the Settings
  tab (no more layout shift / auto-switch to Help); use the shared EmptyState
- Make sampleDatabaseId a required, DatabaseId-typed setup option
- Add a NativeQuerySidebar test asserting 'Try it' is hidden for transforms
  (canUseSampleDatabase=false) and shown otherwise
- Re-add the getEditorOptions canUseSampleDatabase assertion
2026-07-21 10:52:42 +02:00
Kamil Mielnik
07cb2f0a6c GDGT-2872 Revert #77695 to allow custom widget popovers (#78124) 2026-07-21 15:32:23 +07:00
Ngoc Khuat
3e9ff6e040 Bump schema-change test post-run waits on Redshift (#77945)
Bump schema-change test post-run waits to 30s on Redshift
2026-07-21 08:27:45 +00:00
Phoomparin Mano
6569bc1271 test(sdk-package): split useMetabaseQuery hook tests into smaller test suites (#78109)
* split use-metabase-query.unit.spec.tsx into multiple files

* split test fixtures from setup.tsx

* move useMetabaseQuery tests into tests folder

* rename useMetabaseQuery hook test

* remove unused query test fixture fields

* rename mock dataset query helper

* rename bundle exports

* chore: tiny formatting changes

* test: rewrite stale query results test

* test: extract public api tests to type and runtime tests
2026-07-21 05:54:16 +00:00
Anderson
a492c1091b Hide existing metrics from the metric mini picker (#78054)
* Hide metrics from the metric mini picker

* Select metrics from Browse all in E2E test

* Preserve metric picker restrictions
2026-07-20 22:33:19 -03:00
Anderson
cda8f7d301 Keep long chart tooltip values readable within the viewport (#77947)
* Wrap long chart tooltip values

* Constrain chart tooltip height
2026-07-20 22:33:10 -03:00
Andrei Chernykh
2e2b4e1298 Convert the Table visualization from a class to a function component (#78055)
* Convert the Table visualization from a class to a function component

* Stabilize the Table question so async metadata updates don't rebuild columns mid-hover

* Type useSyncedQuestion via VisualizationProps so the SDK d.ts doesn't leak internal types
2026-07-20 16:31:53 -07:00
Alexander Polyankin
7413348d68 Revert "Async workspace provisioning" (#78172) 2026-07-20 22:38:17 +00:00
Chris Vandoodewaard
71d66af5a2 Fix failure in "Re-run Flaky Workflows" workflow (#78135)
* use sendSlackMessage in workflow, update method to accept attachments and blocks

* pass slack token
2026-07-20 16:44:35 -04:00
Nathan Voxland
0de6d6ee2b Better model namespace requiring. (#76571) 2026-07-20 15:39:02 -05:00
Nick Fitzpatrick
c2decd2881 Library in Data Reference (#76917)
* Library in Data Reference

* conditional library option, adjusting table lists

* fix test
2026-07-20 17:32:41 -03:00
Chris Truter
5672dd99c6 Burn down redundant kondo ignores and stamp the false positives (#77787) 2026-07-20 20:25:02 +00:00
Romeo Van Snick
f8230a5b56 Add the react-router v7 engine behind a flag (#78020)
* Author route paths in v7 syntax, shim to v3 in the facade

* Add use-v7-router flag and frontend engine reader

* Add flag-gated react-router v7 engine (provider, tree mapper, bridge)

* Re-point redux navigation and location to the v7 router

* Support both router engines in renderWithProviders

* Switch the app mount to the v7 router behind the flag

* Point setRouteLeaveHook no-op comment at DEV-2374
2026-07-21 06:06:17 +10:00
Nathan Voxland
bfec5d9f26 Don't log user names or emails on authentication (#75539) 2026-07-20 15:01:02 -05:00
Ryan Laurie
096ce217b5 Chain the publish off of the auto-release, not the tag (#78161)
* chain the publish off of the auto-release, not the tag

* disallow auto release on forks

* thank you actionlint

* fail early

* today i learned
2026-07-20 13:33:32 -06:00
Nathan Voxland
a5ee670889 Better validate-setting logging and test handling (#77623) 2026-07-20 14:29:57 -05:00
Michael Eby
628f06a22c docs: fix markdown example link (#78146) 2026-07-20 14:08:25 -04:00
Ben Grabow
cbaff9c03e QUE2-756: Failed cache refresh (e.g. result too large) leaves refresh lease held and keeps serving stale cache (#78132)
* Delete stale cache entries after successfully running a query that shouldn't be saved to cache.
2026-07-20 17:39:25 +00:00
Alexander Polyankin
e1a1e69911 Async workspace provisioning (#78091) 2026-07-20 13:38:02 -04:00
Phoomparin Mano
b041c02f5a Move MCP url to the top and update inline origins copy (#78102)
* move mcp url field and inline origins copy

* copywriting improvements for allowed origins
2026-07-21 01:36:18 +08:00
Chris Vandoodewaard
e93496629b remove unnecessary uses of skipOn (#78048)
* remove some unnecessary uses of skipOn

* try running the slow x-ray test

* use standard @skip tag instead
2026-07-20 13:26:15 -04:00
Alexander Polyankin
9c2758cc0d Revoke schema-level grants when destroying Redshift workspace isolation (#78094)
* Revoke schema-level grants when destroying Redshift workspace isolation

destroy-workspace-isolation! discovered schemas to revoke via
svv_relation_privileges, which only surfaces relation-level grants: a granted
input schema with no tables (or whose tables were dropped after provisioning)
kept its schema-level USAGE grant, and DROP USER then failed with 'user ...
cannot be dropped because the user has a privilege on some object'. Union in
svv_schema_privileges so schema-only grants are revoked too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename workspace+det to workspace in the new destroy test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Run Redshift workspace teardown statements individually, not as a JDBC batch

A failure now surfaces as the plain server error instead of the
BatchUpdateException wrapper ('Batch entry N ... Call getNextException to see
other errors in the batch.'); the transaction still aborts as a whole on the
first error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Revert "Run Redshift workspace teardown statements individually, not as a JDBC batch"

This reverts commit 4bf2d241b5.

* Unwrap BatchUpdateException in Redshift workspace teardown

Keep the batch (there can be many granted schemas, two revokes each) but
rethrow .getNextException so failures read as the plain server error instead
of 'Batch entry N ... Call getNextException to see other errors in the batch.'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* remove comment

* Unwrap BatchUpdateException across all workspace-isolation batch sites

New driver.u/batch-exception util returns the per-statement exception hiding
under .getNextException (or the throwable unchanged). Applied to every
workspace init/grant/destroy executeBatch site (h2, mysql, postgres,
clickhouse, redshift) — init sites unwrap before scrub-exceptions — so
failures read as the plain server error instead of 'Batch entry N ... Call
getNextException to see other errors in the batch.'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep the batch exception on init-workspace-isolation! failures

The init statements carry secrets and pgjdbc echoes the aborted statement in
the BatchUpdateException message — scrub-exceptions redacts it, and the
GHY-3660 test (init-workspace-isolation-never-exposes-password-test) relies on
that redacted CREATE USER echo as proof the run reached the statement.
Unwrapping to the inner server error discarded the echo. grant/destroy keep
the unwrap: their statements carry no secrets and the plain server error is
the better message there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Unwrap the batch envelope on init-workspace-isolation! too

All workspace batch sites now rethrow the underlying per-statement exception;
init scrubs the unwrapped exception so passwords stay out of the message. The
GHY-3660 test now proves the flow reached CREATE USER by asserting the
server's permission error instead of the redacted batch echo (which no longer
exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Test batch-envelope removal for grant! and password scrubbing after unwrap

- driver-level: a failed grant-workspace-read-access! must not surface a
  BatchUpdateException (h2 runs locally; pg/mysql/clickhouse in driver CI;
  non-batching impls pass trivially)
- unit: scrub-exceptions composed with batch-exception redacts the password
  in the unwrapped per-statement message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Batch init/grant/destroy on every SQL workspace driver

Redshift grant, and all three Snowflake / SQL Server workspace methods, now
run their statements as JDBC batches like the other drivers — one round trip,
per-statement exceptions unwrapped via driver.u/batch-exception (init unwraps
before scrub-exceptions). Redshift grants commit before grant! returns, so a
subsequent describe-database from another connection still sees the
privileges; the destroy pre-loop for foreign-grantor default-priv rows stays
per-statement on purpose (each must survive the others failing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fold Redshift foreign-grantor revokes into the destroy batch

The per-statement autocommit pre-loop is gone: one transactional batch now
covers the FOR USER default-priv revokes, relation/schema revokes, and the
drops. A stale discovery row fails the whole destroy, which is fine — the
workspace layer records the failure and the deprovision retry re-discovers
fresh state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Escape workspace passwords in DDL like every other interpolated value

CREATE/ALTER USER statements interpolated the generated password raw into the
SQL string on postgres, redshift, h2, clickhouse, and snowflake (mysql and
sqlserver already escaped). Escape with sql.u/escape-sql :ansi (quote
doubling, valid on all five engines) and scrub both the raw and escaped forms
from failure messages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Unwrap batch exceptions from all JDBC connectors; fix sqlserver batch var scope

The grant-envelope test caught two real issues in driver CI:
- MariaDB's connector chains the per-statement exception via .getCause and
  mssql-jdbc chains nothing at all, so batch-exception now falls back to the
  cause and, failing that, rebuilds a plain SQLException with the same
  message/SQLState/error code.
- mssql-jdbc executes a Statement batch in one T-SQL scope, so the two
  sqlserver destroy statements both DECLAREing @sql collided; the variable
  names are now unique.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Type-hint the rebuilt SQLException ctor to avoid reflection

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Return the batch exception as is when nothing is chained

mssql-jdbc's BatchUpdateException message is already the plain server error —
only pgjdbc adds the 'Batch entry N ... getNextException' wording. So the
unwrap falls back to the original exception instead of synthesizing a
SQLException, and the driver test asserts the envelope wording stays out of
the message rather than checking the exception class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Interpolate the escaped password in Snowflake workspace user DDL

CREATE/ALTER USER spliced the raw password while every other workspace
driver uses the ANSI-escaped value; the escaped form was already
computed and scrubbed here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:17:10 -04:00
Uladzimir Havenchyk
3246bebfe4 fix: allow schemas with slash in the name (#77928)
* fix: allow schemas with slash in the name

* implement Bronsa's approach

* fixup

* fix comments

* fixup

* fixup

* fixup
2026-07-20 16:30:10 +00:00
Timofey Kachalov
b0ba83e74a Remove deleted apps on sync (#78040) 2026-07-20 20:16:17 +04:00
Chris Vandoodewaard
55554106ae ping users on Slack for failures on master/release branches (#77973)
* ping users on Slack for failures on master/release branches

* fix linting errors
2026-07-20 11:11:34 -04:00
dpsutton
64f32ddea7 Truncate data_type in BigQuery describe-fields SQL to prevent sync OOM (#78069)
A STRUCT column's INFORMATION_SCHEMA data_type spells out its entire
nested schema, and the COLUMN_FIELD_PATHS LEFT JOIN repeats it on every
nested-leaf row -- O(n^2) bytes for a column with n leaves. Dynamic-key
STRUCTs (e.g. Cloud Functions log-sink tables whose JSON payloads use
user IDs as keys) reach thousands of leaves with ~270KB type strings;
one such table produced ~4,000 rows x 271KB = 1.09GB buffered in
sync-fields!'s partition-by, OOMing the instance mid-sync.

Only short type-string prefixes are ever consumed
(raw-type->database+base-type dispatches on prefixes up to 10 chars and
stores at most RANGE<TIMESTAMP> verbatim), so SUBSTR(..., 1, 200) in the
query -- applied server-side, before the wire -- is behaviorally
identical while collapsing response size, parse cost, and the buffered
partition (1.09GB -> ~2.8MB for the table above).

The test needs no dataset or network: it captures the compiled SQL via
*process-native* and asserts both data_type columns are truncated.
2026-07-20 09:57:01 -05:00
Nicola Mometto
67de65ca3b fix clickhouse parameter rewriting on sql-tools (#78110)
* sql-tools: fix placeholde rewriting for clickhouse

* add regression test
2026-07-20 14:31:32 +00:00
Uladzimir Havenchyk
e270803b78 fix: use new router syntax (#78119) 2026-07-20 10:16:14 -04:00
Chris Vandoodewaard
d518c62a90 move global unit tests to component folders (#78058)
* remove the last test file from the legacy structure

* convert test table into format of newer tests

* remove test for undefined since this isn't a valid type for RowValue
2026-07-20 09:57:54 -04:00
Michael Eby
35cd24c2fe docs: replace markdown links (#78043) 2026-07-20 09:46:53 -04:00
Nemanja Glumac
7801008876 Restore the backend tests dependency on static-viz (#78112) 2026-07-20 15:12:55 +02:00
metabase-bot[bot]
6ab02a5d6d [chore] Update E2E test timings with recent data (#78095)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-07-20 14:40:40 +02:00
Uladzimir Havenchyk
1a6755d898 Permalinks for dbs, schemas and tables (#77274)
* handle database named permalinks (#76794)

* handle database named permalinks

* fixup

* keep urls in the bar

* add tests

* fixup

* fixup

* address review

* handle schema name permalinks (#76807)

* handle schema named permalinks

* keep urls in the bar

* fixup

* address review

* handle table named permalinks (#76814)

* handle database named permalinks (#76794)

* handle database named permalinks

* fixup

* keep urls in the bar

* add tests

* fixup

* fixup

* address review

* handle schema name permalinks (#76807)

* handle schema named permalinks

* keep urls in the bar

* fixup

* address review

* handle table named permalinks

* fixup

* empty

* collapse table permalink routes into one optional-segment route

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* address review

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add permalink buttons (#77213)

* empty

* add permalink buttons

* fix eslint

* use named URLs in browser instead of id-slug (#77421)

* use named URLs in browser instead of id-slug

* fix linting

* add a comment

* address review

* add tests to permalinks (#77841)

* add tests to permalinks

* fixup

* fixup

* fixup

* fixup

* drop some tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 15:36:10 +03:00
Timofey Kachalov
7c3311ba35 Improve loader overlay for a data app (#78111) 2026-07-20 16:26:28 +04:00
Nick Fitzpatrick
9cb27d3986 Fix leaking plural template (#77606)
* Fix leaking plural template

* PR feedback
2026-07-20 09:01:51 -03:00
Chris Truter
57e0ad66ea Add kondo ignore ratchets (#77773) 2026-07-20 13:25:59 +02:00
Kamil Mielnik
74ec111ffe Prevent claude from inserting // Unjustified type cast. FIXME comments (#78035) 2026-07-20 17:02:44 +07:00
Sébastien
ccd7e61184 Set showDashboardQuestions to false by default on SDK CollectionBrowser (#78038) 2026-07-20 11:29:16 +02:00
Egor Grushin
184c415f6e GDGT-2578 Replace TokenField (#77768)
* migrate TokenField

* fix RemappedOption type for Autocomplete ComboboxStringItem

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* address RecipientPicker review: memoize maps, share lookup/label helpers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix single-value search: accept top match on Enter

Autocomplete only completes on Enter when an option is highlighted, but
search options load async so none is. Restore old TokenField behavior via
onKeyDown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix letters swallowing

* addressing the PR's comments

* added missing tests

* introduce Autocomplete + refactoring

* apply filter by clicking on enter

* fix test

* refactor autocomplete

* refactor autocomplete

* fix edge case + added unit tests

* fix type

* fix test

* fix test

* get rid of lint suppression + allow autoselect on open for multiautocomplete

* revert autoselect destructuring, restore eslint suppression (tests failing)

* explain exhaustive-deps suppression in MultiAutocomplete effect

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:59:28 +01:00
Romeo Van Snick
53c503ff0f Remove withRouter from production and delete its public export (#78019)
* Remove withRouter from production components

* Delete the public withRouter export

* Type the route-leave hook for PlainRoute and drop the route casts
2026-07-20 15:27:00 +10:00
Romeo Van Snick
a9396323f5 Author route paths in v7 syntax, shim to v3 (#78079)
Author route paths in v7 syntax, shim to v3 in the facade
2026-07-20 14:48:00 +10:00
Romeo Van Snick
04000cef07 Restructure permissions routes for v7 matching (#78021)
Restructure permissions routes to v7-compatible depth siblings
2026-07-20 03:38:37 +00:00
Romeo Van Snick
638f7e7f86 Convert the route tree to element/Outlet (#77805)
* Add element/Outlet + useRoute support to the router facade

* Convert root route tree to element/Outlet

* Convert admin route tree to element/Outlet

* Convert data-studio, models and metrics route trees to element/Outlet

* Convert transforms, account, monitor and public route trees to element/Outlet

* Convert enterprise route trees to element/Outlet and drop dead library segment routes

* Convert tenants admin route tree to element/Outlet

* Convert redirect, createEntityIdRedirect and modalRoute to element

* Ban component= on routes with eslint

* Drop component= from the facade Route type and convert remaining routes

* Drop the component= eslint rule, the facade type now enforces it

* Remove the unused withOutlet helper

* Assert the required tenantId route param instead of coercing to NaN

* Bridge element={null} to a nothing-rendering component, matching v7

* Memoize router context value so element routes re-render once per navigation

* Convert data_apps and impersonation routes merged from master to element
2026-07-20 10:34:33 +10:00
Tyler Plude
8610bfc613 [BOT-1871] Fix portable_entity_id being used for read_resource (#78074)
* fix portable_entity_id being used for read_resource

* remove entity_details references
2026-07-18 15:34:13 -07:00
Egor Grushin
8caa13a827 Remove accidentally committed __config.yml with credentials (#78084) 2026-07-18 22:07:39 +01:00
Timofey Kachalov
806e4b756c Data Apps Beta (#74840)
* initial proof-of-concept for data apps

POC 2

POC 3

POC 4

Revert "POC 4"

This reverts commit 1f62e574dc.

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 commit 383a5fa28d.

* Revert "Revert "Merge data-apps specific migrations into one""

This reverts commit 948d904b62.

* 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>
2026-07-18 17:42:50 +01:00
Egor Grushin
8dd86422fe GDGT-2577 Replace deprecated SchedulePicker component (#77911)
* Replace deprecated Schedule

* addressing styling issues

* addressing typing issues

* addressing missing tests

* addressing converting the component to an until

* addressing cron test coverage

* fix e2e subscription tests for Schedule migration

- target Mantine Select via data-testid (select-frequency/select-frame)
  instead of findByDisplayValue (dupe visible+hidden inputs)
- lowercase frequency option labels
- assert token popover not.exist (unmounts on Escape)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:23:59 +02:00
Fraser
f051eb94a5 Compile e2e CLJS in parallel with the uberjar build (#78017) 2026-07-18 15:12:22 +12:00
Timothy S. Dean
3df3238697 Consistency pass over database routing invariants (#78066)
Some cleanup of some false alarms I encountered in test environments while working on a different issue. Destination databases (those with `router_database_id` set) are reached only through their router: metadata, sync, permissions, etc. Only execution goes against the destination's connection. A `data_permissions` row pointing at a destination is never consulted -- it's already actively stripped.

There are test utilities that helpfully create in-test permissions for all databases created, so in a test environment it would look exactly like the invariant was being violated, but that was only because the test utilities deviated from production behavior. This brings the test behaviors into line with production (no permissions get written).

There should be no change in production behavior in this PR, but as a bonus, I figured that a safety guard filtering rows out on reads is not as good as filtering them out on reads *and* failing loudly on writes. I also added guards that a normal database can't be turned into a destination database, which was a behavior tests were relying on for easier setup (but again, the tests then deviated once more from production behavior).
2026-07-17 20:39:45 -06:00
Rodrigo López Dato
3a00f09bfd Disable Liquibase usage analytics (#77992)
Since 4.28 Liquibase ships an opt-out analytics feature that, on the first
migration run, fetches config from config-endpoint.liquibase.com and sends
anonymized usage events.
2026-07-17 23:28:40 +00:00
github-automation-metabase
f5bbb2bfde Auto-update documentation for master (#78062)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-17 21:27:21 +00:00
Mihael Konjević
898f75a2b1 Parse :absolute-datetime string literals in repr resolve (#76436)
* Parse :absolute-datetime string literals in repr resolve

Metabot's representations repair pass wraps bare ISO `between` bounds as
`[:absolute-datetime {} "2024-01-01" :day]` to satisfy the lib `:between`
temporal schema, leaving the literal as a string. That form is schema-valid
(so it passes construct_notebook_query validation), but the JVM query
processor expects a real java.time value — optimize-temporal-filters throws
on a bare string, and the QP's own wrap-value-literals only parses temporal
strings that arrive as raw values, not ones already wrapped in
:absolute-datetime.

Parse those string literals into java.time values in resolve-query, where the
portable repr form becomes QP-ready pMBQL, alongside the existing
annotate-field-types pass.

* Address review: tighten docstrings, use update over assoc

- parseable-temporal-string?: contract-first two-line docstring
- parse-absolute-datetime-literals: move the "why" into inline comments at
  the walk, keep the docstring to contract + idempotency
- use (update node 2 u.date/parse) instead of assoc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review: year/year-month, fail-fast, export round-trip

- parse year/year-month :absolute-datetime literals too (u.date/parse maps
  them to LocalDate), so they are QP-ready instead of crashing later
- fail fast on a structurally-ISO but invalid literal (e.g. "2024-13-45"):
  re-throw as an :agent-error? ex-info so the LLM gets a clean message
- export: format java.time values back to ISO-8601 strings in
  portable-repr-form so resolve -> export round-trips to the documented
  string form
- tests: per-shape parsing (date/datetime/offset/year/year-month),
  idempotency, invalid-literal, and export round-trip

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Move :absolute-datetime string parsing to the QP layer

Revert the resolve-layer parsing and instead teach wrap-value-literals to
parse string-valued :absolute-datetime clauses, not just raw values. The
repair pass wraps bare `between` bounds as `[:absolute-datetime {} "2024-01-01"
:day]`; wrap-value-literals previously only parsed temporal strings that
arrived as raw values, so a pre-wrapped string slipped through and
optimize-temporal-filters threw on the bare string.

The QP is the right layer: wrap-value-literals runs at execution time, where
both the report timezone (bound) and the comparison field's type are
available — so the parse is type- and timezone-aware for free, and the stored
query keeps its portable ISO-string form (no export round-trip to invert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fail fast on malformed :absolute-datetime literals in resolve

Add a resolve-side validation pass that rejects temporal literals which pass
the schema's structural regex but are not real dates (e.g. "2024-13-45"), so
the agent gets an actionable error at construct time instead of a failure at
query execution.

Validation only — it does not coerce. The string -> java.time coercion stays
in the QP's wrap-value-literals, where the field type and report timezone are
available. Accepts every valid :absolute-datetime shape: date, datetime,
offset-datetime, year, and year-month.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Preserve unit and wrap mixed :between bounds for string :absolute-datetime

When re-parsing a string-valued :absolute-datetime in wrap-value-literals:

- Carry the clause's own temporal unit through instead of discarding it. The
  comparison field's bucket still wins, but for an unbucketed field the literal's
  unit is now the fallback (rather than :default), so a year/year-month literal
  like [:absolute-datetime {} "2024" :year] keeps its bucket instead of silently
  collapsing to a single instant.
- Wrap each :between bound independently so a mixed bound (string :absolute-datetime
  on one side, :now / :relative-datetime / an already-parsed :absolute-datetime on
  the other) still coerces the wrappable side instead of falling through unwrapped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Guard comparison literals and tidy :absolute-datetime validation

- wrap-value-literals: guard the field position in the :=/:!=/<…> arms so a
  comparison with a wrappable literal on both sides can't bind a literal as the
  field and leave it unparsed; it now falls through unchanged.
- resolve: validate :absolute-datetime literals via lib.walk/walk-clauses
  (no tree rebuild, shares clause-of-type? detection with the QP) instead of
  clojure.walk/postwalk.
- Move cross-subsystem rationale out of the string-valued-absolute-datetime
  docstring into an inline comment.
- Add a test covering the two-literal comparison fall-through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Parse fieldless :absolute-datetime comparisons and bound bad-literal errors

Wrap a string-valued :absolute-datetime on both sides of a comparison
(no field to wrap against) so each literal is parsed with its own unit
instead of leaving a bare string for later middleware to choke on.

Truncate the untrusted literal before it lands in the agent-facing
exception message and ex-data, so a pathological value can't flood logs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Treat :default field unit as unbucketed for :absolute-datetime literal fallback

A temporal field can carry :temporal-unit :default (add-default-temporal-unit
stamps it on :temporal/requires-default-unit drivers, and an LLM can author it
directly). The unit-fallback guard only skipped a nil unit, so a :default field
dropped the literal's own unit and collapsed e.g. [:absolute-datetime {} "2024"
:year] to a single :default-bucketed instant. Treat :default as unbucketed too.

* Validate :absolute-datetime literals with plain walk to keep resolve gate friendly

validate-absolute-datetime-literals walked the query with lib.walk/walk-clauses,
a mu/defn whose input schema validates the whole query against ::lib.schema/query.
Under test instrumentation that threw a raw "Invalid input" error on an otherwise-
malformed query (e.g. an :offset in :expressions), pre-empting the friendlier
query-not-runnable agent error the downstream canRun gate is meant to surface.
Use clojure.walk/postwalk and a bare [:absolute-datetime _ s _] shape check, matching
the sibling annotate-field-types pass, so only the literals are inspected.

* Use match/match-many for :absolute-datetime literal validation, add mixed-between raw-value test

Addresses PR review feedback: swap the manual walk/postwalk in
validate-absolute-datetime-literals for match/match-many, and cover a
:between with a raw (unwrapped) string bound plus a :relative-datetime
bound in wrap_value_literals_test.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:16:14 +02:00
Alexander Polyankin
05fa507ed7 Workspaces cleanup (#77875) 2026-07-17 17:08:49 -04:00
Nathan Voxland
d1f282210c Improving LLM error handling (#78003)
- Handle mid-stream failures better
- Better retry detection + handling
- Detect invalid json better
2026-07-17 15:17:44 -05:00
Eric Normand
f0ce91f74c Fix Remote Sync push hang: honest stalled UI + meaningful export progress (GHY-4132) (#77904)
* Show honest "still working" state when a Git sync push stalls (GHY-4132)

A large push can stop reporting progress while the export is still running; the
backend then reports the task as :timed-out with ended_at nil. The push modal
treated that as neither success nor error and stayed frozen on "Exporting
content…" until the page was refreshed.

Render a distinct stalled state instead: drop the frozen progress bar and show
"Still working. No update for N minutes." (internationalized, singular/plural
aware). The counter derives from last_progress_report_at and stays fresh via the
existing 2s current-task poll — no separate ticker.

* Add throttled monotonic progress reporter for remote-sync tasks (GHY-4132)

* Ramp remote-sync export progress across plan + serialize phases (GHY-4132)

* Report remote-sync export commit checkpoint before push (GHY-4132)

* Report remote-sync push progress via JGit ProgressMonitor (GHY-4132)

* Fix throttle, dead 0.66 write, and push liveness in export progress (GHY-4132)

commit-staged! was passed a pre-forced report closure, so every push
ProgressMonitor tick bypassed the 10s throttle and hammered the DB. Force only
the one-shot commit checkpoint (0.8) explicitly in finish-commit!, and let
push ticks go through the throttled reporter.

The forced 0.66 serialize-boundary write ran after commit-staged! returned,
by which point the 0.8 checkpoint (and push ticks) had already advanced the
monotonic reporter past it, so it was silently dropped. Move it to the end of
each export's stage-fn, before the checkpoint/push run.

The push monitor only reported during the exactly-titled "Writing objects"
phase with a known positive total; a non-English JVM locale renames that
title and JGit may report an unknown (zero) total, so the push phase could go
silent for its whole duration and re-trip the stale-progress timeout. Report
on every update tick regardless of phase/total, falling back to the 0.8
push-start fraction — repeated equal values are monotonic no-ops but still
bump last_progress_report_at, throttled to at most once per 10s.

* Use dayjs diff for the stalled-progress minute counter (GHY-4132)
2026-07-17 20:12:29 +00:00
Nick Fitzpatrick
a8234bd97c Overhaul pie chart static viz. Fix leaking HSLA values (#77303)
* Overhaul pie chart static viz. Fix leaking HSLA values

* PR feedback:

* label column adjustment to match previous behavior

* fe lint
2026-07-17 16:35:43 -03:00
Jeff Bruemmer
55e1aa315d docs - 2FA (#77883) 2026-07-17 15:06:45 -04:00
Eric Normand
8c87c3489a Expose search's ranked set and page-hydration as separate steps (#78005)
* Expose search's ranked set and page-hydration as separate steps

`search` already ran in two stages: rank the permission-filtered result set
(capped at max-filtered-results), then slice that set to the requested page
and hydrate only those rows. The comment on `search-results` spells out why
the slice comes first — hydration does per-row work, including the expensive
`can_write` check, so it should only touch rows that are actually returned.

Both stages were sealed inside `search`, so a caller holding several ranked
sets had no way to combine them and paginate the combination. Its only option
is to ask `search` for N hydrated pages and merge those, which hydrates N
pages to return one, and — because each page is sliced from its own ranking
before merging — cannot page the merged order correctly at all.

Split the two stages into `ranked-results` and `search-results` and export
both from the module API. `search` is now their composition and behaves
identically; no call site changes.

The motivating caller is metabot's multi-query search, which runs a search per
query and fuses the rankings with RRF. Fusing at the ranked-results seam lets
it hydrate only the page it returns, page the fused order coherently, and
report a total for the fused set.

* Remove comments from search core.clj

Removed comments explaining the purpose of `ranked-results` and `search-results`.

* kick ci
2026-07-17 13:53:48 -05:00
Rodrigo López Dato
7297aaaa8a Clarify what SLO does (#77761) 2026-07-17 14:40:10 -03:00
Jeff Bruemmer
eb8d806ffe docs - custom viz sdk (#77974) 2026-07-17 12:53:23 -04:00
Andrei Chernykh
f08dbabf1b Extract Table viz definition into a typed definition file (#77983) 2026-07-17 09:40:50 -07:00
Nick Fitzpatrick
1ec6f1d2d3 Add missing monthsListControl style to DateInput (#77627) 2026-07-17 13:24:30 -03:00
Andrei Chernykh
8fd7008e90 Convert sandboxes and advanced_permissions plugin modules to TypeScript (#77487)
* Convert sandboxes and advanced_permissions plugin modules to TypeScript

* Address review comments: remove typecasting from sandboxes actions and unit spec
2026-07-17 09:16:29 -07:00
Timur Mashara
144a139152 Revert 'GRO-554 update storage upsell (#76052)' (#78034) 2026-07-17 12:08:44 -03:00
Sébastien
d7a87295df Add pagination and sorting to MCP analytics tool calls (#77599) 2026-07-17 16:42:55 +02:00
Nick Fitzpatrick
cd9d166077 Fix trend viz in FE PDFs (#77612) 2026-07-17 11:08:38 -03:00
Sébastien
95267311e5 Hide dashboard questions in CollectionBrowser by default (#77941) 2026-07-17 14:35:14 +02:00
Cam Saul
74af0a368d [MongoDB] remove :metadata/key-constraints driver feature flag (#77659)
[MongoDB] remove `:metadata/key-constraints` driver feature flag (incorrectly set)
2026-07-17 12:52:04 +02:00
2271 changed files with 82080 additions and 35146 deletions

View File

@@ -26,6 +26,7 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
- **No redundant runtime coercion** — don't wrap already-typed values in `Number()` / `String()` / `Boolean()`.
- **Type guards belong in `frontend/src/metabase-types/guards/`**. Do not redefine them locally.
- **A cast you can't avoid needs a real justification comment.** The `metabase/no-unjustified-type-casts` rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy `// Unjustified type cast. FIXME` placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.
## Type modeling

View File

@@ -179,6 +179,8 @@
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
;; only writes .clj-kondo/ratchets.edn, from a :once fixture that finishes before the parallel tests
dev.kondo-ratchet/fix!
metabase.ai-tracing.core/record!
metabase.api.macros.defendpoint.tools-manifest/assert-optional-fields-nullable!
metabase.channel.core/send!
@@ -524,6 +526,7 @@
metabase.test/with-temp-empty-app-db clojure.core/let
metabase.test/with-temp-file clojure.core/let
metabase.test/with-user-in-groups clojure.core/let
metabase.transforms-rest.api.transform-runs-test/with-runs-fixture! clojure.core/fn
metabase.upload.impl-test/with-upload-table! clojure.core/let
metabase.util/for-map clojure.core/for
metabase.util.files/with-open-path-to-resource clojure.core/let
@@ -736,6 +739,7 @@
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
metabase-enterprise.database-routing.e2e-test/with-routing-setup! macros.metabase-enterprise.database-routing.e2e-test/with-routing-setup!
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!

View File

@@ -1048,6 +1048,7 @@
health-inspector
{:team "Querying Platform"
:api #{metabase.health-inspector.api
metabase.health-inspector.core
metabase.health-inspector.init}
:uses #{api
lib
@@ -1303,7 +1304,8 @@
metabase.metabot.core
metabase.metabot.init
metabase.metabot.self
metabase.metabot.settings}
metabase.metabot.settings
metabase.metabot.tools.entity-details}
:friends #{agent-api
enterprise/metabot
enterprise/metabot-analytics
@@ -1323,6 +1325,7 @@
documents
driver
entity-retrieval
events
interestingness
legacy-mbql
lib
@@ -1335,6 +1338,8 @@
permissions
premium-features
pulse
queries
query-permissions
query-processor
request
search
@@ -1360,6 +1365,7 @@
:model/DashboardCard
:model/DashboardTab
:model/Database
:model/Document
:model/Field
:model/Glossary
:model/Measure
@@ -2011,6 +2017,7 @@
metabase.search.core
metabase.search.engine
metabase.search.in-place.scoring
metabase.search.index-health
metabase.search.ingestion
metabase.search.init
metabase.search.scoring
@@ -2025,6 +2032,7 @@
collections
config
events
health-inspector
lib
lib-be
models
@@ -2442,6 +2450,7 @@
:friends #{models
enterprise/transforms-python}
:model-exports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
@@ -2499,12 +2508,35 @@
transforms-base
util}
:model-imports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
:model/TransformRun
:model/TransformTag}}
typed-schemas
{:team "Embedding"
:api #{metabase.typed-schemas.api}
:uses #{actions
api
collections
lib
lib-be
metabot
metrics
models
system
types
util}
:model-imports #{:model/Action
:model/Card
:model/Collection
:model/Database
:model/Field
:model/Measure
:model/Table}}
types
{:team "Querying Platform"
:api #{metabase.types.core
@@ -2687,6 +2719,7 @@
warehouses}
:model-exports #{:model/Dimension
:model/Field
:model/FieldUserSettings
:model/FieldValues
:model/Table}
:model-imports #{:model/Card
@@ -2991,20 +3024,6 @@
:model/QueryCache
:model/QueryExecution}}
enterprise/checker
{:team "Graphy"
;; todo: add ability to prevent usage (transitive even) of app db
:api #{}
:uses #{classloader
lib
models
plugins
query-processor
sql-tools
util
enterprise/dependencies}
:model-imports #{:model/Database}}
enterprise/cloud-add-ons
{:team "Cloud"
:api #{metabase-enterprise.cloud-add-ons.api}
@@ -3072,6 +3091,17 @@
:api #{}
:uses #{premium-features}}
enterprise/data-apps
{:team "Gadget"
:api #{metabase-enterprise.data-apps.api
metabase-enterprise.data-apps.init
metabase-enterprise.data-apps.sync}
:uses #{api
models
premium-features
settings
util}}
enterprise/data-complexity-score
{:team "Metabot"
:api #{metabase-enterprise.data-complexity-score.api
@@ -3149,8 +3179,7 @@
enterprise/dependencies
{:team "Graphy"
:api #{metabase-enterprise.dependencies.analysis
metabase-enterprise.dependencies.api
:api #{metabase-enterprise.dependencies.api
metabase-enterprise.dependencies.core
metabase-enterprise.dependencies.init
metabase-enterprise.dependencies.models.dependency}
@@ -3233,7 +3262,9 @@
:uses #{analytics-interface
collections
entity-retrieval
health-inspector
premium-features
search
task
util
enterprise/semantic-search}
@@ -3420,6 +3451,7 @@
task
task-history
util
enterprise/data-apps
enterprise/serialization
enterprise/transforms-python}
:model-exports #{:model/RemoteSyncTask}
@@ -3428,6 +3460,7 @@
:model/Dashboard
:model/Document
:model/Field
:model/FieldUserSettings
:model/Measure
:model/NativeQuerySnippet
:model/PythonLibrary
@@ -3512,8 +3545,7 @@
settings
system
users
util
enterprise/serialization}
util}
:model-imports #{:model/ApiKey
:model/PermissionsGroup
:model/PermissionsGroupMembership
@@ -3554,6 +3586,7 @@
metabase-enterprise.semantic-search.core
metabase-enterprise.semantic-search.db.datasource
metabase-enterprise.semantic-search.embedding
metabase-enterprise.semantic-search.embedding-health
metabase-enterprise.semantic-search.init}
:uses #{activity-feed
analytics
@@ -3564,6 +3597,7 @@
config
connection-pool
events
health-inspector
lib-be
llm
metabot
@@ -3581,12 +3615,7 @@
enterprise/serialization
{:team "Graphy"
:api #{metabase-enterprise.serialization.api
metabase-enterprise.serialization.cmd
metabase-enterprise.serialization.core
metabase-enterprise.serialization.init
metabase-enterprise.serialization.schema
metabase-enterprise.serialization.v2.backfill-ids}
:api #{metabase-enterprise.serialization.api metabase-enterprise.serialization.cmd metabase-enterprise.serialization.core}
:friends #{enterprise/memoize-monitor}
:uses #{analytics
api
@@ -3602,9 +3631,7 @@
search
server
setup
sync
util
warehouse-schema}
util}
:model-imports :bypass}
enterprise/snippet-collections
@@ -3765,12 +3792,10 @@
premium-features
query-processor
sample-data
server
settings
sql-tools
util
workspaces
enterprise/serialization}
workspaces}
:model-exports #{:model/Workspace}
:model-imports #{:model/Database
:model/Table

View File

@@ -3,3 +3,8 @@
(defmacro with-temp-dbs! [bindings & body]
`(let [~@(interleave bindings (repeat nil))]
~@body))
(defmacro with-routing-setup! [[router-binding destinations] & body]
`(let [~router-binding nil
~@(interleave (map first destinations) (repeat nil))]
~@body))

47
.clj-kondo/ratchets.edn Normal file
View File

@@ -0,0 +1,47 @@
;; Per-linter budgets for inline `:clj-kondo/ignore` forms.
;; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.
;; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it
;; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit
;; to defend in your PR.
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
{:ignore-counts {:aliased-namespace-symbol 3
:all 50
:case-symbol-test 1
:clojure-lsp/unused-public-var 9
:def-fn 1
:deprecated-namespace 97
:deprecated-var 81
:discouraged-java-method 3
:discouraged-namespace 81
:discouraged-var 106
:equals-true 1
:main-without-gen-class 1
:metabase/defsetting-namespace 7
:metabase/disallow-hardcoded-driver-names-in-tests 34
:metabase/discourage-millis-duration 1
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 14
:metabase/modules 8
:metabase/namespace-name 4
:metabase/prefer-with-dynamic-fn-redefs 25
:metabase/test-helpers-use-non-thread-safe-functions 17
:metabase/validate-defendpoint-has-response-schema 441
:metabase/validate-defendpoint-query-params-use-kebab-case 40
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 8
:missing-docstring 10
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3
:redundant-fn-wrapper 1
:syntax 2
:unquote-not-syntax-quoted 1
:unresolved-namespace 3
:unresolved-require 1
:unresolved-symbol 4
:unsorted-required-namespaces 1
:unused-alias 1
:unused-binding 9
:unused-excluded-var 2
:unused-namespace 1
:unused-private-var 8
:unused-value 1}}

View File

@@ -153,6 +153,7 @@
"build."
"i18n." ; bin/i18n
"lint-migrations-file-test"
"mage."
"main-test" ; bin/release-list
"metabase.deps-edn-test"
"metabase.driver."

3
.github/CODEOWNERS vendored
View File

@@ -10,6 +10,7 @@ src/metabase/query_processor @metabase/core-backend-querying-platform
src/metabase/graph @metabase/graphy
enterprise/backend/src/metabase_enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/monitor/dependency-diagnostics @metabase/graphy
# @metabase/embedding
@@ -49,5 +50,5 @@ docs/installation-and-operation/commands.md # auto-generated
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
# @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
frontend/src/metabase/metabot @metabase/metabot-frontend

View File

@@ -38,8 +38,9 @@ runs:
CONFLICT_LIST=$(git ls-files -u)
CONFLICTS=$(git ls-files -u | wc -l)
CONFLICTS=1 # TEMP(test branch): force the conflict path — revert before merge
if [ "$CONFLICTS" -gt 0 ]; then
git cherry-pick --abort
git cherry-pick --abort || true # TEMP(test branch): abort fails when the cherry-pick was clean
echo "Could not cherry pick because of a conflict"
echo "has-conflicts=true" >> $GITHUB_OUTPUT
@@ -57,14 +58,12 @@ runs:
#${ORIGINAL_PULL_REQUEST_NUMBER}
> [!IMPORTANT]
> Manual conflict resolution is required.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Check the box below when done.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Mark the PR as ready for review once conflicts have been addressed.
Conflicts:
\`\`\`shell
${CONFLICT_LIST}
\`\`\`
- [ ] Conflicts resolved
END
)
else
@@ -74,7 +73,12 @@ runs:
git push -u origin ${BACKPORT_BRANCH}
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
if [ "$CONFLICTS" -gt 0 ]; then
PR_ARGS+=(--draft)
fi
BACKPORT_PR_URL=$(gh pr create "${PR_ARGS[@]}")
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT

View File

@@ -8,8 +8,8 @@ default: &default
- "bin/**/!(*.md)"
actionlint:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
- added|modified: ".github/workflows/*.yml"
- added|modified: ".github/workflows/*.yaml"
shared_sources: &shared_sources
- "src/**/*.cljc"

View File

@@ -19,8 +19,7 @@ while [[ $# -gt 0 ]]; do
done
echo "Cloning python-runner repository..."
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git --branch seccomp python-runner
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
if [ "$SKIP_START" = false ]; then
echo "Starting python-runner..."

View File

@@ -0,0 +1,35 @@
name: TEMP test create-backport
# TEMPORARY — delete before merging, along with the TEMP lines in
# .github/actions/create-backport/action.yml that force CONFLICTS=1.
#
# Opens a real backport PR against release-x.<CURRENT_VERSION>.x. Because the
# action is forced down its conflict path, the PR is created as a draft and the
# auto-approve / auto-merge steps are skipped — nothing can merge.
#
# Label-gated: this opens a real PR, so it must not fire on every push.
on:
pull_request:
types: [labeled]
jobs:
test:
if: github.event.label.name == 'test-backport'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Pick a commit to backport
id: commit
run: echo "sha=$(git rev-parse origin/master)" >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/create-backport
with:
target-version: ${{ vars.CURRENT_VERSION }}
backport-commit: ${{ steps.commit.outputs.sha }}
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
github-token-2: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,83 @@
# CodeQL's overlay analysis saves a ~400MB "base database" cache on every master run, keyed by commit SHA.
#
# Only the newest entry is ever restored: CodeQL looks its cache up by key *prefix* and takes the most recent match,
# so everything behind it is dead weight. This job keeps the newest few and deletes the rest.
name: CodeQL Cache Cleanup
on:
schedule:
- cron: "0 4 * * *" # daily at 04:00 UTC
workflow_dispatch:
inputs:
keep:
description: "How many of the newest CodeQL caches to keep"
required: false
default: "2"
dry-run:
description: "List what would be deleted without deleting it"
type: boolean
required: false
default: false
concurrency:
group: cache-cleanup-codeql
cancel-in-progress: false
permissions:
actions: write # required to delete cache entries
contents: read
jobs:
cleanup:
# don't run the schedule on forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
KEEP: ${{ github.event.inputs.keep || '2' }}
DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }}
PREFIX: codeql-overlay-base-database-
steps:
- name: Delete superseded CodeQL caches
shell: bash
run: |
set -euo pipefail
# created_at is ISO-8601, so a reverse lexicographic sort is newest-first.
gh api --paginate "/repos/${GITHUB_REPOSITORY}/actions/caches?per_page=100" \
--jq ".actions_caches[] | select(.key | startswith(\"${PREFIX}\")) | \"\(.created_at)\t\(.id)\t\(.size_in_bytes)\t\(.key)\"" \
| sort -r > all.tsv || true
found=$(wc -l < all.tsv | tr -d ' ')
echo "Found $found cache(s) matching '${PREFIX}'; keeping the newest $KEEP."
if [ "$found" -eq 0 ]; then
echo "Nothing to do." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
tail -n +$((KEEP + 1)) all.tsv > doomed.tsv || true
doomed=$(wc -l < doomed.tsv | tr -d ' ')
freed=0
while IFS=$'\t' read -r created id size key; do
[ -z "${id:-}" ] && continue
echo " deleting $key ($((size / 1000000))MB, created $created)"
if [ "$DRY_RUN" = "true" ]; then
echo " (dry run — not deleted)"
else
# A cache can vanish between listing and deleting (concurrent eviction); don't fail the job over it.
gh api --method DELETE "/repos/${GITHUB_REPOSITORY}/actions/caches/${id}" >/dev/null \
|| echo " (already gone, skipping)"
fi
freed=$((freed + size))
done < doomed.tsv
{
echo "### CodeQL cache cleanup"
echo ""
echo "- matched: **$found**"
echo "- kept (newest): **$KEEP**"
echo "- deleted: **$doomed**"
echo "- reclaimed: **$((freed / 1000000)) MB**${DRY_RUN:+ (dry run: $DRY_RUN)}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -250,8 +250,7 @@ jobs:
const message = [
`:announcement-animated: ${eng} The v${next} release branch has been cut.`,
`- \`backport\`s will go to v${next} now`,
`- If you want to go back to v${current}, you'll need to add the double-backport tag`,
`Add the \`backport\` label to backport changes to all supported release branches.`,
].join('\n');
await sendSlackMessage({

View File

@@ -1,22 +0,0 @@
name: Auto-update documentation (release)
on:
schedule:
- cron: "0 23 * * 1-5" # Every Monday through Friday at 11 PM UTC
workflow_dispatch:
inputs:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.63.x"
type: string
jobs:
update-release-docs:
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -8,7 +8,7 @@ on:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.62.x"
default: "release-x.63.x"
type: string
jobs:
@@ -17,6 +17,6 @@ jobs:
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.62.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -64,6 +64,11 @@ jobs:
# workspace where the gate step runs; an absolute path anchored to the
# workspace makes both agree regardless of cwd.
QUARANTINE_FAILURES_FILE: ${{ github.workspace }}/target/quarantine-failures.jsonl
# Cypress records video for every spec but the passing specs' videos are
# deleted in after:spec so recording is ~5-10% slowdown per shard and almost
# never yields an artifact. Failure screenshots are unaffected, and the
# stress-test workflows have their own config and keep video.
CYPRESS_VIDEO: "false"
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
@@ -123,9 +128,20 @@ jobs:
- name: Get failed specs
if: github.run_attempt != '1'
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
run: echo "specs=$(cat failed-specs)" >> "$GITHUB_ENV"
# run-tests.yml compiles this in parallel with the uberjar build. Downloading it is ~10x faster than compiling it here.
# Callers without that job (stress tests, coverage shards, cross-run re-runs) fall through to compiling as before.
- name: Download pre-built CLJS
id: prebuilt-cljs
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
- name: Compile CLJS
if: steps.prebuilt-cljs.outcome != 'success'
run: bun run build-pure:cljs
- name: Run Metabase

View File

@@ -0,0 +1,85 @@
name: Kondo Ratchets Self-Heal
# Stale budgets in .clj-kondo/ratchets.edn (above the actual ignore counts) fail CI. On PRs
# labelled kondo-ratchets-self-healing, this commits the lowered budgets to the branch;
# unlabelled PRs rely on local test runs to tighten the file.
#
# Two jobs so the automation token never shares an environment with PR-controlled code: the
# ratchet-down job runs ./bin/mage from the PR branch and only exports the fixed file as an
# artifact; the push job starts from a clean runner, executes nothing from the PR, and pushes.
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- "**.clj"
- "**.cljc"
- "**.cljs"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
ratchet-down:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
# fork PRs don't get the automation token, so we couldn't push to them anyway
if: >-
contains(github.event.pull_request.labels.*.name, 'kondo-ratchets-self-healing')
&& github.event.pull_request.head.repo.full_name == github.repository
outputs:
changed: ${{ steps.diff.outputs.changed }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Lower ignore budgets to match the source tree
run: ./bin/mage fix-kondo-ratchets
- name: Check for changes
id: diff
run: |
if git diff --quiet .clj-kondo/ratchets.edn; then
echo "budgets already tight"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/upload-artifact@v7
if: steps.diff.outputs.changed == 'true'
with:
name: ratchets-edn
path: .clj-kondo/ratchets.edn
# dot-directories are excluded by default, which would ship an empty artifact
include-hidden-files: true
push:
needs: ratchet-down
if: needs.ratchet-down.outputs.changed == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: ratchets-edn
path: .clj-kondo/
- name: Commit to the PR branch
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
# METABASE_AUTOMATION_USER_TOKEN so the self-healing commit triggers CI
# (a GITHUB_TOKEN push would not)
AUTOMATION_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
git config --global user.name "Metabase Automation"
git config --global user.email "github-automation@metabase.com"
git add .clj-kondo/ratchets.edn
git commit -m "Ratchet down kondo ignore budgets" --no-verify
git push "https://x-access-token:${AUTOMATION_TOKEN}@github.com/${REPO}.git" "HEAD:$PR_BRANCH"

View File

@@ -24,15 +24,42 @@ jobs:
check-migration-versions:
if: github.event_name == 'pull_request'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
timeout-minutes: 10 # the `backport`-label path builds the release scripts
steps:
- uses: actions/checkout@v6
with:
filter: blob:none
fetch-depth: 0
# The `backport` label backports to every supported release line (see auto-backport.yml),
# so migrations on such a PR must sit in the oldest supported line's directory. Resolve
# that floor with the same getSupportedMajors the backport workflow uses; skip the
# release-script build entirely for PRs without the label.
- name: Prepare release scripts
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: ./.github/actions/prepare-release-scripts
- name: Compute oldest supported backport target
id: oldest-supported
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: actions/github-script@v9
env:
AWS_S3_STATIC_BUCKET: ${{ vars.AWS_S3_STATIC_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION }}
with:
script: | # js
const { getSupportedMajors } = require('${{ github.workspace }}/release/dist/index.cjs');
const targets = await getSupportedMajors();
console.log('supported backport targets', targets);
if (!targets.length || !targets.every(Number.isInteger)) {
throw new Error(`getSupportedMajors() returned no usable majors: ${JSON.stringify(targets)}`);
}
return Math.min(...targets);
- name: Check migration versions
env:
BASE_REF: ${{ github.base_ref }}
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
OLDEST_SUPPORTED_VERSION: ${{ steps.oldest-supported.outputs.result }}
run: bin/check-migration-versions

View File

@@ -19,40 +19,7 @@ jobs:
run: bun install --frozen-lockfile
- name: get module boundary stats
id: get-stats
run: |
NAMED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const named = elements.filter(e => !['shared/other', 'app/misc', 'other'].includes(e.type));
console.log(named.length);
")
ENFORCED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const enforced = new Set(elements.filter(e => e.enforceOutgoing).map(e => e.type));
console.log(enforced.size);
")
NAMED_DIRS=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const dirs = new Set();
for (const el of elements) {
const m = el.pattern.match(/^frontend\/src\/metabase\/(\w[\w-]*)\//)
if (m) dirs.add(m[1]);
}
for (const d of dirs) console.log(d);
")
TOTAL_DIRS=$(ls -d frontend/src/metabase/*/ | xargs -I{} basename {} | wc -l | tr -d ' ')
NAMED_COUNT=$(echo "$NAMED_DIRS" | grep -c . || true)
UNMODULED=$(($TOTAL_DIRS - $NAMED_COUNT))
VIOLATIONS=$(bun run module-boundaries 2>&1 | grep -o '✖ [0-9]* problems' | grep -o '[0-9]*' || echo "0")
echo "$NAMED_MODULES modules, $ENFORCED_MODULES enforced, $UNMODULED unmoduled folders, $VIOLATIONS violations"
echo "NAMED_MODULES=$NAMED_MODULES" >> $GITHUB_OUTPUT
echo "ENFORCED_MODULES=$ENFORCED_MODULES" >> $GITHUB_OUTPUT
echo "UNMODULED=$UNMODULED" >> $GITHUB_OUTPUT
echo "VIOLATIONS=$VIOLATIONS" >> $GITHUB_OUTPUT
run: node frontend/lint/module-boundaries-stats.mjs | tee -a "$GITHUB_OUTPUT"
- name: Upload stats
uses: actions/github-script@v9
env:

View File

@@ -34,6 +34,12 @@ jobs:
commit: ${{ steps.compute.outputs.commit }}
should_release: ${{ steps.compute.outputs.should_release }}
steps:
- name: Check if running on a fork
run: |
if [ "${{ github.repository }}" != "metabase/metabase" ]; then
echo "This workflow should not run on forks."
exit 1
fi
- uses: actions/checkout@v6
with:
sparse-checkout: |
@@ -125,7 +131,7 @@ jobs:
core.setOutput('should_release', 'false');
- name: Write job summary
if: always()
if: always() && github.repository == 'metabase/metabase'
shell: bash
run: |
{
@@ -157,3 +163,12 @@ jobs:
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: [prepare, trigger-release]
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}

View File

@@ -1,46 +1,103 @@
# Step 2 of the SDK release process: merging a version-bump PR (opened by
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) publishes the version
# and dist-tag committed in package.template.json's `sdkRelease` block.
#
# workflow_dispatch publishes the same way, from the branch chosen in the ref
# picker, without a PR. It reads the same committed sdkRelease block and never
# bumps the version - so it is not how you cut a release. Use it only to publish
# a version that is already committed, when the bump PR cannot do it.
#
# To retry a failed run, re-run the failed jobs on that run rather than
# dispatching a fresh one: if publish-npm already succeeded, a fresh run would
# stop at check-git-tag on the existing tag.
name: Release Metabase Embedding SDK for React
run-name: Release SDK from ${{ inputs.branch }}
run-name: Release SDK from ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.pull_request.base.ref }}
on:
workflow_dispatch:
inputs:
branch:
description: "Branch we want to release the SDK from (to release from other branches use the workflow from those branches)"
read_first:
description: |
⚠️ To cut a release, use the "SDK Version Bump PR" workflow instead — it bumps the version and opens a PR; merging that PR publishes.
This publishes the version already committed in package.template.json. It never bumps. Pick the branch in "Use workflow from" above.
Use it only when neither the bump PR nor re-running the failed jobs will do.
type: choice
required: true
default: "master"
required: false
options:
- master
- Publish the committed version from the branch picked in "Use workflow from" above
# Step 2 of the new release process: merging a version-bump PR (opened by
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) triggers a release
# of whatever branch that PR targeted. See `prepare-release` below.
#
# The `paths` filter keeps this from creating a run for every PR closed
# anywhere in the repo - version-bump PRs always touch package.template.json,
# so this is a reliable pre-filter ahead of the label/merge check below.
pull_request:
types: [closed]
paths:
- enterprise/frontend/src/embedding-sdk-package/package.template.json
concurrency:
# We want to ensure only one job is running at a time per branch because
# there could be a conflict when updating the readme file.
group: ${{ github.workflow }}-${{ inputs.branch }}
cancel-in-progress: true
# One release at a time per branch; base.ref is empty on workflow_dispatch, so
# it falls back to the ref picker's branch. Never cancel in progress - a queued
# release that turns out stale fails loudly at check-git-tag.
#
# Non-release closed PRs queue here too and skip in seconds. GitHub keeps only
# one pending run per group and cancels any earlier one, so one could in theory
# evict a release waiting to start; that needs two same-branch releases at once.
# `queue: max` would fix it, but actionlint cannot parse it (rhysd/actionlint#654).
group: sdk-release-${{ github.event.pull_request.base.ref || github.ref_name }}
cancel-in-progress: false
jobs:
# Gates the whole workflow: either a manual workflow_dispatch, or a merged
# PR carrying the sdk-release-trigger label. Every other job depends
# (directly or transitively) on this one, so if it's skipped the rest of
# the run is skipped too - no separate `if:` needed on downstream jobs.
prepare-release:
name: Prepare release trigger
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'sdk-release-trigger'))
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
branch: ${{ steps.resolve.outputs.branch }}
steps:
# base.ref is only set on a pull_request event; on a dispatch it is empty,
# so this falls through to the branch picked in the ref picker.
- name: Resolve target branch
id: resolve
run: echo "branch=${{ github.event.pull_request.base.ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
workflow-summary:
name: Log inputs
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Generate workflow summary
env:
BRANCH: ${{ needs.prepare-release.outputs.branch }}
run: |
tee -a $GITHUB_STEP_SUMMARY << EOF
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**Inputs:**
- \`branch\`: ${{ inputs.branch }}
- \`branch\`: $BRANCH
- \`trigger\`: ${{ github.event_name }}
triggered by: @${{ github.event.sender.login }}
EOF
send-slack-notification-start:
name: "Send Slack notification: SDK release started"
needs: determine-sdk-version
# Builds release/dist/index.cjs once, for every Slack job to download. It hangs
# off prepare-release only - not off the metadata read or anything downstream -
# so the artifact exists even when the release fails at its very first step,
# which is exactly when send-slack-notification-failure needs it.
build-release-scripts:
name: Build release scripts
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
steps:
- uses: actions/checkout@v6
with:
@@ -51,12 +108,28 @@ jobs:
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Cache build scripts
- name: Upload release scripts
uses: actions/upload-artifact@v7
with:
name: build-scripts
path: release/dist/index.cjs
send-slack-notification-start:
name: "Send Slack notification: SDK release started"
needs:
- build-release-scripts
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
steps:
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
path: release/dist/
- name: Send build start message
id: send-slack-message-start
uses: actions/github-script@v9
@@ -66,7 +139,7 @@ jobs:
result-encoding: string
script: | # js
const { mentionUserByGithubLogin, sendSlackMessage, githubRunLink } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const title = `:rocket: *SDK release: ${sdkVersion}* :rocket:`
const runLink = githubRunLink(
@@ -87,23 +160,22 @@ jobs:
send-slack-notification-failure:
name: "Send Slack notification: SDK release failed"
needs:
[
"determine-sdk-version",
"send-slack-notification-start",
"check-git-tag",
"test",
"build-sdk",
"publish-npm",
"tag-latest",
"git-tag",
]
- build-release-scripts
- read-sdk-release-metadata
- send-slack-notification-start
- check-git-tag
- test
- build-sdk
- publish-npm
- tag-latest
- git-tag
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
if: ${{ always() && contains(needs.*.result, 'failure') }}
steps:
- name: Retrieve build scripts
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
@@ -115,8 +187,9 @@ jobs:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const { githubRunLink, sendSlackReply, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const messageId = "${{ env.slackMessageId }}";
const runLink = githubRunLink(
`:x: ${sdkVersion} Release has failed`,
@@ -124,23 +197,34 @@ jobs:
context.repo.owner,
context.repo.repo,
);
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId: ${{ env.slackMessageId}},
broadcast: true
});
if (messageId) {
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId,
broadcast: true,
});
} else {
sendSlackMessage({
channelName: 'team-embedding-releases',
message: runLink,
});
}
send-slack-notification-successful:
name: "Send Slack notification: SDK release successful"
needs:
["determine-sdk-version", "send-slack-notification-start", "publish-npm"]
- build-release-scripts
- read-sdk-release-metadata
- send-slack-notification-start
- publish-npm
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
steps:
- name: Retrieve build scripts
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
@@ -153,7 +237,7 @@ jobs:
with:
script: | # js
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const runLink = githubRunLink(
`:partydeploy: ${sdkVersion} Release is complete :partydeploy:`,
@@ -164,93 +248,104 @@ jobs:
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId: ${{ env.slackMessageId}},
messageId: "${{ env.slackMessageId }}",
broadcast: true
});
determine-sdk-version:
# Reads the version + release metadata straight from package.template.json
# instead of computing them (the version and sdkRelease.distTag/tagAsLatest
# are already correct on this branch, because sdk-version-bump-pr.yml wrote
# them there and a human reviewed them before merging).
read-sdk-release-metadata:
name: Read SDK release metadata
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
timeout-minutes: 5
outputs:
sdk_version: ${{ fromJson(steps.new-sdk-version.outputs.result).version }}
major_version: ${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}
pre_release_label: ${{ fromJson(steps.new-sdk-version.outputs.result).preReleaseLabel }}
sdk_version: ${{ steps.metadata.outputs.sdk_version }}
major_version: ${{ steps.metadata.outputs.major_version }}
dist_tag: ${{ steps.metadata.outputs.dist_tag }}
tag_as_latest: ${{ steps.metadata.outputs.tag_as_latest }}
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Get current SDK version
id: current-sdk-version
run: |
VERSION=$(jq -r .version ./enterprise/frontend/src/embedding-sdk-package/package.template.json)
echo "sdk_current_version=$VERSION" >> $GITHUB_OUTPUT
- name: Get next SDK patch or pre-release version
id: new-sdk-version
- name: Read version and sdkRelease metadata
id: metadata
uses: actions/github-script@v9
with:
script: | #js
const { getNextSdkVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
script: | # js
const { readSdkReleaseMetadata } = require('${{ github.workspace }}/release/dist/index.cjs');
const branch = "${{ inputs.branch }}";
const currentVersion = "${{ steps.current-sdk-version.outputs.sdk_current_version }}";
let metadata;
try {
metadata = readSdkReleaseMetadata({
packageTemplatePath:
'enterprise/frontend/src/embedding-sdk-package/package.template.json',
});
} catch (error) {
core.setFailed(error.message);
return;
}
const result = getNextSdkVersion(branch, currentVersion);
return {
version: result.version,
preReleaseLabel: result.preReleaseLabel,
majorVersion: result.majorVersion
};
core.setOutput('sdk_version', metadata.version);
core.setOutput('major_version', metadata.majorVersion);
core.setOutput('dist_tag', metadata.distTag);
core.setOutput('tag_as_latest', String(metadata.tagAsLatest));
- name: Append workflow summary
run: |
tee -a $GITHUB_STEP_SUMMARY << EOF
**Derived inputs:**
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**SDK release metadata (from package.template.json):**
- \`current_version\`: "${{ steps.current-sdk-version.outputs.sdk_current_version }}"
- \`next_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).version }}"
- \`major_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}"
- \`version\`: "${{ steps.metadata.outputs.sdk_version }}"
- \`major_version\`: "${{ steps.metadata.outputs.major_version }}"
- \`distTag\`: "${{ steps.metadata.outputs.dist_tag }}"
- \`tagAsLatest\`: "${{ steps.metadata.outputs.tag_as_latest }}"
EOF
check-git-tag:
needs: determine-sdk-version
needs:
- prepare-release
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
fetch-depth: 0
fetch-tags: true
- name: check tags
run: |
if [ $(git tag -l | grep $tag$) ]
if git tag -l | grep -q "${tag}$"
then
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow again."
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow."
exit 1
else
exit 0
fi
test:
needs: check-git-tag
needs:
- prepare-release
- check-git-tag
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
@@ -262,14 +357,16 @@ jobs:
run: bun run embedding-sdk:test-unit
build-sdk:
needs: [test, determine-sdk-version]
needs:
- prepare-release
- test
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
fetch-depth: 0
fetch-tags: true
@@ -279,11 +376,9 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Bump published npm package version
# NOTE: this should happen before "Build SDK bundle" as we inject SDK version into the code during build step
run: |
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ needs.determine-sdk-version.outputs.sdk_version }}
# No version bump here anymore: package.template.json's version is
# already correct on this branch (bumped by sdk-version-bump-pr.yml
# before merge), so we build with it as-is.
- name: Build SDK package
run: bun run build-embedding-sdk-package
@@ -297,94 +392,22 @@ jobs:
path: ./resources/embedding-sdk
publish-npm:
needs: [build-sdk, determine-sdk-version]
needs:
- prepare-release
- build-sdk
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
permissions:
id-token: write # OIDC handshake for npm trusted publishing
actions: read # actions/download-artifact reads the Actions API via GITHUB_TOKEN
pull-requests: write # gh pr review --approve step uses GITHUB_TOKEN
env:
sdk_version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
steps:
- name: Check out branch to prepare for a SDK version bump PR
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
# when we created version update PR, it would only have diff from 2 files SDK readme and SDK package.json template
ref: ${{ inputs.branch}}
- name: Setup git user
run: |
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase Automation"
- name: Update readme
if: inputs.branch != 'master'
run: |
bash ./bin/embedding-sdk/release_utils.bash update_readme ${{ env.sdk_version }}
- name: Bump published npm package version
run: |
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ env.sdk_version }}
- name: Create a PR updating readme + published version (using GitHub Metabase Automation account)
run: |
git checkout -b update-sdk-version-${{ env.sdk_version }}
if [ "${{ inputs.branch }}" = "master" ]; then
COMMIT_MSG="chore(sdk): Update published npm version to ${{ env.sdk_version }}"
PR_TITLE="chore(sdk): Update SDK version to ${{ env.sdk_version }}"
PR_BODY="Update published npm package version to ${{ env.sdk_version }}"
else
COMMIT_MSG="docs(sdk): Update Readme version references and published npm version to ${{ env.sdk_version }}"
PR_TITLE="docs(sdk): Update SDK version to ${{ env.sdk_version }}"
PR_BODY="Update Readme version references and published npm package version to ${{ env.sdk_version }}"
fi
git commit -a -m "$COMMIT_MSG"
git push origin HEAD
gh pr create --base "${{ inputs.branch }}" \
--assignee "${GITHUB_ACTOR}" \
--title "$PR_TITLE" \
--body "$PR_BODY"
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Edit PR adding useful information (using Metabase bot app)
# metabase/embedding team is added automatically as code owner of updated `package.template.json` file
run: |
gh pr edit --add-reviewer albertoperdomo --add-label no-backport
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Auto approve PR (using GitHub Actions account)
run: |
gh pr review --approve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Pull request auto-merge (using GitHub Metabase Automation account)
run: |
gh pr merge --auto --squash
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Retrieve build SDK package artifact
uses: actions/download-artifact@v8
with:
name: metabase-sdk
path: sdk
- name: Compute master tag
id: compute-tag
run: |
if [ -n "${{ needs.determine-sdk-version.outputs.pre_release_label }}" ]; then
echo "masterTag=${{ needs.determine-sdk-version.outputs.pre_release_label }}" >> $GITHUB_OUTPUT
else
echo "masterTag=nightly" >> $GITHUB_OUTPUT
fi
# Trusted publishing requires npm >= 11.5.1 and Node >= 22.14.0.
# Node 22.x line bundles npm 10.x, so pin Node 24.x which ships npm 11.x.
- name: Setup Node.js for npm publish with trusted publishing
@@ -396,34 +419,41 @@ jobs:
- name: Publish to NPM
working-directory: sdk
run: |
TAG="${{ steps.compute-tag.outputs.masterTag }}"
TAG="${{ needs.read-sdk-release-metadata.outputs.dist_tag }}"
echo "Resolved npm tag: $TAG"
npm publish --tag "$TAG"
# When a newer release branch goes gold, remove this entire job from this branch.
tag-latest:
name: Tag npm release as latest
# Pre-gold: remove `if: false` once this branch becomes gold.
if: false
needs: [publish-npm, determine-sdk-version]
# Driven by package.template.json's sdkRelease.tagAsLatest instead of a
# hardcoded `if: false` that had to be hand-edited (uncommented) once a
# branch went gold - see sdk-version-bump-pr.yml, which is where a human
# sets this to true when appropriate.
if: ${{ needs.read-sdk-release-metadata.outputs.tag_as_latest == 'true' }}
needs:
- publish-npm
- read-sdk-release-metadata
uses: ./.github/workflows/embedding-sdk-tag-latest.yml
with:
version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
version: ${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
secrets: inherit
git-tag:
needs: [publish-npm, determine-sdk-version]
needs:
- prepare-release
- publish-npm
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
stable_tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.major_version }}-stable
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
stable_tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.major_version }}-stable
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Setup git user
run: |
@@ -436,7 +466,7 @@ jobs:
git push origin ${{ env.tag }}
- name: Create and push SDK stable version tag
if: ${{ startsWith(inputs.branch, 'release-x.') }}
if: ${{ startsWith(needs.prepare-release.outputs.branch, 'release-x.') }}
run: |
git tag -a -f ${{ env.stable_tag }} -m "Tagging SDK stable version ${{ env.stable_tag }}"
git push origin -f ${{ env.stable_tag }}

View File

@@ -15,20 +15,30 @@ jobs:
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
AUTHOR_NAME: ${{ github.event.workflow_run.head_commit.author.name }}
AUTHOR_LOGIN: ${{ github.event.workflow_run.head_commit.author.username }}
steps:
- name: Generate job summary
run: |
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:" >> $GITHUB_STEP_SUMMARY
echo "${{ github.event.workflow_run.html_url }}" >> $GITHUB_STEP_SUMMARY
{
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:"
echo ""
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:"
echo "${{ github.event.workflow_run.html_url }}"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/setup-node@v6.4.0
- uses: actions/checkout@v6
with:
node-version: lts/Jod # 22.x.x
- run: npm install @slack/web-api
sparse-checkout: |
.github
release
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Trigger a re-run
uses: actions/github-script@v9
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const MAX_ATTEMPTS = 1;
@@ -37,6 +47,7 @@ jobs:
const FAILED_RUN_NAME = "${{ github.event.workflow_run.name }}";
const BREAKING_COMMIT = "${{ github.event.workflow_run.head_sha }}";
const AUTHOR = process.env.AUTHOR_NAME;
const AUTHOR_LOGIN = process.env.AUTHOR_LOGIN;
const BRANCH = process.env.BRANCH_NAME;
if (ATTEMPT <= MAX_ATTEMPTS) {
@@ -55,8 +66,7 @@ jobs:
}
} else if (!BRANCH.includes('backport-')) {
// notify slack of repeated failure
const { WebClient } = require('@slack/web-api');
const slack = new WebClient('${{ secrets.SLACK_BOT_TOKEN }}');
const { mentionUserByGithubLogin, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
// find out which jobs failed
const { owner, repo } = context.repo;
@@ -87,9 +97,9 @@ jobs:
]
}));
await slack.chat.postMessage({
channel: 'engineering-ci',
text: 'Failing tests',
await sendSlackMessage({
channelName: 'engineering-ci',
message: 'Failing tests',
blocks: [
{
"type": "header",
@@ -107,7 +117,7 @@ jobs:
"type": "section",
"text": {
"type": "mrkdwn",
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} (${mentionUserByGithubLogin(AUTHOR_LOGIN)}) has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
},
},
{

View File

@@ -93,13 +93,63 @@ jobs:
uses: ./.github/workflows/uberjar.yml
secrets: inherit
# Compile CLJS in parallel with the uberjar and pass it to each e2e group.
# The compile is doesn't depend on MB_EDITION
e2e-cljs:
name: Compile CLJS for e2e
needs: [create-test-plan]
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' && needs.create-test-plan.outputs.e2e_all == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Upload compiled CLJS
uses: actions/upload-artifact@v7
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
if-no-files-found: error
# Uploads fail when an artifact of this name already exists in the run
# replace it rather than fail on a re-run.
overwrite: true
static-viz-files-changed:
name: Check whether static-viz files changed
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') && !contains(github.event.pull_request.labels.*.name,'minimal-tests') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
static_viz: ${{ steps.static_viz.outputs.static_viz }}
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build static-viz frontend
run: bun run build-static-viz
env:
MB_EDITION: ee
- name: Check for static viz changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: static_viz
with:
token: ${{ github.token }}
filters: .github/static-viz-sources.yaml
backend-tests:
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' }}
needs: [create-test-plan]
needs: [create-test-plan, static-viz-files-changed]
uses: ./.github/workflows/backend.yml
secrets: inherit
with:
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' }}
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' && needs.static-viz-files-changed.outputs.static_viz != 'true' }}
semantic-search-tests:
needs: create-test-plan

View File

@@ -0,0 +1,180 @@
# Step 1 of the SDK release process: bump the version (and the sdkRelease
# metadata that controls the npm dist-tag / `latest` promotion) in
# package.template.json, then open a PR labeled `sdk-release-trigger`.
# Merging that PR is what actually triggers a release, via the
# `pull_request` trigger on release-embedding-sdk.yml.
name: SDK Version Bump PR
run-name: Bump Embedding SDK version (${{ inputs.release_type }}) on ${{ github.ref_name }}
on:
workflow_dispatch:
inputs:
read_first:
description: "Bumps the version on the branch chosen in 'Use workflow from' above."
type: choice
required: false
options:
- Pick the branch in "Use workflow from" above
release_type:
description: |
Before gold:
• beta — release branch: publish a beta
At gold:
• patch — release branch: graduate the beta to 0.N.0
• preminor — master: move master to the next major
After gold:
• patch — release branch: ship a patch (0.N.1, 0.N.2, ...)
Any time:
• alpha — master: bump the development version
• custom — one-off branch (esbuild, data-apps): publish under its own prerelease id
type: choice
required: true
options:
- alpha
- beta
- preminor
- patch
- custom
prerelease_id:
description: "release_type=custom only, and only on the FIRST cut of a one-off branch (e.g. data-apps). Becomes the prerelease id in the version (0.62.5-data-apps.0) and the npm dist-tag (62-data-apps). Leave empty on later bumps - the id is reused from the current version."
type: string
required: false
concurrency:
# one version-bump in flight per branch at a time, to avoid racing PRs
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
bump-version:
name: Bump SDK version and open review PR
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
env:
package_template: enterprise/frontend/src/embedding-sdk-package/package.template.json
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Setup git user
run: |
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase Automation"
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Compute version and sdkRelease metadata
id: metadata
uses: actions/github-script@v9
# Pass inputs via env, not inlined `${{ }}` in the script: github-script
# interpolates expressions into the JS source before running it, so a
# free-form input like prerelease_id could inject code. env values are
# read at runtime and can't escape into the code.
env:
BRANCH: ${{ github.ref_name }}
RELEASE_TYPE: ${{ inputs.release_type }}
PRERELEASE_ID: ${{ inputs.prerelease_id }}
LATEST_MAJOR_VERSION: ${{ vars.LATEST_MAJOR_VERSION }}
PACKAGE_TEMPLATE: ${{ env.package_template }}
with:
script: | # js
const { applySdkVersionBump } = require('${{ github.workspace }}/release/dist/index.cjs');
let result;
try {
result = applySdkVersionBump({
packageTemplatePath: process.env.PACKAGE_TEMPLATE,
branch: process.env.BRANCH,
releaseType: process.env.RELEASE_TYPE,
prereleaseId: process.env.PRERELEASE_ID,
latestMajorVersion: process.env.LATEST_MAJOR_VERSION,
});
} catch (error) {
core.setFailed(error.message);
return;
}
core.setOutput("previous_version", result.previousVersion);
core.setOutput("new_version", result.newVersion);
core.setOutput("major_version", result.majorVersion);
core.setOutput("dist_tag", result.distTag);
core.setOutput("tag_as_latest", String(result.tagAsLatest));
- name: Append workflow summary
env:
BRANCH: ${{ github.ref_name }}
run: |
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**SDK version bump:**
- \`branch\`: $BRANCH
- \`release_type\`: ${{ inputs.release_type }}
- \`previous_version\`: ${{ steps.metadata.outputs.previous_version }}
- \`new_version\`: ${{ steps.metadata.outputs.new_version }}
- \`distTag\` (default, review before merging): ${{ steps.metadata.outputs.dist_tag }}
- \`tagAsLatest\` (auto-computed from \`vars.LATEST_MAJOR_VERSION\`, review before merging): ${{ steps.metadata.outputs.tag_as_latest }}
triggered by: @${{ github.event.sender.login }}
EOF
- name: Commit version bump
env:
BUMP_BRANCH: sdk-version-bump-${{ steps.metadata.outputs.new_version }}
run: |
# A leftover branch means an earlier bump to this same version was
# abandoned. Say so, rather than pushing a near-duplicate alongside it.
if git ls-remote --exit-code --heads origin "$BUMP_BRANCH" >/dev/null 2>&1; then
echo "::error::'$BUMP_BRANCH' already exists on origin - an earlier bump to this version was never merged. Close its PR, delete the branch, then re-run."
exit 1
fi
git checkout -b "$BUMP_BRANCH"
git add "$package_template"
git commit -m "chore(sdk): bump SDK version to ${{ steps.metadata.outputs.new_version }} (${{ inputs.release_type }})"
git push origin HEAD
- name: Open version-bump PR (using GitHub Metabase Automation account)
id: open-pr
env:
BRANCH: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
# TODO (EMB-2095) auto-approve and auto-merge this PR once the computed
# version/distTag/tagAsLatest have proven out over a few real releases.
# Nothing gates on review today - merging is only manual as a seatbelt.
#
# --reviewer tags the team so a release is visible, not because anything
# waits on a review. It needs the automation PAT: GITHUB_TOKEN cannot
# resolve an org team.
gh pr create --base "$BRANCH" \
--assignee "${GITHUB_ACTOR}" \
--reviewer metabase/embedding \
--label sdk-release-trigger \
--title "chore(sdk): bump SDK version on $BRANCH to ${{ steps.metadata.outputs.new_version }}" \
--body "$(cat <<'BODY'
Bumps the Embedding SDK version in `package.template.json` and updates its `sdkRelease` metadata.
| | |
|---|---|
| Branch | `${{ github.ref_name }}` |
| Release type | `${{ inputs.release_type }}` |
| Version | `${{ steps.metadata.outputs.previous_version }}` → `${{ steps.metadata.outputs.new_version }}` |
| npm dist-tag | `${{ steps.metadata.outputs.dist_tag }}` |
| Tag as `latest` | `${{ steps.metadata.outputs.tag_as_latest }}` |
`distTag` and `tagAsLatest` are computed from the branch and release type. `tagAsLatest` is `true` only when patching the current gold major (`vars.LATEST_MAJOR_VERSION`).
Merging this PR (with the `sdk-release-trigger` label) triggers [release-embedding-sdk.yml](../actions/workflows/release-embedding-sdk.yml), which builds, publishes to npm, and tags the release.
_Opened by the [SDK Version Bump PR workflow](../actions/workflows/sdk-version-bump-pr.yml), triggered by @${{ github.event.sender.login }}._
BODY
)"

View File

@@ -140,8 +140,8 @@ jobs:
run: |
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
git checkout $RELEASE_BRANCH
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch" \
(git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch") \
|| (echo "Commit not found in correct release branch" && exit 1)
git checkout -
@@ -196,7 +196,7 @@ jobs:
version='${{ needs.check-version.outputs.oss }}'
fi
echo "VERSION=$version" >> $GITHUB_ENV
echo "VERSION=$version" >> "$GITHUB_ENV"
- name: Setup Java
uses: actions/setup-java@v5.2.0
with:
@@ -215,9 +215,9 @@ jobs:
echo "Old version.properties:"
cat version.properties
sed -i "s/^tag=.*/tag=$VERSION/" version.properties
echo "\n\nNew version.properties:"
printf "\n\nNew version.properties:"
cat version.properties
echo "\n\nUpdating metabase.jar with new version.properties"
printf "\n\nUpdating metabase.jar with new version.properties"
jar uf metabase.jar version.properties
- name: Store commit's SHA-1 hash
@@ -280,12 +280,3 @@ jobs:
with:
commit: ${{ inputs.commit }}
version: ${{ inputs.version }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: run-pre-release-tests
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ inputs.version }}
commit: ${{ inputs.commit }}

2
.gitignore vendored
View File

@@ -89,6 +89,7 @@ cypress.env.json
/resources/frontend_client/app/embedding-sdk.js.map
/resources/frontend_client/app/embedding-sdk/
/resources/frontend_client/tmp-*/
/resources/frontend_client/data-app.html
/resources/frontend_client/embed.html
/resources/frontend_client/embed-sdk.html
/resources/frontend_client/embed-mcp.html
@@ -150,6 +151,7 @@ mise.local.toml
**/.clj-kondo/*
!**/.clj-kondo/README.md
!**/.clj-kondo/config.edn
!**/.clj-kondo/ratchets.edn
!**/.clj-kondo/config/
!**/.clj-kondo/src/
!**/.clj-kondo/test/

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,27 @@
import { Comment, Converter } from "typedoc";
const TAG = "@notExported";
export function load(app) {
app.options.getDeclaration("blockTags").defaultValue?.push?.(TAG);
app.converter.on(Converter.EVENT_RESOLVE_END, (context) => {
const project = context.project;
const intentional = new Set(app.options.getValue("intentionallyNotExported"));
for (const id in project.reflections) {
const ref = project.reflections[id];
const tags = ref.comment?.blockTags ?? [];
for (const tag of tags) {
if (tag.tag === TAG) {
const name = Comment.combineDisplayParts(tag.content).trim();
if (name) {
intentional.add(name);
}
}
}
}
app.options.setValue("intentionallyNotExported", [...intentional]);
});
}

View File

@@ -10,6 +10,7 @@ const config = {
"typedoc-plugin-dt-links",
"typedoc-plugin-redirect",
"./typedoc-plugin-replace-text.js",
"./typedoc-plugin-not-exported.js",
],
entryPoints: ["../resources/embedding-sdk/dist/index.d.ts"],
router: "structure",

View File

@@ -77,6 +77,36 @@ It piggybacks on a running dev nREPL (~5s) and auto-spawns a JVM if none is runn
the four generated keys; structural changes it can't safely make (a new module needs a human `:team`, or
modules need reordering) are printed as `WARNING:` lines for you to resolve by hand.
## Kondo Ignore Ratchets
`.clj-kondo/ratchets.edn` records, per linter, how many inline `:clj-kondo/ignore` forms the backend source
tree may contain. `metabase.core.kondo-ratchet-test` fails when the budgets drift from the actual counts,
in either direction. Prefer fixing the underlying warning over adding an ignore.
Budget too high (you removed ignores): a local run of the test tightens the file for you — commit the
change. PRs labelled `kondo-ratchets-self-healing` get the lowered budgets committed to the branch by CI.
To tighten by hand (babashka, no JVM; a no-op prints `unchanged`):
```bash
./bin/mage fix-kondo-ratchets
```
Budget too low (you added an ignore): the task only raises a budget when told to. If the ignore is
genuinely required, run `./bin/mage fix-kondo-ratchets --seed :the-linter` and defend the increase in the
PR.
Introducing a new linter: `./bin/mage kondo-insert-ignores :the-linter` inserts an ignore at every site it
flags, then `./bin/mage fix-kondo-ratchets --seed :the-linter` records the budget — no big-bang cleanup.
To burn debt down, `./bin/mage kondo-redundant-ignores` lists ignores that are no longer needed (slow:
full kondo run). Kondo's redundancy report can't see hook-linter warnings, so `--fix` re-lints after
removing, puts any still-working ignore back exactly as it was, and stamps it with a `[kondo-keep]`
comment; marked sites are skipped on later runs. That verification needs a clean starting point, so
files with pre-existing lint findings are excluded from the sweep and reported. `--fix --audit` rechecks the
marked sites too, removing any that have become truly redundant along with their stamped marker
comments (a marker trailing on a code line is left for a hand fix). `[kondo-keep]` can also be added
by hand to protect an ignore whose exact form matters — it only counts on the line directly above the
ignore, or trailing on the ignore's own line.
## Tool Preferences
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.

31
bb.edn
View File

@@ -1,7 +1,8 @@
{:min-bb-version "1.12.212"
;; we put path as bin, and everything is in the ./mage subdirectory,
;; so the namespaces are mage.cli, mage.format, etc.
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
;; dev/src is here for tasks whose logic is shared with JVM tests (e.g. dev.kondo-ratchet)
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src"]
;; use Git version of dep temporarily until a new release > 0.16.4 is cut
:deps {dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
:git/url "https://github.com/weavejester/cljfmt"} ;; run cljfmt from bb
@@ -12,7 +13,7 @@
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
table/table {:mvn/version "0.5.0"} ;; table printing
}
:pods {org.babashka/postgresql {:version "0.1.0"}}
;; No :pods key on purpose: tasks lazily load only what they need.
:tasks
{:requires [[mage.cli :as cli]
[mage.color :as c]]
@@ -274,6 +275,32 @@
:requires [[mage.modules]]
:task (task! (mage.modules/cli-fix-config (cli/parse! (current-task))))}
fix-kondo-ratchets
{:doc "Lower the per-linter ignore budgets in .clj-kondo/ratchets.edn to match the source tree."
:examples [["./bin/mage fix-kondo-ratchets" "Ratchet budgets down (never raises one; over-budget linters are only warned about)"]
["./bin/mage fix-kondo-ratchets --seed :some-linter" "Also set :some-linter's budget to its actual count, for landing a new linter"]]
:options [["-s" "--seed LINTER" "Set LINTER's budget to the actual count (may add or raise it)"]]
:requires [[dev.kondo-ratchet]]
:task (task! (dev.kondo-ratchet/fix! (:options parsed)))}
kondo-redundant-ignores
{:doc "Report inline kondo ignores that are no longer necessary (slow: full kondo run)."
:examples [["./bin/mage kondo-redundant-ignores" "Scan the backend tree for removable ignores"]
["./bin/mage kondo-redundant-ignores --fix" "Also remove them from source (restores + marks [kondo-keep] any that re-trigger)"]
["./bin/mage kondo-redundant-ignores --fix --audit" "Recheck [kondo-keep] sites too, unmarking any now truly redundant"]]
:options [["-f" "--fix" "Remove the candidate ignores from source"]
["-a" "--audit" "Include sites marked [kondo-keep] instead of skipping them"]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/redundant-ignores parsed))}
kondo-insert-ignores
{:doc "Insert inline ignores at every site a linter flags -- for landing a new linter without a big-bang fix."
:examples [["./bin/mage kondo-insert-ignores :metabase/new-linter" "Insert ignores everywhere the linter fires, then seed its budget"]
["./bin/mage kondo-insert-ignores :equals-true test/metabase/pulse" "Only under the given paths"]]
:arg-schema [:sequential [:string {:name "linter-then-paths"}]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/insert-ignores (:arguments parsed)))}
ls
{:doc "List all mage public and private tasks"
:examples [["./bin/mage ls" "List all tasks"]]

View File

@@ -169,7 +169,6 @@
(doseq [^File file (file-seq source-path)
:when (not (directory? file))
:when
#_{:clj-kondo/ignore [:discouraged-var]}
(or (str/ends-with? (str/lower-case file) "yaml")
(str/ends-with? (str/lower-case file) "yml"))]
(when verbose (out/safe-println "Zipping file:" file))

View File

@@ -27,7 +27,6 @@
(deftest ^:parallel backend-message?
(testing "messages present in any .clj and .cljc files are detected as backend messages"
#_{:clj-kondo/ignore [:equals-true]}
(are [source-references expected] (= expected
(i18n/backend-message? {:source-references source-references}))
;; Simple .clj and .cljc files with and without line numbers

View File

@@ -3,6 +3,7 @@ set -euo pipefail
# Requires: BASE_REF, CURRENT_VERSION (for master)
# Optional: PR_LABELS (comma-separated, for backport handling)
# OLDEST_SUPPORTED_VERSION (required when PR_LABELS contains `backport`)
if [[ -z "${BASE_REF:-}" ]]; then
echo "ERROR: BASE_REF is not set." >&2
@@ -39,30 +40,53 @@ elif [[ "$BASE_REF" == "master" ]]; then
fi
echo "CURRENT_VERSION: ${CURRENT_VERSION}"
matches=0
for l in backport single-backport double-backport triple-backport; do
if [[ ",$labels," == *",$l,"* ]]; then
# N.B. not ((matches++)) — that exits 1 when matches=0 under set -e
matches=$((matches + 1))
# Backport labels shift the target version back so migrations land on the earliest release
# branch the backport will reach. `backport` targets every supported release line (see
# .github/workflows/auto-backport.yml), so its floor is the oldest supported major from
# version-info.json, passed in as OLDEST_SUPPORTED_VERSION. The legacy depth labels keep
# their fixed offsets from CURRENT_VERSION.
candidates=()
if [[ ",$labels," == *",backport,"* ]]; then
if [[ -z "${OLDEST_SUPPORTED_VERSION:-}" ]]; then
echo "ERROR: PR has the 'backport' label but OLDEST_SUPPORTED_VERSION is not set." >&2
exit 1
fi
done
if ! [[ "$OLDEST_SUPPORTED_VERSION" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: OLDEST_SUPPORTED_VERSION must be a positive integer, got: '${OLDEST_SUPPORTED_VERSION}'" >&2
exit 1
fi
echo "OLDEST_SUPPORTED_VERSION: ${OLDEST_SUPPORTED_VERSION}"
candidates+=("$OLDEST_SUPPORTED_VERSION")
fi
[[ ",$labels," == *",single-backport,"* ]] && candidates+=("$CURRENT_VERSION")
[[ ",$labels," == *",double-backport,"* ]] && candidates+=($((CURRENT_VERSION - 1)))
[[ ",$labels," == *",triple-backport,"* ]] && candidates+=($((CURRENT_VERSION - 2)))
constraint="="
version=$((CURRENT_VERSION + 1))
# Backport labels shift the target version back so migrations land on the correct release branch.
[[ ",$labels," == *",backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",single-backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",double-backport,"* ]] && version=$((CURRENT_VERSION - 1))
[[ ",$labels," == *",triple-backport,"* ]] && version=$((CURRENT_VERSION - 2))
# ${arr[@]+...} guards the empty-array case, which trips `set -u` on bash < 4.4
for candidate in ${candidates[@]+"${candidates[@]}"}; do
[[ "$candidate" -lt "$version" ]] && version=$candidate
done
[[ $matches -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
[[ ${#candidates[@]} -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
else
echo "Target branch '${BASE_REF}' is not master or a release branch — skipping."
exit 0
fi
# Per-feature migration directories (resources/migrations/NNN/) start at v60. Older versions keep a
# single shared file (resources/migrations/0NN_update_migrations.yaml), and pre-60 release branches
# only load those shared files (endsWithFilter: "_update_migrations.yaml"), so a per-feature file
# targeting a pre-60 version would be silently ignored there.
DIR_LAYOUT_FLOOR=60
expected_dir=$(printf "%03d" "$version")
echo "Expected directory: ${constraint} ${expected_dir}"
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "Expected location: resources/migrations/${expected_dir}_update_migrations.yaml (per-feature directories start at v${DIR_LAYOUT_FLOOR})"
else
echo "Expected directory: ${constraint} ${expected_dir}"
fi
echo ""
# Check that new migration files are in the correct directory
@@ -84,9 +108,16 @@ while IFS= read -r file; do
dir_version=$((10#$dir))
dir=$(printf "%03d" "$dir_version")
if [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
if [[ "$dir_version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} targets v${dir_version}, but per-feature directories start at v${DIR_LAYOUT_FLOOR} — a pre-${DIR_LAYOUT_FLOOR} changeset goes in resources/migrations/${dir}_update_migrations.yaml" >&2
errors=$((errors + 1))
elif [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
[[ "$constraint" == "<=" && "$dir_version" -gt "$version" ]]; then
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} is in ${dir} but the changeset belongs in resources/migrations/${expected_dir}_update_migrations.yaml" >&2
else
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
fi
errors=$((errors + 1))
fi
done <<<"$changed_files"
@@ -98,19 +129,24 @@ if [[ "$errors" -gt 0 ]]; then
echo "Why: Migration files must be in the directory matching the target release version." >&2
echo "A misplaced migration misrepresents when it was introduced and will break downgrades spanning more than one version." >&2
echo "" >&2
if [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory." >&2
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "To fix: append the changeset(s) to resources/migrations/${expected_dir}_update_migrations.yaml." >&2
echo "Pre-${DIR_LAYOUT_FLOOR} releases only load the shared per-version files, not NNN/ directories." >&2
elif [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory (>= 0${DIR_LAYOUT_FLOOR})." >&2
else
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/." >&2
fi
echo "" >&2
echo "If this PR will be backported, ensure the correct backport label is applied" >&2
echo "(backport, single-backport, double-backport, or triple-backport) and re-run CI, even if you" >&2
echo "plan to create the backport PRs manually. Base the label on the oldest" >&2
echo "release you're targeting, and simply close any unwanted PRs." >&2
echo "and re-run CI, even if you plan to create the backport PRs manually:" >&2
echo " - backport: all supported release lines (the check targets the oldest one)" >&2
echo " - single-backport: the current release only" >&2
echo " - double-backport / triple-backport: 1 or 2 releases before the current one" >&2
echo "Base the label on the oldest release you're targeting, and simply close any unwanted PRs." >&2
echo "" >&2
echo "If you are backporting more than 3 releases back, or this is an exceptional" >&2
echo "circumstance, you will need to either update this script or force merge." >&2
echo "If none of the labels matches the releases you're targeting, or this is an" >&2
echo "exceptional circumstance, you will need to either update this script or force merge." >&2
exit 1
fi

View File

@@ -1,12 +1,15 @@
#!/usr/bin/env node
/* eslint-env node */
/* eslint-disable import/no-commonjs, import/order, no-console */
/* eslint-disable import/order */
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const glob = require("glob");
const { Extractor, ExtractorConfig } = require("@microsoft/api-extractor");
const SDK_DIST_DIR_PATH = path.resolve("./resources/embedding-sdk/dist");
const API_EXTRACTOR_BIN_PATH = path.resolve(
"./node_modules/@microsoft/api-extractor/bin/api-extractor",
);
/*
* This script replaces all custom aliases in Embedding SDK generated ".d.ts" files so that this imports could be resolved
@@ -26,10 +29,52 @@ const REPLACES_MAP = {
cljs: "target/cljs_release",
};
const API_EXTRACTOR_CONFIG_PATH = path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
);
const DTS_ROLLUPS = [
{
name: "main",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
},
{
name: "data-app",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app.d.ts",
),
},
{
name: "data-app-dev",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.d.ts",
),
},
{
name: "data-app-dev-config",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-config-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.config.d.ts",
),
},
];
const getLogger = (prefix) => {
const verbose = process.env.SDK_FIXUP_VERBOSE_LOGS === "true";
@@ -110,7 +155,7 @@ const removeUnresolvedReexports = (filePath) => {
}
};
const fixupTypesAfterCompilation = ({ isWatchMode }) => {
const fixupTypesAfterCompilation = async ({ isWatchMode }) => {
log("Fixing SDK d.ts files...");
const dtsFilePaths = glob.sync(`${SDK_DIST_DIR_PATH}/**/*.d.ts`);
@@ -123,45 +168,97 @@ const fixupTypesAfterCompilation = ({ isWatchMode }) => {
console.log("[dts fixup] Done!");
if (!isWatchMode) {
generateDtsRollup();
await generateDtsRollup();
}
};
const generateDtsRollup = () => {
const generateDtsRollup = async () => {
// Dts rollup logger
const { log, error } = getLogger("dts rollup");
log("Generate dts rollup...");
const dtsRollupEntryPointPath = path.resolve(
path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
);
const dtsRollupEntryPointPathExist = fs.existsSync(dtsRollupEntryPointPath);
const existingEntryPointCount = DTS_ROLLUPS.filter(({ entryPointPath }) =>
fs.existsSync(path.resolve(entryPointPath)),
).length;
if (!dtsRollupEntryPointPathExist) {
if (existingEntryPointCount === 0) {
log("It looks like dts rollup is already generated, skipping...");
return;
}
const apiExtractorConfig = ExtractorConfig.loadFileAndPrepare(
API_EXTRACTOR_CONFIG_PATH,
if (existingEntryPointCount !== DTS_ROLLUPS.length) {
DTS_ROLLUPS.forEach(({ entryPointPath, name }) => {
const resolvedEntryPointPath = path.resolve(entryPointPath);
if (!fs.existsSync(resolvedEntryPointPath)) {
error(
`Dts rollup entry point for ${name} does not exist: ${resolvedEntryPointPath}`,
);
}
});
process.exitCode = 1;
return;
}
let hasExtractorFailure = false;
// The extractors are independent (each reads the shared compiled d.ts tree
// and writes its own rollup), so run them concurrently. Output is buffered
// per extractor and printed on completion so parallel runs don't interleave.
const runApiExtractor = ({ configPath, name }) =>
new Promise((resolve) => {
const child = spawn(
process.execPath,
[
API_EXTRACTOR_BIN_PATH,
"run",
"--local",
"--verbose",
"--config",
path.resolve(configPath),
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let output = "";
child.stdout.on("data", (chunk) => (output += chunk));
child.stderr.on("data", (chunk) => (output += chunk));
child.on("close", (status, signal) =>
resolve({ name, status, signal, output }),
);
});
const results = await Promise.all(
DTS_ROLLUPS.filter(({ entryPointPath, name }) => {
if (fs.existsSync(path.resolve(entryPointPath))) {
return true;
}
error(
`Dts rollup entry point for ${name} does not exist: ${path.resolve(entryPointPath)}`,
);
hasExtractorFailure = true;
return false;
}).map(runApiExtractor),
);
const extractorResult = Extractor.invoke(apiExtractorConfig, {
localBuild: true,
showVerboseMessages: true,
});
for (const { name, status, signal, output } of results) {
log(`--- api-extractor: ${name} ---`);
process.stdout.write(output);
if (!extractorResult.succeeded) {
error(
`API Extractor completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
if (status !== 0 || signal) {
error(
`API Extractor failed for ${name}` +
(signal ? ` with signal ${signal}` : ""),
);
hasExtractorFailure = true;
}
}
if (hasExtractorFailure) {
process.exitCode = 1;
return;
}
log("API Extractor completed successfully");
@@ -220,7 +317,7 @@ const isWatchMode = process.argv.includes("--watch");
const run = async () => {
// when running on a clean state and with --watch, the folder might not exist yet
await waitForFolder(SDK_DIST_DIR_PATH);
fixupTypesAfterCompilation({ isWatchMode });
await fixupTypesAfterCompilation({ isWatchMode });
if (isWatchMode) {
console.log("\n\n\n");

View File

@@ -42,10 +42,16 @@ function generateSdkPackage() {
);
const sdkPackageTemplateJsonContent = JSON.parse(sdkPackageTemplateJson);
// `sdkRelease` is release-process metadata (distTag/tagAsLatest) read by
// .github/workflows/release-embedding-sdk.yml — it must never ship inside
// the published npm package.json.
const { sdkRelease: _sdkRelease, ...publishableTemplateJsonContent } =
sdkPackageTemplateJsonContent;
const todayDate = new Date().toJSON().slice(0, 10).replaceAll("-", "");
const mergedContent = {
...sdkPackageTemplateJsonContent,
...publishableTemplateJsonContent,
version: maybeCommitHash
? `${sdkPackageTemplateJsonContent.version}-${todayDate}-${maybeCommitHash}`
: sdkPackageTemplateJsonContent.version,

View File

@@ -1,11 +0,0 @@
#!/bin/bash
function update_readme() {
sed -i -E 's|(embedding-sdk-)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
sed -i -E 's|(http://downloads.metabase.com/sdk/v)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
}
function update_package_json_template() {
sed -i -E 's/("version": ").*"/\1'"$1"'"/' enterprise/frontend/src/embedding-sdk-package/package.template.json
}
$1 $2

View File

@@ -207,16 +207,42 @@
(str/join ", " bad-ids))
{:invalid-ids (vec bad-ids)}))))))
(defn require-primary-key-exists-has-table-name
"Ensures that all primaryKeyExists preconditions specify a tableName."
(def ^:private table-scoped-preconditions
"Existence preconditions mapped to the key that scopes them to a single table. Without that key,
Liquibase answers the check by snapshotting the whole schema — ~2 seconds per precondition on a
large postgres appdb, which is how 27 name-only preconditions once added ~50s to every full
migration."
{:primaryKeyExists :tableName
:indexExists :tableName
:uniqueConstraintExists :tableName
:foreignKeyConstraintExists :foreignKeyTableName})
(defn- unscoped-preconditions
"Walk a preConditions tree (maps nested under and/or/not, in either flat-map or list form) and
return [precondition-key required-key] pairs for every table-scoped existence check that is
missing its table key."
[form]
(cond
(map? form) (concat
(for [[precondition required] table-scoped-preconditions
:let [check (get form precondition)]
:when (and (map? check) (not (contains? check required)))]
[precondition required])
(mapcat unscoped-preconditions (vals form)))
(sequential? form) (mapcat unscoped-preconditions form)
:else nil))
(defn require-table-scoped-existence-preconditions
"Ensures that primaryKeyExists/indexExists/uniqueConstraintExists preconditions specify a
tableName, and foreignKeyConstraintExists a foreignKeyTableName."
[change-log]
(doseq [{:keys [changeSet]} change-log
:when changeSet
:let [s (pr-str (:preConditions changeSet))]
:when (and (str/includes? s ":primaryKeyExists")
(not (str/includes? s ":tableName")))]
:let [[precondition required] (first (unscoped-preconditions (:preConditions changeSet)))]
:when precondition]
(throw (validation-error
(format "Migration '%s' has primaryKeyExists precondition without tableName" (:id changeSet))
(format "Migration '%s' has %s precondition without %s: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
(:id changeSet) (name precondition) (name required))
{:id (:id changeSet)}))))
(s/def ::changeSet
@@ -292,7 +318,7 @@
(require-no-bare-blob-or-text-types change-log)
(require-no-bare-boolean-types change-log file)
(require-no-datetime-type change-log file)
(require-primary-key-exists-has-table-name change-log)
(require-table-scoped-existence-preconditions change-log)
(let [{:keys [changeSet]
:as all} (group-by only-key change-log)]
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
@@ -368,17 +394,14 @@
(println)
#_:clj-kondo/ignore
(printf "Error in %s:\t%s\n" (:file (ex-data e)) (.getMessage e))
#_:clj-kondo/ignore
(printf "Details:\n\n %s" (with-out-str (pprint/pprint (dissoc (ex-data e) ::validation-error))))
(println))
(do
(pprint/pprint (Throwable->map e))
(println (.getMessage e))))
#_:clj-kondo/ignore
(System/exit 1))
(catch #_:clj-kondo/ignore
Throwable e
(pprint/pprint (Throwable->map e))
(println (.getMessage e))
#_:clj-kondo/ignore
(System/exit 1))))

View File

@@ -398,3 +398,40 @@
:remarks "none"
:type "timestamp with time zone"}}]}}]
:rollback nil))))))
(deftest ^:parallel require-table-scoped-existence-preconditions-test
(testing "foreignKeyConstraintExists without foreignKeyTableName is rejected"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has foreignKeyConstraintExists precondition without foreignKeyTableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"}}]}]))))
(testing "foreignKeyConstraintExists with foreignKeyTableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"
:foreignKeyTableName "my_table"}}]}])))))
(testing "indexExists without tableName is rejected, including in flat-map not form"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has indexExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"}}}]))))
(testing "indexExists with tableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"
:tableName "my_table"}}}])))))
(testing "primaryKeyExists without tableName is still rejected (previous rule subsumed)"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has primaryKeyExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:primaryKeyExists {:primaryKeyName "pk_my_table"}}])))))

149
bun.lock
View File

@@ -67,7 +67,6 @@
"@tiptap/starter-kit": "^3.2.0",
"@tiptap/suggestion": "^3.2.0",
"@types/js-yaml": "^4.0.9",
"@types/react-router": "3.0.27",
"@uiw/react-codemirror": "^4.23.6",
"@visx/axis": "^3.10.1",
"@visx/clip-path": "^3.3.0",
@@ -102,7 +101,6 @@
"formik": "^2.4.5",
"frimousse": "^0.3.0",
"fuse.js": "^7.1.0",
"history": "3",
"html2canvas-pro": "^2.0.2",
"icepick": "2.4.0",
"iframe-resizer": "~4.3.2",
@@ -118,6 +116,7 @@
"leaflet-draw": "^0.4.9",
"lodash.debounce": "^4.0.8",
"lodash.orderby": "^4.6.0",
"marked": "18.0.6",
"merge-refs": "^1.3.0",
"mustache": "^2.3.2",
"normalizr": "^3.0.2",
@@ -145,7 +144,6 @@
"react-qr-code": "^2.0.15",
"react-redux": "^9.1.2",
"react-resizable": "^3.1.3",
"react-router": "3",
"react-transition-group": "^4.4.5",
"react-use": "^17.5.1",
"react-virtualized": "9.22.6",
@@ -153,6 +151,7 @@
"redux-undo": "^1.1.0",
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.0",
"remend": "1.3.0",
"reselect": "^5.1.0",
"resize-observer-polyfill": "^1.5.1",
"server-text-width": "^1.0.2",
@@ -277,6 +276,7 @@
"@types/tether": "^1.4.6",
"@types/underscore": "^1.13.0",
"@types/use-sync-external-store": "^1.5.0",
"@vitejs/plugin-react": "^6.0.2",
"ajv": "^8.5.0",
"babel-loader": "^9.1.2",
"babel-plugin-ttag": "^1.8.16",
@@ -378,7 +378,7 @@
"raf": "^3.4.0",
"react-docgen-typescript-plugin": "^1.0.6",
"react-refresh": "^0.14.2",
"react-router-v7": "npm:react-router@7.18.1",
"react-router": "7.18.1",
"reg-cli": "^0.17.7",
"semver": "^7.6.3",
"shadow-cljs": "^3.3.5",
@@ -402,6 +402,9 @@
"typedoc-plugin-redirect": "^1.2.0",
"typescript-eslint": "8.60.0",
"typescript-plugin-css-modules": "^5.1.0",
"vite": "^8.0.16",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-svgr": "^5.2.0",
"web-streams-polyfill": "^4.1.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "^4.10.1",
@@ -1275,6 +1278,8 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.34.0", "", {}, "sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA=="],
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.38.0", "", { "os": "android", "cpu": "arm" }, "sha512-lTN4//sgYywK8ulQo7a/EZVzOTGomGQv2IG/7tMYdqTV3xN3QTqWpXcZBGUzaicC4B882N+5zJLYZ37IWfUMcg=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.38.0", "", { "os": "android", "cpu": "arm64" }, "sha512-XbVgqR1WsIcCkfxwh2tdg3M1MWgR23YOboW2nbB8ab0gInNNLGy7cIAdr78XaoG/bGdaF4488XRhuGWq67xrzA=="],
@@ -1439,6 +1444,40 @@
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
"@rspack/binding": ["@rspack/binding@2.0.3", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "2.0.3", "@rspack/binding-darwin-x64": "2.0.3", "@rspack/binding-linux-arm64-gnu": "2.0.3", "@rspack/binding-linux-arm64-musl": "2.0.3", "@rspack/binding-linux-x64-gnu": "2.0.3", "@rspack/binding-linux-x64-musl": "2.0.3", "@rspack/binding-wasm32-wasi": "2.0.3", "@rspack/binding-win32-arm64-msvc": "2.0.3", "@rspack/binding-win32-ia32-msvc": "2.0.3", "@rspack/binding-win32-x64-msvc": "2.0.3" } }, "sha512-4exVNhGhW5RFHjK87XeTKbkA/qAgI5NHJlT1jNqiJv0gcUXLqTOEU3w7f8+f9zUo4JMFvPc0c9veOi4M19YYTg=="],
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@2.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4UyCjLJwU/WxR6K1/gG4u3+jUsoaRHJ5rNu9fto/UbvrItwdlVNULChAApqZFw6mcSetMddSjSICeuj5pSB6sA=="],
@@ -1969,8 +2008,6 @@
"@types/react-resizable": ["@types/react-resizable@3.0.4", "", { "dependencies": { "@types/react": "*" } }, "sha512-+QguN9CDfC1lthq+4noG1fkxh8cqkV2Fv/Mu3mdknCCBiwwNLecnBdk1MmNNN7uJpT23Nx/aVkYsbt5NuWouFw=="],
"@types/react-router": ["@types/react-router@3.0.27", "", { "dependencies": { "@types/history": "^3", "@types/react": "*" } }, "sha512-Sq4bfTZ8TuHFTHXyzA3tu/BTGm/d4B8CwtOK4qOyJNoJMkY87qMOazkBizMHNpszVR1+kYOCNLjPdh/wAD7ESw=="],
"@types/react-textarea-autosize": ["@types/react-textarea-autosize@4.3.6", "", { "dependencies": { "@types/react": "*" } }, "sha512-cTf8tCem0c8A7CERYbTuF+bRFaqYu7N7HLCa6ZhUhDx8XnUsTpGx5udMWljt87JpciUKuUkImKPEsy6kcKhrcQ=="],
"@types/react-transition-group": ["@types/react-transition-group@4.4.5", "", { "dependencies": { "@types/react": "*" } }, "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA=="],
@@ -2157,6 +2194,8 @@
"@visx/vendor": ["@visx/vendor@3.5.0", "", { "dependencies": { "@types/d3-array": "3.0.3", "@types/d3-color": "3.1.0", "@types/d3-delaunay": "6.0.1", "@types/d3-format": "3.0.1", "@types/d3-geo": "3.1.0", "@types/d3-interpolate": "3.0.1", "@types/d3-scale": "4.0.2", "@types/d3-time": "3.0.0", "@types/d3-time-format": "2.1.0", "d3-array": "3.2.1", "d3-color": "3.1.0", "d3-delaunay": "6.0.2", "d3-format": "3.1.0", "d3-geo": "3.1.0", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", "d3-time": "3.1.0", "d3-time-format": "4.1.0", "internmap": "2.0.3" } }, "sha512-yt3SEZRVmt36+APsCISSO9eSOtzQkBjt+QRxNRzcTWuzwMAaF3PHCCSe31++kkpgY9yFoF+Gfes1TBe5NlETiQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
"@vitest/expect": ["@vitest/expect@2.0.5", "", { "dependencies": { "@vitest/spy": "2.0.5", "@vitest/utils": "2.0.5", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" } }, "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@2.1.8", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ=="],
@@ -2681,8 +2720,6 @@
"create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="],
"create-react-class": ["create-react-class@15.7.0", "", { "dependencies": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } }, "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cron-expression-validator": ["cron-expression-validator@1.0.20", "", {}, "sha512-g0osBTdp+1ryDw2vzlG6UpDPaa4fO94ZChF2R0lEnRurbuUEL74XEVX7xZJ13m4Mq/gb3ni6UQu8+Oqt+eocsw=="],
@@ -2905,7 +2942,7 @@
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
@@ -3119,7 +3156,7 @@
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
@@ -3401,8 +3438,6 @@
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
"history": ["history@3.3.0", "", { "dependencies": { "invariant": "^2.2.1", "loose-envify": "^1.2.0", "query-string": "^4.2.2", "warning": "^3.0.0" } }, "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw="],
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -3853,6 +3888,30 @@
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.1.6", "", {}, "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="],
@@ -3953,7 +4012,7 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="],
"marky": ["marky@1.2.5", "", {}, "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q=="],
@@ -4629,8 +4688,6 @@
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"query-string": ["query-string@4.3.4", "", { "dependencies": { "object-assign": "^4.1.0", "strict-uri-encode": "^1.0.0" } }, "sha1-u7aTucqRXCMlFbIosaArYJBD2+s="],
"querystring": ["querystring@0.2.0", "", {}, "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="],
"querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="],
@@ -4713,9 +4770,7 @@
"react-resizable": ["react-resizable@3.1.3", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw=="],
"react-router": ["react-router@3.2.6", "", { "dependencies": { "create-react-class": "^15.5.1", "history": "^3.0.0", "hoist-non-react-statics": "^3.3.2", "invariant": "^2.2.1", "loose-envify": "^1.2.0", "prop-types": "^15.7.2", "react-is": "^16.13.0", "warning": "^3.0.0" } }, "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw=="],
"react-router-v7": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-router": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
@@ -4811,6 +4866,8 @@
"remedial": ["remedial@1.0.8", "", {}, "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg=="],
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
"remove-trailing-spaces": ["remove-trailing-spaces@1.0.8", "", {}, "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA=="],
"renderkid": ["renderkid@3.0.0", "", { "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg=="],
@@ -4869,6 +4926,8 @@
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
"rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
@@ -5093,8 +5152,6 @@
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
"strict-uri-encode": ["strict-uri-encode@1.1.0", "", {}, "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-length": ["string-length@6.0.0", "", { "dependencies": { "strip-ansi": "^7.1.0" } }, "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg=="],
@@ -5461,6 +5518,12 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vite-plugin-css-injected-by-js": ["vite-plugin-css-injected-by-js@3.5.2", "", { "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ=="],
"vite-plugin-svgr": ["vite-plugin-svgr@5.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=3.0.0" } }, "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ=="],
"vitest-axe": ["vitest-axe@0.1.0", "", { "dependencies": { "aria-query": "^5.0.0", "axe-core": "^4.4.2", "chalk": "^5.0.1", "dom-accessibility-api": "^0.5.14", "lodash-es": "^4.17.21", "redent": "^3.0.0" } }, "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA=="],
"vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="],
@@ -5473,8 +5536,6 @@
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
"warning": ["warning@3.0.0", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"watskeburt": ["watskeburt@5.0.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA=="],
@@ -5815,6 +5876,8 @@
"@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@pmmmwh/react-refresh-webpack-plugin/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="],
"@redocly/cli/ajv": ["@redocly/ajv@8.17.4", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-BieiCML/IgP6x99HZByJSt7fJE4ipgzO7KAFss92Bs+PEI35BhY7vGIysFXLT+YmS7nHtQjZjhOQyPPEf7xGHA=="],
@@ -5835,6 +5898,16 @@
"@redocly/respect-core/colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@rollup/pluginutils/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@rushstack/node-core-library/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@rushstack/node-core-library/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
@@ -6055,12 +6128,8 @@
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
@@ -6953,9 +7022,7 @@
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-router-v7/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -6985,6 +7052,8 @@
"redoc/classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"redoc/marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"reduce-css-calc/balanced-match": ["balanced-match@0.4.2", "", {}, "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg=="],
"reduce-function-call/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -7007,8 +7076,12 @@
"ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="],
"rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
"rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"router/path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
"rxjs/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
@@ -7197,6 +7270,12 @@
"vfile-message/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
"vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"vite/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"vitest-axe/aria-query": ["aria-query@5.0.0", "", {}, "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg=="],
"vitest-axe/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -7463,6 +7542,14 @@
"@redocly/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="],
@@ -8331,6 +8418,8 @@
"unbox-primitive/which-boxed-primitive/is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"vite/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.51.0", "", {}, "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="],
"webpack-dev-middleware/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],
@@ -8517,6 +8606,8 @@
"@redocly/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],

View File

@@ -126,7 +126,7 @@
{:mvn/version "2.26.1"} ; allows the custom json logging format
org.apache.logging.log4j/log4j-slf4j2-impl
{:mvn/version "2.26.1"} ; allows the slf4j2 API to work with log4j 2
org.apache.pdfbox/pdfbox {:mvn/version "3.0.5"} ; PDF generation (backend dashboard PDF prototype)
org.apache.pdfbox/pdfbox {:mvn/version "3.0.8"} ; PDF generation (backend dashboard PDF prototype)
org.apache.poi/poi {:mvn/version "5.5.1"} ; Work with Office documents (e.g. Excel spreadsheets) -- newer version than one specified by Docjure
org.apache.poi/poi-ooxml {:mvn/version "5.5.1"
:exclusions [org.bouncycastle/bcpkix-jdk15on

View File

@@ -158,7 +158,6 @@
(let [h2-dbs (t2/select :model/Database :engine :h2)
in-memory? (fn [db] (some-> db :details :db (str/starts-with? "mem:")))
can-connect? (fn [db]
#_:clj-kondo/ignore
(binding [driver.settings/*allow-testing-h2-connections* true]
(try
(driver/can-connect? :h2 (:details db))

View File

@@ -0,0 +1,252 @@
(ns dev.kondo-ratchet
"Ratchet on inline kondo ignore forms.
Per-linter budgets live in `.clj-kondo/ratchets.edn`.
`metabase.core.kondo-ratchet-test` fails when they drift from the tree;
`./bin/mage fix-kondo-ratchets` lowers budgets, never raises them.
Loaded by both the bb task and the JVM test, so keep it dependency-free."
{:clj-kondo/config '{:linters {:discouraged-var {clojure.core/println {:level :off}}}}}
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]))
(set! *warn-on-reflection* true)
(def ratchets-file
"The budgets file, relative to the repo root."
".clj-kondo/ratchets.edn")
(def ^:private source-roots
["src" "test" "enterprise" "modules/drivers" "dev" "bin" "mage"])
(def ^:private source-extensions
[".clj" ".cljc" ".cljs"])
;; Concatenated so this file never contains a literal ignore marker.
(def ^:private ignore-marker
(str ":clj-kondo" "/ignore"))
;; `#_{... [:some-linter]}`, `^{... [:some-linter]}`, and the prefix-less attr-map form
;; `(ns foo {... [:some-linter]})`; the vector may span lines. The lazy tail after the vector runs to the
;; map's own closing brace, so extra keys still count and removal spans the whole form; a nested-brace
;; value stops the match at the vector instead.
(def ^:private vector-form-re
(re-pattern (str "(?:(?:#_|\\^)\\s*)?\\{\\s*" ignore-marker "\\s*\\[([^\\]]*)\\](?:[^{}]*?\\})?")))
;; Bare `#_kw` / `^kw` with no linter vector: suppresses every linter on the next form.
(def ^:private bare-form-re
(re-pattern (str "(?:#_\\s*|\\^)" ignore-marker "(?![\\w./-])")))
(defn mask-strings-and-comments
"`content` with string-literal and line-comment interiors replaced by spaces, newlines kept.
Same length as the input, so offsets and line numbers carry over.
Ignore forms inside strings (test fixtures) or commented-out code must not count."
[content]
(let [sb (StringBuilder. ^String content)
n (count content)]
(loop [i 0, state :code]
(if (>= i n)
(str sb)
(let [c (.charAt sb i)]
(case state
:code (case c
\" (recur (inc i) :string)
\; (do (.setCharAt sb i \space)
(recur (inc i) :comment))
;; char literal: never treat the next char as a delimiter
\\ (recur (+ i 2) :code)
(recur (inc i) :code))
:string (case c
\" (recur (inc i) :code)
\\ (do (.setCharAt sb i \space)
(when (< (inc i) n)
(when-not (= (.charAt sb (inc i)) \newline)
(.setCharAt sb (inc i) \space)))
(recur (+ i 2) :string))
\newline (recur (inc i) :string)
(do (.setCharAt sb i \space)
(recur (inc i) :string)))
:comment (if (= c \newline)
(recur (inc i) :code)
(do (.setCharAt sb i \space)
(recur (inc i) :comment)))))))))
(defn- linter-keywords
[vector-contents]
(map (comp keyword #(subs % 1))
(re-seq #":[A-Za-z][A-Za-z0-9*+!?<>=._/-]*" vector-contents)))
(defn line-linters
"Linter keywords suppressed by inline ignore forms on `line`.
The bare vector-less form counts as `:all`.
Like [[scan]], ignore forms inside string literals or line comments don't count."
[line]
(let [masked (mask-strings-and-comments line)]
(concat (mapcat (comp linter-keywords second) (re-seq vector-form-re masked))
(repeat (count (re-seq bare-form-re masked)) :all))))
(defn- offset->line
"1-based line number of character offset `i` in `content`."
[content i]
(inc (count (filter #(= % \newline) (subs content 0 i)))))
(defn- matches-with-offsets
"Like re-seq, but returns `{:start _, :end _, :linters [...]}` for each match of `re` in `masked`."
[re masked bare?]
(let [m (re-matcher re masked)]
(loop [acc []]
(if (.find m)
(recur (conj acc {:start (.start m)
:end (.end m)
:linters (if bare? [:all] (vec (linter-keywords (.group m 1))))}))
acc))))
(defn ignore-matches
"Inline ignore matches in `content`, in file order:
`{:start _, :end _, :line _, :linters [...]}` with character offsets and a 1-based line.
Matches inside string literals or line comments are excluded."
[content]
(let [masked (mask-strings-and-comments content)]
(->> (concat (matches-with-offsets vector-form-re masked false)
(matches-with-offsets bare-form-re masked true))
(sort-by :start)
(map #(assoc % :line (offset->line masked (:start %)))))))
(defn scan
"Occurrences of inline ignore forms under `roots` (relative to the repo root).
Returns `{:file \"src/...\", :line 42, :linters [...]}` maps.
Forms inside string literals or line comments don't count."
([]
(scan source-roots))
([roots]
(for [root roots
^java.io.File f (file-seq (io/file root))
:when (and (.isFile f)
(some #(str/ends-with? (.getPath f) %) source-extensions))
:let [content (slurp f)]
:when (str/includes? content ignore-marker)
m (ignore-matches content)]
{:file (.getPath f)
:line (:line m)
:linters (:linters m)})))
(defn actual-counts
"Per-linter occurrence counts for `occurrences`, as returned by [[scan]]."
[occurrences]
(frequencies (mapcat :linters occurrences)))
(defn- sorted-by-str
[kvs]
(into (sorted-map-by #(compare (str %1) (str %2))) kvs))
(defn drift
"Linters whose count in `occurrences` differs from its budget in `recorded` (absent = 0, either side).
Returns `{linter {:recorded _, :actual _}}`, plus `:examples` (up to 5 `file:line`) when over budget."
[recorded occurrences]
(let [actual (actual-counts occurrences)]
(sorted-by-str
(for [linter (into (set (keys actual)) (keys recorded))
:let [budget (get recorded linter 0)
n (get actual linter 0)]
:when (not= budget n)]
[linter (cond-> {:recorded budget, :actual n}
(> n budget)
(assoc :examples (->> occurrences
(filter #(some #{linter} (:linters %)))
(map #(str (:file %) ":" (:line %)))
(take 5)
vec)))]))))
(defn read-ratchets
"Parsed contents of [[ratchets-file]], with empty defaults when the file doesn't exist."
[]
(merge {:ignore-counts {}}
(when (.exists (io/file ratchets-file))
(edn/read-string (slurp ratchets-file)))))
(def ^:private header
(str ";; Per-linter budgets for inline `" ignore-marker "` forms.\n"
";; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.\n"
";; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it\n"
";; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit\n"
";; to defend in your PR.\n"
";; :all is the vector-less ignore form, which suppresses every linter on the next form.\n"))
(defn render
"Text of the ratchets file for the `{:ignore-counts _}` map `ratchets`.
Byte-stable: [[fix!]] idempotency and the file-hygiene test depend on it."
[{:keys [ignore-counts]}]
(let [counts-indent (apply str (repeat (count "{:ignore-counts {") \space))]
(str header
"{:ignore-counts "
(if (empty? ignore-counts)
"{}"
(let [entries (sort-by (comp str first) ignore-counts)
width (apply max (map (comp count str first) entries))]
(str "{"
(str/join (str "\n" counts-indent)
(for [[linter n] entries]
(format (str "%-" width "s %d") (str linter) n)))
"}")))
"}\n")))
(defn lowered-counts
"`recorded` with each budget lowered to its actual count; entries with no ignores left are dropped.
Linters in `seeded` get their budget set to the actual count outright — the explicit escape hatch for
landing a new linter. Otherwise never raises a budget, never adds one."
[recorded actual seeded]
(into (sorted-by-str
(for [linter seeded
:when (pos? (get actual linter 0))]
[linter (get actual linter)]))
(keep (fn [[linter budget]]
(let [n (get actual linter 0)]
(cond
(contains? (set seeded) linter) nil
(zero? n) nil
(< n budget) [linter n]
:else [linter budget]))))
recorded))
(defn change-report
"The lines [[fix!]] prints: lowered/dropped/seeded budgets, plus warnings for anything over budget."
[{:keys [ignore-counts]} occurrences seeded]
(let [actual (actual-counts occurrences)]
(concat
(for [linter seeded
:let [n (get actual linter 0)]]
(if (pos? n)
(format "seeded %s at %d" linter n)
(format "WARNING: %s has no inline ignores -- nothing to seed" linter)))
(for [[linter budget] (sort-by (comp str first) (apply dissoc ignore-counts seeded))
:let [n (get actual linter 0)]
:when (not= n budget)]
(cond
(zero? n) (format "dropped %s (no ignores left)" linter)
(< n budget) (format "lowered %s %d -> %d" linter budget n)
:else (format "WARNING: %s is over budget (%d recorded, %d actual) -- remove ignores, or accept them all with `--seed %s`"
linter budget n linter)))
(for [[linter n] (sort-by (comp str first) (apply dissoc actual (concat seeded (keys ignore-counts))))]
(format "WARNING: %s has %d ignores but no budget entry -- seed one with `./bin/mage fix-kondo-ratchets --seed %s`"
linter n linter)))))
(defn fix!
"Rewrite [[ratchets-file]]: lower budgets and normalize formatting.
`--seed LINTER` (`{:seed \"...\"}` here) sets that budget to the actual count, adding or raising it.
Prints the [[change-report]], or `unchanged` on a no-op."
([]
(fix! nil))
([{:keys [seed]}]
(let [{:keys [ignore-counts] :as ratchets} (read-ratchets)
occurrences (scan)
seeded (if seed [(keyword (str/replace-first seed #"^:" ""))] [])
actual (actual-counts occurrences)
text (render {:ignore-counts (lowered-counts ignore-counts actual seeded)})
file (io/file ratchets-file)
old (when (.exists file) (slurp file))]
(run! println (change-report ratchets occurrences seeded))
(if (= old text)
(println "unchanged")
(do (spit file text)
(println (str "wrote " ratchets-file)))))))

View File

@@ -182,6 +182,7 @@ Metabase's reference documentation.
- [Managing people and groups](./people-and-groups/managing.md)
- [Password complexity](./people-and-groups/changing-password-complexity.md)
- [Session expiration](./people-and-groups/changing-session-expiration.md)
- [Two-factor authentication](./people-and-groups/two-factor-authentication.md)
- [Google Sign-In](./people-and-groups/google-sign-in.md)
- [LDAP](./people-and-groups/ldap.md)
- [API keys](./people-and-groups/api-keys.md)

View File

@@ -76,7 +76,7 @@ If something goes wrong, Metabase will display the error message it received fro
### Example `UPDATE` action
You could write an action that would update the `plan` column for a record in the `invoices` table in the Sample Database:
You could write an action that would update the `plan` column for a record in the `invoices` table:
```sql
{% raw %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -15,7 +15,7 @@ System prompts let you customize instructions for Metabot. You can write a separ
- **Natural language queries**: query-builder questions generated from natural language.
- **SQL generation**: SQL written from prompts (sidebar or [inline](./metabot.md#inline-sql-editing)).
Like text cards on dashboards, prompts support [Markdown](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
Like text cards on dashboards, prompts support [Markdown](../dashboards/markdown.md).
## Prompt tips

2413
docs/api.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ Metabase Cloud only works with:
- [Metabase's officially supported databases](../databases/connecting.md#connecting-to-supported-databases) (with the exception of SQLite and H2).
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite and H2), because there's currently no file storage available.
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite), because there's currently no file storage available.
## Limited custom certificate support

View File

@@ -6,9 +6,9 @@ title: "Configuration file"
{% include plans-blockquote.html feature="Loading from a configuration file" self-hosted-only="true" %}
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file named `config.yml`. The config file should be located at:
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file. The config file should be located at:
- The current directory (the directory where the running Metabase JAR is located).
- A file named `config.yml` or `config.yaml` in the current directory (the directory where the running Metabase JAR is located).
- The path specified by the [`MB_CONFIG_FILE_PATH`](./environment-variables.md#mb_config_file_path) environment variable.
The settings in the config file work the same as if you'd set the settings in the Admin Settings in your Metabase. Settings defined in this configuration file will update any existing settings. If, for example, a database already exists (that is, you'd already added it via the initial set up or **Admin settings** > **Databases**, Metabase will update the database entry based on the data in the config file). Which means: if you define a setting in the config file, and then later change that setting in your Metabase application, keep in mind that the config file will overwrite that change whenever Metabase restarts. Let's reiterate that in a blockquote:

View File

@@ -1881,8 +1881,7 @@ on your IdP, this usually looks something like `http://www.example.com/141xkex60
- Default: `null`
- [Configuration file name](./config-file.md): `saml-identity-provider-slo-uri`
This is the URL where your users go to logout of your identity provider. Depending on which IdP you're
using, this usually looks like `https://your-org-name.example.com` or `https://example.com/app/my_saml_app/abc123/sso/slo`.
If SAML single logout (SLO) is enabled, Metabase will make an HTTP-Redirect SLO request to this endpoint when a user logs out of Metabase.
### `MB_SAML_IDENTITY_PROVIDER_URI`
@@ -1933,7 +1932,7 @@ Absolute path to the Keystore file to use for signing SAML requests.
- Default: `false`
- [Configuration file name](./config-file.md): `saml-slo-enabled`
Is SAML Single Log Out enabled?
If enabled, Metabase will redirect users to your configured SAML Single Logout endpoint when they log out of Metabase.
### `MB_SAML_USER_PROVISIONING_ENABLED`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -82,7 +82,7 @@ You can use [Markdown](https://commonmark.org/help/) to format the text in your
![Result](images/result.png)
To learn more, see [Fun with Markdown in your dashboards](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
To learn more, see [Format text with Markdown](./markdown.md).
### Including variables in text cards
@@ -321,7 +321,7 @@ See [Caching per dashboard](../configuring-metabase/caching.md#dashboard-caching
## Sharing dashboards with public links
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **sharing** icon to find its public links.
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **Share** icon to find its public links.
![Share icon](./images/share-icon.png)

View File

@@ -79,7 +79,7 @@ To nest a bulleted item under another bulleted item, indent the nested item with
To add a link, put the link text in square brackets, followed by the URL in parentheses:
```text
[the internet's finest webpage](https://www.metabase.com)
[the internet's finest webpage](https://www.example.com)
```
This renders as a clickable link: [the internet's finest webpage](https://www.metabase.com).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -34,7 +34,7 @@ People in the Data Analyst group will have table metadata and data structure acc
_Data Studio > Tables_
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, visibility layer, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can search for table names, but the search will only match beginnings of words in table names. So for example, if you search for "base", results will include names like "Baseball stats" and "All your base are belong to us", but the results won't include tables like "Metabase secrets".

View File

@@ -48,7 +48,7 @@ The databases listed below have official drivers maintained by the Metabase team
If you don't see your database listed here, see [Community drivers](../developers-guide/community-drivers.md).
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database, as well as to provide some sample data out of the box.
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database.
## Connecting to databases hosted by a cloud provider

View File

@@ -36,8 +36,6 @@ There are a few things admins need to do to support CSV uploads:
To upload data to Metabase, an admin will need to connect your Metabase to a database that supports uploads using a database user account that has write access to that database.
You can also upload data to the Sample Database included with Metabase (an H2 database), though we don't recommend using the Sample Database for any data that you want to keep around.
For more, check out:
- [Adding and managing databases](./connecting.md)

View File

@@ -8,6 +8,16 @@ title: API changelog
- Self-hosted environments must now explicitly enable transforms before beginning to use them via the API. Admins can enable transforms in Data Studio or by setting the MB_TRANSFORMS_ENABLED environment variable to true.
- The endpoints that fetch prefill values for action forms have been converted from GET to POST so that parameter values are sent in the JSON request body instead of the URL query string. `parameters` is now a JSON object in the request body rather than a JSON-encoded query-string parameter:
- `GET /api/action/:action-id/execute` has been replaced by `POST /api/action/:action-id/execute/values`.
- `GET /api/dashboard/:dashboard-id/dashcard/:dashcard-id/execute` has been replaced by
`POST /api/dashboard/:dashboard-id/dashcard/:dashcard-id/execute/values`.
- `GET /api/public/dashboard/:uuid/dashcard/:dashcard-id/execute` has been replaced by
`POST /api/public/dashboard/:uuid/dashcard/:dashcard-id/execute/values`.
The GET variants have been removed without a deprecation period.
## Metabase 0.61.0
- `POST /api/metabot/describe/card` and `POST /api/metabot/describe/dashboard/:id` have been removed. These endpoints

View File

@@ -268,7 +268,7 @@ settings: {
| `group` | Sub-heading within a section for grouping related settings. |
| `index` | Display order within a group. |
| `inline` | When `true`, renders the widget on the same line as `title` (handy for `"toggle"`). |
| `widget` | A [built-in widget](#built-in-widgets) name, or a [custom React component](#custom-widgets). |
| `widget` | A [built-in widget](#built-in-widgets) name, or a [custom React component](#custom-widgets). |
| `getDefault(series, settings)` | Computes the default value when none is stored. |
| `getValue(series, settings)` | Always-computed value — overrides the stored value on every render. |
| `getProps(series, settings)` | Returns widget-specific props. |
@@ -417,13 +417,15 @@ Metabase runs plugin code in an isolated sandbox, so a visualization works only
- **Navigation and the rest of the app**: history changes, the host page's URL and referrer, and any DOM outside the plugin's own container.
- **Unsafe DOM and timing APIs**: `document.write`, `execCommand`, constructable stylesheets, raw HTML parsers (`DOMParser`, `setHTMLUnsafe`, `XSLTProcessor`), and resource-timing APIs that expose other requests the page has made.
### Custom visualizations don't render in embeds or public links
### When embedding, custom visualizations are only available in the SDK
[Embeds](../embedding/introduction.md) and [public links](../embedding/public-links.md) (to questions, dashboards, and documents) don't render custom visualizations. Any card that uses one falls back to the default visualization (a table).
The [Modular embedding SDK](../embedding/sdk/introduction.md) renders custom visualizations when you allowlist them with the [`allowedCustomVisualizations` prop](../embedding/sdk/config.md#custom-visualizations) on `MetabaseProvider`.
Other embedding types don't render custom visualizations. In [modular embedding](../embedding/modular-embedding.md) with web components and [public links](../embedding/public-links.md), any card that uses a custom visualization falls back to the default visualization for the query's results.
### Custom visualizations don't render in exports and subscriptions
Static renders—like charts in alerts or dashboard subscriptions—fall back to a table for any card that uses a custom visualization.
Static renders—like charts in alerts or dashboard subscriptions—fall back to a default visualization for the card's data shape.
## Example plugins

View File

@@ -6,6 +6,17 @@ title: Driver interface changelog
## Metabase 0.64.0
- `metabase.driver/workspace-isolation-details` `[driver database workspace]` -- new workspace-isolation
multimethod. Computes the isolation identifiers (`:schema`, and driver-specific `:database_details` such as
user/password) for a workspace *before* any warehouse work happens; `init-workspace-isolation!`,
`grant-workspace-read-access!`, and `destroy-workspace-isolation!` now receive those identifiers on the
`workspace` map instead of computing them internally. `:sql-jdbc` drivers inherit a default implementation
(deterministic schema/user names plus a random password); non-JDBC drivers that support workspaces must
implement it themselves.
- `metabase.driver/check-isolation-permissions`, added in 0.59.0, has been removed. Permission problems now
surface as failures from `init-workspace-isolation!`/`grant-workspace-read-access!` themselves.
- Added a `:native-pivot-tables` driver feature flag for drivers that can compile a pivot query as a single
`GROUP BY GROUPING SETS (...)` statement instead of the legacy multi-query path. Drivers that opt in must also
derive from `:sql-mbql5` (which provides the `:pivot` clause compiler). Defaults to `false`.

View File

@@ -9,7 +9,7 @@ redirect_from:
You can style your embedded [Metabase components](./components.md) with a **theme**.
![Embed share button](./images/embed-share-button.png)
![Share button to embed dashboard](./images/sharing-embed.png)
On OSS and Starter plans, embeds come with two theme presets: [light and dark](#dark-mode-and-light-mode).

View File

@@ -24,12 +24,12 @@ Toggle **Enable guest embeds**.
## Creating a guest embed
![Sharing button to embed dashboard](./images/sharing-embed.png)
![Share button to embed dashboard](./images/sharing-embed.png)
To create a guest embed:
1. Go to the item that you want to embed in your website. You can also open a command palette with Ctrl/Cmd+K and type "New embed".
2. Click the **sharing icon**.
2. Click the **Share** icon.
3. Select **Embed**.
4. Under **Authentication**, select **Guest**.
5. Optional: [customize the appearance of the embed](./appearance.md)
@@ -130,7 +130,7 @@ Parameters are disabled by default, which also makes them hidden from end-users.
To configure parameters:
1. Go to your embedded question or dashboard.
2. Click the **sharing icon** and select **Embed**.
2. Click the **Share** icon and select **Embed**.
3. Under **Parameters**, select the visibility option for each parameter, and optionally default value(s).
4. Click **Publish**.
@@ -453,7 +453,7 @@ function paramsFor(user, customContext) {
## Disabling embedding for a question or dashboard
1. Visit the embeddable question or dashboard.
2. Click the **sharing icon** (square with an arrow pointing to the top right).
2. Click the **Share** icon (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Guest embedding**
5. Click **Unpublish**.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -60,23 +60,24 @@ If you'd like to share your data with the good people of the internet, admins ca
## Comparison of embedding types
| Action | [Modular SDK](./sdk/introduction.md) | [Modular SSO](./modular-embedding.md) | [Modular Guest](./guest-embedding.md) | [Full app](./full-app-embedding.md) | [Public](../embedding/public-links.md) |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------- | ----------------------------------- | -------------------------------------- |
| Charts and dashboards | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Filter widgets](https://www.metabase.com/glossary/filter-widget) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Export results\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Locked filters](./static-embedding-parameters.md#restricting-data-in-a-static-embed-with-locked-parameters) | ❌ | ❌ | ✅ | ❌ | ❌ |
| [Data segregation](../permissions/embedding.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Drill-through menu](../questions/visualizations/drill-through.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Query builder](../questions/query-builder/editor.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Basic appearance customization](../configuring-metabase/appearance.md)\*\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Advanced theming](./appearance.md) | ✅ | ✅ | ❌ | ❌ | ❌ |
| [Usage analytics](../usage-and-performance-tools/usage-analytics.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Embed individual Metabase components | ✅ | ✅ | ❌ | ❌ | ❌ |
| Manage access and interactivity per component | ✅ | ✅ | ❌ | ❌ | ❌ |
| Custom layouts | ✅ | ❌ | ❌ | ❌ | ❌ |
| Customize behavior with [plugins](./sdk/plugins.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| AI chat | ✅ | ✅ | ❌ | ✅ | ❌ |
| Action | [Modular SDK](./sdk/introduction.md) | [Modular SSO](./modular-embedding.md) | [Modular Guest](./guest-embedding.md) | [Full app](./full-app-embedding.md) | [Public](../embedding/public-links.md) |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------- | ------------------------------------- | ----------------------------------- | -------------------------------------- |
| Charts and dashboards | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Filter widgets](https://www.metabase.com/glossary/filter-widget) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Export results\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Locked filters](./static-embedding-parameters.md#restricting-data-in-a-static-embed-with-locked-parameters) | ❌ | ❌ | ✅ | ❌ | ❌ |
| [Data segregation](../permissions/embedding.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Drill-through menu](../questions/visualizations/drill-through.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Query builder](../questions/query-builder/editor.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Basic appearance customization](../configuring-metabase/appearance.md)\*\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Advanced theming](./appearance.md) | ✅ | ✅ | ❌ | ❌ | ❌ |
| [Usage analytics](../usage-and-performance-tools/usage-analytics.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Embed individual Metabase components | ✅ | ✅ | ❌ | ❌ | ❌ |
| Manage access and interactivity per component | ✅ | ✅ | ❌ | ❌ | ❌ |
| Custom layouts | ✅ | ❌ | ❌ | ❌ | ❌ |
| Customize behavior with [plugins](./sdk/plugins.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| [Custom visualizations](../questions/visualizations/custom.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| AI chat | ✅ | ✅ | ❌ | ✅ | ❌ |
\* Each embedding type allows data downloads by default, but only [Pro and Enterprise](https://www.metabase.com/pricing/) plans can disable data downloads.
@@ -103,7 +104,7 @@ For information about the anonymous usage data Metabase collects from embedded c
## Embedding limitations
- Currently, you can't embed [documents](../documents/introduction.md) (though you can create [public documents](./public-links.md)).
- Embeds don't render [custom visualizations](../questions/visualizations/custom.md): any card that uses one falls back to the default visualization (a table).
- Only the [Modular embedding SDK](./sdk/introduction.md) renders [custom visualizations](../questions/visualizations/custom.md), and only ones you allowlist with the [`allowedCustomVisualizations` prop](./sdk/config.md#custom-visualizations). In other embedding types, any card that uses a custom visualization falls back to the default visualization for the query's results.
## Further reading

View File

@@ -36,10 +36,10 @@ You can generate a code snippet for embedding a component by going through the e
In your Metabase:
1. Visit the item you want to embed.
2. Click the sharing icon.
2. Click the **Share** icon.
3. Select **Embed**.
![Embed share button](./images/embed-share-button.png)
![Share button to embed dashboard](./images/sharing-embed.png)
You can also open a command palette with Ctrl/Cmd+K, type "New embed". You'll get a wizard to help you set up your embed.

View File

@@ -15,7 +15,7 @@ Admins can create and share public links (URLs) for questions, dashboards, and d
![Create a public link for a question](./images/create-a-public-link.png)
To create a public link for a question, admins can click on the **Sharing** icon at the top right of a question and select **Create a public link**. Copy the link and test it out by viewing the link in a private/incognito browser session.
To create a public link for a question, admins can click on the **Share** icon at the top right of a question and select **Create a public link**. Copy the link and test it out by viewing the link in a private/incognito browser session.
## Public link to export question results in CSV, XLSX, JSON
@@ -23,7 +23,7 @@ This export option is only available for questions, not dashboards.
To create a public link that people can use to download the results of a question:
1. Click on the **Sharing** icon for the question.
1. Click on the **Share** icon for the question.
2. Select **Create a public link**.
3. Click on the file format you want (below the **Public link** URL): CSV, XLSX, or JSON.
@@ -33,7 +33,7 @@ Open the public link in a new tab to test the download.
## Create a public link for a dashboard
To share a dashboard via a public link, admins can click on the **Sharing** button in the top right menu.
To share a dashboard via a public link, admins can click on the **Share** button in the top right menu.
![Sharing a dashboard](./images/dashboard-sharing.png)
@@ -41,7 +41,7 @@ To embed a dashboard, see [guest embedding](./guest-embedding.md).
## Create a public link for a document
To share a document via a public link, admins can click on the **Sharing** button in the top right menu and select **Create a public link**.
To share a document via a public link, admins can click on the **Share** button in the top right menu and select **Create a public link**.
Public documents are read-only: viewers cannot edit the content or add comments. For charts embedded in the document, viewers can download the results in CSV, XLSX, or JSON format using the **Download results** option in the chart menu.
@@ -86,7 +86,7 @@ For example, if you have a primary public dashboard that displays **Invoices** d
If you want to embed your question or dashboard as an iframe in a simple web page or app:
1. Click on the **Sharing** icon for your question or dashboard.
1. Click on the **Share** icon for your question or dashboard.
2. Click **Embed**.
3. In the bottom of the embedding popup, click on **Get embedding code**.
4. Copy the iframe snippet Metabase generates for you.
@@ -170,7 +170,7 @@ If you disable public sharing, then re-enable public sharing, all your previousl
### Individual question or dashboard links and embeds
1. Visit the question or dashboard.
2. Click on **Sharing** icon.
2. Click on the **Share** icon.
3. Select **Public link** or **Embed**.
4. Click **Remove public link**.

View File

@@ -32,6 +32,21 @@ To pass a theme, use `defineMetabaseTheme`. See [Reuse a saved theme in the SDK]
{% include_file "{{ dirname }}/api/snippets/MetabaseProviderProps.md" snippet="properties" %}
## Custom visualizations
The SDK can render [custom visualizations](../../questions/visualizations/custom.md). To allow custom visualizations in the embed, pass an allowlist of the custom visualizations to the `allowedCustomVisualizations` prop on `MetabaseProvider`:
```typescript
{% include_file "{{ dirname }}/snippets/config/config-with-custom-visualizations.tsx" snippet="example" %}
```
Only the custom visualizations you list will load. Each entry is the visualization's name (the manifest `name`, which you can find under **Admin** > **Settings** > **Custom visualizations** > **Manage visualizations**), prefixed with `custom:`. For example, a custom visualization named `Calendar Heatmap` becomes `"custom:Calendar Heatmap"`. Names are case-sensitive, so `"custom:calendar heatmap"` won't match a visualization named `Calendar Heatmap`.
Omitting the prop, or passing an empty array, turns off custom visualizations. Cards that use a custom visualization will fall back to the default visualization for the query's results. If you allowlist a name that doesn't match an installed custom visualization, the SDK logs a warning to the console and falls back to the default visualization.
For security, the SDK runs each custom visualization's code in an isolated sandbox, so a visualization can't reach your app or make network requests. The sandbox doesn't block passive image loads, though. A visualization can still trigger outbound requests through `<img>` tags or CSS `url()`. To limit where custom visualizations can load images from, set a Content Security Policy with an `img-src` allowlist in your app (the core Metabase app does this with [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains)). [Only add visualizations you trust](../../questions/visualizations/custom.md#only-add-visualizations-you-trust).
## Global event handlers
You can listen for events by defining the `eventHandlers` prop for `MetabaseProvider`.

View File

@@ -0,0 +1,20 @@
import type { PropsWithChildren } from "react";
import {
type MetabaseAuthConfig,
MetabaseProvider,
} from "@metabase/embedding-sdk-react";
const authConfig = {} as MetabaseAuthConfig;
const Example = ({ children }: PropsWithChildren) => (
// [<snippet example>]
<MetabaseProvider
authConfig={authConfig}
// Allowlist the custom visualizations to load, by their names,
// each prefixed with `custom:`.
allowedCustomVisualizations={["custom:Calendar Heatmap", "custom:Thumbs"]}
>
{children}
</MetabaseProvider>
// [<endsnippet example>]
);

View File

@@ -35,7 +35,7 @@ Unsigned parameters, such as appearance settings, should be added directly to yo
You can use **editable parameters** to add [filter widgets](https://www.metabase.com/glossary/filter-widget) to embedded dashboards or SQL questions.
1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters.md) or [SQL variable](../questions/native-editor/sql-parameters.md).
2. Click on the **sharing icon** > **Embed this item in an application**.
2. Click on the **Share** icon > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Editable** for each parameter that should get a filter widget on your embed.
5. Click **Publish** to save your changes.
@@ -112,7 +112,7 @@ Whenever you're adding a parameter to the embedding URL in your iframe's `src` a
If you want to restrict the data that's displayed in an embedded dashboard or SQL question, you can set up a **locked parameter**. A locked parameter filters the data in a dashboard or SQL question _before_ the results are displayed to people in a static embed.
1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters.md) or [SQL variable](../questions/native-editor/sql-parameters.md).
2. Click on the **sharing icon** > **Embed this item in an application**.
2. Click on the **Share** icon > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Locked** for each parameter that you want to restrict your data with.
5. Optional: select a value under **Preview locked parameters** to see what the restricted data looks like.

View File

@@ -41,12 +41,12 @@ If you want to embed charts with additional interactive features, like [drill-do
## Making a question or dashboard embeddable
![Sharing button to embed dashboard](./images/sharing-embed.png)
![Share button to embed dashboard](./images/sharing-embed.png)
To create a static embed:
1. Go to the question or dashboard that you want to embed in your website.
2. Click on the **sharing icon**.
2. Click the **Share** icon.
3. Select **Embed**.
4. Select **Static embedding**.
5. Optional: [customize the appearance of the embed](./static-embedding-parameters.md#customizing-the-appearance-of-a-static-embed)
@@ -63,7 +63,7 @@ metabase_site_url/embed/question/your_jwt_token?parameter_name=value
Once you've made a question or dashboard [embeddable](#making-a-question-or-dashboard-embeddable), you'll need to put the embedding URL for that question or dashboard on your website:
1. Go to the question or dashboard > **sharing icon** > **Embed**.
1. Go to the question or dashboard > **Share** icon > **Embed**.
2. Make any changes and copy the code.
3. [Preview the code](#previewing-the-code-for-an-embed)
4. Add the code to the server code that builds your website.
@@ -73,7 +73,7 @@ For more examples, see our [reference apps repo](https://github.com/metabase/emb
## Previewing the code for an embed
1. Go to the question or dashboard > **sharing icon** > **Embed this item in an application**.
1. Go to the question or dashboard > **Share** icon > **Embed**.
2. Click **Code**.
3. In the top code block, you'll find the sample code for your web server. You'll also find the iframe snippet to plug into your HTML template or single page app.
@@ -124,7 +124,7 @@ If you change the [parameters](./static-embedding-parameters.md) of your embedde
You can find a list of all static embeds of questions and dashboards from **Admin settings** > **Embedding** > **Static**.
1. Visit the embeddable question or dashboard.
2. Click on the **sharing icon** (square with an arrow pointing to the top right).
2. Click the **Share** icon (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Static embedding**
5. Click **Unpublish**.

View File

@@ -34,10 +34,6 @@ Generates a markdown file with some documentation and an example configuration f
Print a list of all multimethods available for a driver to implement, optionally with their docstrings.
## `drop-entity-ids`
Drop entity IDs for instances of serializable models. Useful for migrating from v1 serialization (x.46 and earlier) to v2 (x.47+).
## `dump-to-h2 h2-filename opts`
Transfer data from existing database to newly created H2 DB with specified filename. Target H2 file is deleted before dump, unless the --keep-existing flag is given.
@@ -103,10 +99,6 @@ Reset the password for a user with `email-address`.
Rotate the encryption key of a metabase database. The MB_ENCRYPTION_SECRET_KEY environment variable has to be set to the current key, and the parameter `new-key` has to be the new key. `new-key` has to be at least 16 chars.
## `seed-entity-ids`
Add entity IDs for instances of serializable models that don't already have them.
## `version`
Print version information about Metabase and the current system.

View File

@@ -1,47 +1,80 @@
---
title: Account settings
summary: Update your profile, change your password, view login history, manage notifications, and customize your Metabase experience.
redirect_from:
- /docs/latest/users-guide/account-settings
---
# Account settings
To access your account settings, click the **profile or grid icon** in the top right corner, then click your **profile name and email.**
## Account profile
You can set your first and last names, change your email address, and set your language. See our list of [supported languages](../configuring-metabase/localization.md).
## Theme
You can choose to display Metabase in light or dark mode.
To quickly toggle dark mode, use the [command palette](../exploration-and-organization/exploration.md#command-palette).
The theme only applies to your account. The theme doesn't apply to chart colors or embedded Metabases.
## Account password
You can change your password here. Note that if your Metabase uses Single Sign-On (SSO), your administrator will have disabled this password section, as your identity provider will manage logins.
If you're having trouble logging in, see our [People can't log into Metabase](../troubleshooting-guide/cant-log-in.md).
## Account login history
The login history lists each login, along with some location information (if available), and some client information (like Browser (Firefox/Windows)).
If you see any suspicious login attempts, change your password and notify your administrator.
### A note about new login emails
Whenever you log in from a new device, Metabase will send you an email just to let you know someone (presumably you) has logged in from an unrecognized device. If you see this email, but don't remember logging in, or don't recognize the device, change your password and let your administrator know.
## Disable animations in Metabase
This isn't an in-Metabase setting, but just so you know: you can disable UI animations in Metabase (like sidebars sliding around, or rotating spinners) by changing the settings for your operating system so it respects the `prefers-reduced-motion` CSS media feature. This change will also affect other applications, not just Metabase. Check out the instructions for how to set the user preferences for your operating system in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences).
## Notifications
If you subscribe or are added to dashboard subscriptions or alerts, you’ll be able to manage those notifications here (as well as on the relevant question or dashboard themselves).
Metabase excludes notifications for [comments](../documents/introduction.md#comment-notifications) from this page.
---
title: Account settings
summary: Update your profile, change your password, view login history, manage notifications, and customize your Metabase experience.
redirect_from:
- /docs/latest/users-guide/account-settings
---
# Account settings
To access your account settings, click the **profile or grid icon** in the top right corner, then click your **profile name and email.**
## Account profile
You can set your first and last names, change your email address, and set your language. See our list of [supported languages](../configuring-metabase/localization.md).
## Theme
You can choose to display Metabase in light or dark mode.
To quickly toggle dark mode, use the [command palette](../exploration-and-organization/exploration.md#command-palette).
The theme only applies to your account. The theme doesn't apply to chart colors or embedded Metabases.
## Account password
You can change your password here. Note that if your Metabase uses Single Sign-On (SSO), your administrator will have disabled this password section, as your identity provider will manage logins.
If you log in through LDAP, you won't see this password section either, since your directory manages your password. You may get a **Security** tab to enroll in 2FA if your admin has [turned it on](./two-factor-authentication.md#turn-on-two-factor-authentication).
If you're having trouble logging in, see our [People can't log into Metabase](../troubleshooting-guide/cant-log-in.md).
## Two-factor authentication
{% include plans-blockquote.html feature="Two-factor authentication" %}
If your admin has turned on [two-factor authentication](./two-factor-authentication.md) (2FA), you'll see a **Security** tab in your account settings. 2FA adds a second step to logging in: after your password, you enter a code from an authenticator app, so your password alone won't let someone into your account.
### Set up two-factor authentication on your account
1. Click the grid icon in the upper right.
2. Click your profile.
3. On the **Security** tab, click **Set up two-factor authentication**.
4. Confirm your password.
5. Scan the QR code with an authenticator app like Google Authenticator, 1Password, or Authy. If you can't scan it, enter the key manually in your authenticator app.
6. Enter the six-digit code from the app.
7. Save your recovery codes. Metabase shows these ten codes only once, so copy them and store them somewhere safe. Each recovery code only works once. If you [regenerate recovery codes](#regenerate-recovery-codes), the old codes will no longer work.
### Logging in with two-factor authentication
Once you set up two-factor authentication, Metabase will ask for a code every time you log in. After you enter your email and password:
- Enter the six-digit code from your authenticator app.
- Lost your app? Click **Use a recovery code instead** and enter one of your saved codes.
- If your admin set up email, click **Email me a code** to get a one-time code in your inbox.
### Regenerate recovery codes
Recovery codes are single-use, so it's worth generating a fresh set if you're running low or think someone else has seen them. On the **Security** tab, click **Generate recovery codes** and confirm with a current authenticator or recovery code. Generating new codes invalidates the old ones.
### Turn off two-factor authentication
In your account settings, on the **Security** tab , click **Disable**. Your account will be protected by your password only, and your recovery codes will stop working.
## Account login history
The login history lists each login, along with some location information (if available), and some client information (like Browser (Firefox/Windows)). If you see any suspicious login attempts, change your password and notify your administrator.
### A note about new login emails
Whenever you log in from a new device, Metabase will send you an email just to let you know someone (presumably you) has logged in from an unrecognized device. If you see this email, but don't remember logging in, or don't recognize the device, change your password and let your administrator know.
## Disable animations in Metabase
This isn't an in-Metabase setting, but just so you know: you can disable UI animations in Metabase (like sidebars sliding around, or rotating spinners) by changing the settings for your operating system so it respects the `prefers-reduced-motion` CSS media feature. This change will also affect other applications, not just Metabase. Check out the instructions for how to set the user preferences for your operating system in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences).
## Notifications
If you subscribe or are added to dashboard subscriptions or alerts, you’ll be able to manage those notifications here (as well as on the relevant question or dashboard themselves).
Metabase excludes notifications for [comments](../documents/introduction.md#comment-notifications) from this page.

View File

@@ -25,12 +25,14 @@ Make people use longer and more complex passwords.
Tell Metabase how long it should wait before asking people to log in again.
## [Two-factor authentication](./two-factor-authentication.md)
Have people confirm their identity with an authenticator app when they log in.
## Authentication
Metabase offers several options for authentication.
> If you need to set up 2-step or multi-factor authentication (2FA or MFA) for your Metabase, consider using one of the SSO options below.
### SSO for Metabase Open Source and Starter plans
- [Google Sign-in](./google-sign-in.md)

View File

@@ -0,0 +1,52 @@
---
title: Two-factor authentication
summary: Add a second step to logins by having people confirm their identity with an authenticator app. A Pro/Enterprise feature.
---
# Two-factor authentication
{% include plans-blockquote.html feature="Two-factor authentication" %}
Two-factor authentication (2FA) adds a second step to signing in. On top of people's email and password logins, they'll have to confirm their identity with a time-based code from an authenticator app.
Metabase's native 2FA applies to password logins and LDAP logins. 2FA for Single Sign-on (SSO) via JWT, SAML, or OIDC is managed through your identity provider.
## Turn on two-factor authentication
An admin can turn 2FA on for your Metabase:
1. Go to **Admin settings** > **Settings** > **Authentication**.
2. Find the **Two-factor authentication** card.
3. Toggle it to **Enabled**.
Once enabled, a **Security** tab shows up in each person's account settings, where they can enroll in 2FA.
If you configure Metabase through environment variables or a [config file](../configuring-metabase/config-file.md), the matching setting is [`MB_MFA_ENFORCEMENT`](../configuring-metabase/environment-variables.md#mb_mfa_enforcement). Set `MB_MFA_ENFORCEMENT` to `optional` to let people enroll, or `off` to turn 2FA off.
## Supported methods
- **Authenticator app (primary).** People scan a QR code with an app like Google Authenticator or 1Password, which then generates a new six-digit code every 30 seconds.
- **Email code (fallback at login).** If an admin has set up [email](../configuring-metabase/email.md), people who are already enrolled in two-factor authentication can have Metabase email them a one-time code when they can't reach their authenticator app. You can only use the code once, and expires after ten minutes. If email isn't configured, Metabase hides this option.
- **Recovery codes.** When someone enrolls in 2FA, Metabase gives them ten single-use codes to save.
Metabase doesn't support SMS codes or hardware keys (passkeys, U2F, or WebAuthn).
## Before you turn on two-factor authentication
### If you're self-hosting Metabase, set an encryption key
Set the [`MB_ENCRYPTION_SECRET_KEY`](../databases/encrypting-details-at-rest.md) environment variable so Metabase encrypts authenticator secrets at rest. If you turn on 2FA without it, Metabase shows a warning on the settings page.
If you're using Metabase Cloud, we've encrypted your keys for you.
### Set up email for the fallback code
Metabase requires people to use an email address as a login, but that doesn't mean that email is set up. The email fallback only appears if your Metabase can send [email](../configuring-metabase/email.md). If you skip setting up email, people will have to rely on their authenticator app and recovery codes only.
## See who's enrolled
When 2FA is on, go to **Admin settings** > **Settings** > **Authentication** > **Two-factor authentication**. The 2FA card shows how many people have enrolled in 2FA and how many haven't.
## Further reading
- [Enrolling in two-factor authentication](./account-settings.md#two-factor-authentication)
- [Authentication options](./start.md#authentication)

View File

@@ -103,7 +103,7 @@ You can export the results of a dashboard and its cards in different ways:
### Export dashboard as PDF
You can export a dashboard as a PDF. Click on the **Sharing** button, then select **Export as PDF**.
To export a dashboard as a PDF, click **Share**, then select **Export as PDF**. On dashboards with multiple tabs, select **Export tab as PDF** to export the current tab.
![Exporting a dashboard as a PDF](./images/export-dashboard-as-pdf.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -208,7 +208,7 @@ Syntax: `Percentile(column, percentile-value)`
Example: `Percentile([Score], 0.9)` would return the value at the 90th percentile for all values in that column.
Databases that don't support `percentile`: Druid, H2, MariaDB, MySQL, MongoDB, SQL Server, SQLite, Vertica. Presto only provides approximate results.
Databases that don't support `percentile`: Druid, MariaDB, MySQL, MongoDB, SQL Server, SQLite, Vertica. Presto only provides approximate results.
### Share
@@ -660,7 +660,7 @@ Syntax: `regexExtract(text, regular_expression)`
Example: `regexExtract([Address], "[0-9]+")`
Databases that don't support `regexExtract`: H2, SQL Server, SQLite.
Databases that don't support `regexExtract`: SQL Server, SQLite.
Related: [contains](#contains), [doesNotContain](#doesnotcontain), [substring](#substring).
@@ -1048,8 +1048,6 @@ Example: `Offset(Sum([Total]), -1)` would get the `Sum([Total])` value from the
Limitations are noted for each aggregation and function above, and here they are in summary:
**H2** (including Metabase Sample Database): `Median`, `Percentile`, `convertTimezone`, `regexExtract`, `datetime`, `float`, `splitPart`.
**Athena**: `convertTimezone`, `datetime`, `float`, `splitPart`.
**Databricks**: `convertTimezone`, `datetime`, `float`, `splitPart`.
@@ -1070,7 +1068,7 @@ Limitations are noted for each aggregation and function above, and here they are
**SQL Server**: `Median`, `Percentile`, `regexExtract`, `datetime`, `float`, `splitPart`.
**SQLite**: `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
**SQLite** (including Metabase Sample Database): `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
**Vertica**: `Median`, `Percentile`, `datetime`, `float`, `splitPart`.

View File

@@ -1,6 +1,6 @@
---
title: Custom visualizations
summary: Add your own chart types to Metabase by uploading visualization plugins built with the Custom Visualizations SDK.
summary: Add your own chart types to Metabase by uploading custom visualizations built with the Custom Visualizations SDK.
---
# Custom visualizations
@@ -19,7 +19,7 @@ This page covers how to add a custom visualization to your Metabase. To _create_
### Restrict image domains first
Before you can turn on custom visualizations, you need to enable [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains). A custom visualization runs third-party JavaScript in your Metabase. By restricting image (and font) domains, you limit where that code can load assets from, which narrows the ways a plugin could leak data through outbound asset requests. See [Only add plugins you trust](#only-add-plugins-you-trust).
Before you can turn on custom visualizations, you need to enable [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains). A custom visualization runs third-party JavaScript in your Metabase. By restricting image (and font) domains, you limit where that code can load assets from, which narrows the ways a custom visualization could leak data through outbound asset requests. See [Only add visualizations you trust](#only-add-visualizations-you-trust).
While custom visualizations are enabled, you can't turn **Restrict image domains** back off. You'll need to first disable custom visualizations.
@@ -38,8 +38,8 @@ Once you've [built the custom visualization](../../developers-guide/custom-visua
3. Click **Add visualization**.
- Bundles must be smaller than 5 MiB.
- Each plugin lists the Metabase versions it supports (for example, "Requires Metabase >=1.62"). If your Metabase version isn't in that range, Metabase rejects the upload and tells you which version the plugin needs.
- The **Manage visualizations** page shows each plugin's icon, name, the first eight characters of the bundle's hash, and its required Metabase version range, so you can tell which version is installed.
- Each custom visualization lists the Metabase versions it supports (for example, "Requires Metabase >=1.62"). If your Metabase version isn't in that range, Metabase rejects the upload and tells you which version the visualization needs.
- The **Manage visualizations** page shows each custom visualization's icon, name, the first eight characters of the bundle's hash, and its required Metabase version range, so you can tell which version is installed.
## Using a custom visualization
@@ -47,42 +47,44 @@ On a question, dashboard or document card, open the visualization sidebar (the *
![Gondola line chart](../images/gondola-line-chart.png)
If a custom visualization can't render the current query results (for example, if the query is missing a column the visualization needs), Metabase shows the error message from the plugin so you can adjust the query or pick a different chart.
If a custom visualization can't render the current query results (for example, if the query is missing a column the visualization needs), Metabase shows the error message from the custom visualization so you can adjust the query or pick a different chart.
Custom visualizations behave like built-in charts in most places:
- **Settings.** Click the **gear** icon in the visualization sidebar to change the visualization's settings. A plugin defines its own setting tabs: each setting names the section it belongs to.
- **Dark mode.** Plugins that use Metabase's colors adapt to [dark mode](../../people-and-groups/account-settings.md#theme) automatically.
- **Settings.** Click the **gear** icon in the visualization sidebar to change the visualization's settings. A custom visualization defines its own setting tabs: each setting names the section it belongs to.
- **Dark mode.** Custom visualizations that use Metabase's colors adapt to [dark mode](../../people-and-groups/account-settings.md#theme) automatically.
- **Icons.** A custom visualization shows its own icon in the visualization picker, and questions that use it show that icon in collections and bookmarks.
### Custom visualizations don't render in embeds
### Custom visualizations in embeds only work when using the SDK
Custom visualizations only render in your Metabase. In [embeds](../../embedding/start.md) and [public links](../../embedding/public-links.md) (public questions, dashboards, and documents), any card that uses a custom visualization falls back to the default visualization (a table).
The [Modular embedding SDK](../../embedding/sdk/introduction.md) can render custom visualizations. You allowlist the custom visualizations you want to load with the [`allowedCustomVisualizations` prop](../../embedding/sdk/config.md#custom-visualizations) on `MetabaseProvider`.
Other embedding types don't render custom visualizations. In [modular embedding](../../embedding/modular-embedding.md) with web components, [guest and static embeds](../../embedding/introduction.md), and [public links](../../embedding/public-links.md), any card that uses a custom visualization falls back to the default visualization for the query's results.
## Managing custom visualizations
_Admin > Settings > Custom visualizations > Manage visualizations_
- **Disable a visualization.** Any question, dashboard card, or document card that used the visualization falls back to the default visualization for that query's results. If you re-enable the plugin, those cards will go back to using the custom visualization.
- **Replace a bundle.** Upload a new `.tgz` to ship an updated version of a plugin. The new bundle's manifest `name` _must_ match the existing plugin's identifier, so questions that already use the visualization keep working.
- **Disable a visualization.** Any question, dashboard card, or document card that used the visualization falls back to the default visualization for that query's results. If you re-enable the visualization, those cards will go back to using the custom visualization.
- **Replace a bundle.** Upload a new `.tgz` to ship an updated version of a custom visualization. The new bundle's manifest `name` _must_ match the existing visualization's identifier, so questions that already use the visualization keep working.
- **Remove a visualization.** Cards that used the custom viz fall back to the default visualization.
## Exports
- **Dashboard subscriptions and alerts don't use custom visualizations**. Cards that use custom visualizations will fall back to a default visualization for the card's data shape.
- **PDF exports of dashboards include custom visualizations**.
- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that plugin. PNG export is off by default.
- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that visualization. PNG export is off by default.
## Only add plugins you trust
## Only add visualizations you trust
A custom visualization plugin runs JavaScript in your Metabase. Only upload plugins from sources you trust (like plugins you've built yourself, or have vetted).
A custom visualization runs JavaScript in your Metabase. Only upload custom visualizations from sources you trust (like ones you've built yourself, or have vetted).
Metabase runs custom visualizations in a sandbox to limit what a plugin can do:
Metabase runs custom visualizations in a sandbox to limit what a custom visualization can do:
- A plugin renders inside an isolated container and can't reach the rest of the Metabase app.
- A plugin can't call Metabase's APIs or make network requests.
- A custom visualization renders inside an isolated container and can't reach the rest of the Metabase app.
- A custom visualization can't call Metabase's APIs or make network requests.
While this sandboxing limits the damage a plugin can do, you still need to review the code.
While this sandboxing limits the damage a custom visualization can do, you still need to review the code.
## Further reading

View File

@@ -1,114 +0,0 @@
---
title: Auditing tools
redirect_from:
- /docs/latest/enterprise-guide/audit
---
# Auditing tools
> Auditing tools are deprecated. Instead, check out the (much better) [Usage analytics](./usage-analytics.md).
{% include plans-blockquote.html feature="Audit logs" %}
As an administrator of Metabase, you already know the importance of using data to understand how people interact with your products or services. With the Audit tool, you can use Metabase to understand how people in your organization use Metabase. It's, well, meta!
To view the audit logs, go to the top right of the screen and click on the **gear** icon > **Admin settings** > **Audit**. There's a lot of data available, not only about your people, but also about your questions, dashboards, databases and more! We'll walk you through each of the sections below.
## People
### Team members
Use the **People** section to gain a better understanding of how people are interacting with Metabase. You'll want to pay attention to the **Overview** tab, especially when you first launch Metabase at your organization — it will give you data around how many active and newly created accounts you have each day. Further down the page, you'll see charts showing you which people are most engaged.
![Team Members](./images/audit-team.png)
The **Team Members** panel has a view of other options for viewing your team's usage data. The **All Members** tab will give you a list of your team members, and stats about their activity, such as when they were last active.
The **Audit Log** tab will display, in chronological order, each query, who viewed it, and when. Note that if the person didn't save their query, its name will be listed as `Ad-hoc`. Each query name can be clicked to view more details about the person's interactions with it, including a full revision history. You'll also be able to view the query in Metabase. Note that this link will always show the latest version of the query - use the revision history to see changes over time.
Throughout the People section, names can be clicked to access the profile of a specific person’s activity. This profile includes:
- Dashboard views
- Query views
- Downloads
- Subscriptions and Alerts
![Team Members](./images/audit-teammember.png)
## Data
The Data section focuses on your databases, schemas and tables, and is divided into corresponding sections. Look here if you're trying to uncover queries and schemas that need optimization. Each section provides visualizations around the use and speed of querying against your databases, schemas or tables. You will also be able to view lists of stats about all of your databases, schemas and tables.
![Data](./images/audit-data.png)
## Items
The **Items** section focuses on questions, dashboards, downloads, and Subscriptions and Alerts.
### Questions
The Questions section will show you your most popular queries, as well as your slowest queries. If queries you think are important aren't appearing on your most popular queries list, you may want to make sure your team is focusing on the right things.
If important queries are appearing on the list of slowest queries, you will want to look at optimizing them. One option is to adjust your [caching settings](../configuring-metabase/caching.md), but there are plenty of other options as well. To help you determine if your optimization efforts are heading in the right direction, use the `Query views and speed per day` visualization at the bottom of the page.
![Items](./images/audit-questions.png)
A list of all of your questions is available as well, and you can see various data points about each question at a glance:
- Query Runs
- Average Runtime
- Cache Duration
- Total Runtime
- Database
- Table
- Collection
- Public Link
You can also click on any question to drill into a more detailed profile showing:
- View activity
- Revision History
- A full audit log of who viewed the question, and when
### Dashboards
The **Dashboards** section helps you understand what dashboards people are looking at, and to make sure they're having a smooth experience. If you notice that a popular dashboard has a high average question loading time, you can investigate further using the Questions section outlined above.
![Items](./images/audit-dashboards.png)
A list of all of your dashboards is available as well, and you can see various data points about each dashboard at a glance, such as:
- Number of views
- Average question execution time(ms)
- Number of cards
- Saved by
- Cache duration
- Public link
- Saved on
- Last edited on
### Downloads
Use the **Downloads** section to understand which people are downloading (or exporting) data, and the size (number of rows) of the downloads they're performing. This section contains some visualizations, as well as a list of all downloads.
### Subscriptions and Alerts
Here admins can get an overview of all of the [dashboard subscriptions][dashboard-subscriptions] and [alerts][alerts] that are currently active for that Metabase.
- Dashboard name (or Question name for Alerts)
- Recipients
- Type (e.g., email or Slack)
- Collection
- Frequency
- Created By
- Created At
- Filters
Admins can add and remove people from a subscription or alert by clicking on the item's **Recipients** number. Admins can also delete the subscription or alert entirely by clicking on the **X** on the relevant line.
Everyone can view all of their subscriptions and alerts by clicking the **profile or grid icon** in the upper right, then clicking their **profile name and email** to open **Account settings**, and navigating to **Notifications**.
For more, see [how permissions work with alerts and subscriptions](../permissions/notifications.md).
[alerts]: ../questions/alerts.md
[dashboard-subscriptions]: ../dashboards/subscriptions.md

View File

@@ -344,7 +344,7 @@ Columns:
- Conversation ID
- Created At
- User ID
- Summary
- Title
- User Display Name
- Message Count
- User Message Count
@@ -427,6 +427,7 @@ Columns:
- Action Qualified ID
- Query
- Embedding Client
- Embedding Client Identifier
- Is Preview
- Embedding SDK Package Version
- Metabase Version
@@ -530,6 +531,7 @@ Columns:
- Entity ID
- Entity Qualified ID
- Embedding Client
- Embedding Client Identifier
- Auth Method
- Is Preview
- Metabase Version

View File

@@ -1,5 +1,8 @@
---
title: Usage analytics
redirect-from:
- /docs/latest/enterprise-guide/audit
- /docs/latest/usage-and-performance-tools/audit
---
# Usage analytics

View File

@@ -0,0 +1,6 @@
# Each test app under this directory commits its `src/` and its `data_app.yaml`
# manifest. Everything else — the SDK/template base pulled in at test setup and
# the built `dist/` bundle (Vite API) — is generated, so it's ignored here.
/*/*
!/*/src
!/*/data_app.yaml

View File

@@ -0,0 +1,2 @@
name: Custom Error Component
path: ./dist/index.js

View File

@@ -0,0 +1,34 @@
import {
type SdkErrorComponentProps,
StaticQuestion,
} from "@metabase/embedding-sdk-react";
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app";
// A question id that does not exist, so the SDK takes its error path — which this
// app overrides with its own `errorComponent`, asserting an app-supplied component
// wins over the host's default neutral error state.
const MISSING_QUESTION_ID = 999999999;
function CustomError({ message }: SdkErrorComponentProps) {
return (
<div data-testid="custom-error-component">Custom app error: {message}</div>
);
}
function App() {
return (
<div data-testid="custom-error-app" style={{ padding: 24 }}>
<h1>Custom error app</h1>
<div style={{ height: 300 }}>
<StaticQuestion questionId={MISSING_QUESTION_ID} />
</div>
</div>
);
}
const factory: DataAppFactory = () => ({
component: App,
providerProps: { errorComponent: CustomError },
});
export default factory;

View File

@@ -0,0 +1,2 @@
name: Kitchen Sink
path: ./dist/index.js

View File

@@ -0,0 +1,436 @@
import {
InteractiveQuestion,
StaticQuestion,
useAction,
} from "@metabase/embedding-sdk-react";
import {
aggregations,
breakout,
copy,
DataAppLink,
DataAppRouter,
filter,
orderBy,
useDataAppLocation,
useMetabaseQuery,
useMetabaseQueryObject,
} from "@metabase/embedding-sdk-react/data-app";
import { type ComponentType, useEffect, useState } from "react";
import { getTestEnv } from "./test-env";
// The sandbox throws host-realm errors, whose `instanceof Error` can be false in
// the guest; read `.message` defensively.
const describeError = (err: unknown): string =>
(err as { message?: string })?.message ?? String(err);
const PROBE_COLOR = "rgb(0, 128, 0)";
function Overview() {
const { scalarQuery, questionQuery } = getTestEnv();
const ordersCount = useMetabaseQuery(scalarQuery);
const totalOrders = ordersCount.data?.rawRows?.[0]?.[0];
const ordersQuery = useMetabaseQueryObject(questionQuery);
return (
<div data-testid="data-app-content" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Orders overview</h1>
<section
style={{
padding: 16,
border: "1px solid #e0e0e0",
borderRadius: 8,
marginBottom: 24,
maxWidth: 240,
}}
>
<div
style={{
color: "#6b7280",
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 0.5,
}}
>
Total orders
</div>
<div
data-testid="orders-count"
style={{ fontSize: 32, fontWeight: 700 }}
>
{ordersCount.isLoading ? "…" : String(totalOrders ?? "—")}
</div>
</section>
<h2 style={{ margin: "0 0 8px" }}>All orders</h2>
<div style={{ height: 400 }}>
{ordersQuery.query ? (
<InteractiveQuestion card={{ query: ordersQuery.query }} />
) : (
<div>…</div>
)}
</div>
</div>
);
}
function Details() {
return (
<div data-testid="data-app-details" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Order details</h1>
<p>A nested page reached through the data-app router.</p>
</div>
);
}
function QueryStates() {
const { scalarQuery, errorQuery } = getTestEnv();
const broken = useMetabaseQuery(errorQuery!);
const working = useMetabaseQuery(scalarQuery);
const count = working.data?.rawRows?.[0]?.[0];
return (
<div data-testid="data-app-query-states" style={{ padding: 24 }}>
<h1>Query states</h1>
<div data-testid="query-error">{broken.error ? "error" : "no-error"}</div>
<div data-testid="query-loading">
{working.isLoading ? "loading" : "done"}
</div>
<div data-testid="query-value">
{working.isLoading ? "…" : String(count ?? "—")}
</div>
<button
type="button"
data-testid="query-refetch"
onClick={() => {
working.refetch();
}}
>
refetch
</button>
</div>
);
}
function StaticQuestionPage() {
const { questionQuery } = getTestEnv();
const q = useMetabaseQueryObject(questionQuery);
return (
<div data-testid="data-app-static-question" style={{ padding: 24 }}>
<h1>Static question</h1>
<div style={{ height: 360 }}>
{q.query ? (
<StaticQuestion card={{ query: q.query }} />
) : (
<div data-testid="static-question-loading">…</div>
)}
</div>
</div>
);
}
function Combinators() {
const combinators = getTestEnv().combinators!;
const countAgg = aggregations.count();
const q = useMetabaseQuery({
source: combinators.source,
filters: [filter(combinators.filterField, ">", combinators.filterValue)],
aggregations: [countAgg],
breakouts: [breakout(combinators.breakoutField)],
orderBys: [orderBy(countAgg, "desc")],
});
return (
<div data-testid="data-app-combinators" style={{ padding: 24 }}>
<h1>Combinators</h1>
<div data-testid="combinators-loading">
{q.isLoading ? "loading" : "done"}
</div>
<div data-testid="combinators-error">
{q.error ? "error" : "no-error"}
</div>
<div data-testid="combinators-rowcount">
{String(q.data?.rows?.length ?? 0)}
</div>
</div>
);
}
function Actions() {
const { actionId, actionParams } = getTestEnv();
const action = useAction(actionId ?? null);
const [output, setOutput] = useState("idle");
const onExecute = async () => {
try {
const res = await action.execute(actionParams ?? {});
setOutput(res ? "returned-result" : "returned-null");
} catch {
setOutput("threw");
}
};
return (
<div data-testid="data-app-actions" style={{ padding: 24 }}>
<h1>Actions</h1>
<button type="button" data-testid="action-execute" onClick={onExecute}>
execute
</button>
<button
type="button"
data-testid="action-reset"
onClick={() => action.reset()}
>
reset
</button>
<div data-testid="action-executing">
{action.isExecuting ? "executing" : "idle"}
</div>
<div data-testid="action-result">
{action.result ? "has-result" : "no-result"}
</div>
<div data-testid="action-error">
{action.error ? "has-error" : "no-error"}
</div>
<div data-testid="action-output">{output}</div>
</div>
);
}
function Clipboard() {
const [status, setStatus] = useState("idle");
const onCopy = async () => {
try {
await copy("data-app-clipboard-payload");
setStatus("copied");
} catch (err) {
setStatus(`failed: ${describeError(err)}`);
}
};
return (
<div data-testid="data-app-clipboard" style={{ padding: 24 }}>
<h1>Clipboard</h1>
<button type="button" data-testid="clipboard-copy" onClick={onCopy}>
copy
</button>
<div data-testid="clipboard-status">{status}</div>
</div>
);
}
// With no app-supplied `errorComponent`, the host's default neutral error state
// renders here.
function MissingQuestion() {
return (
<div
data-testid="data-app-missing-question"
style={{ padding: 24, height: 300 }}
>
<StaticQuestion questionId={999999999} />
</div>
);
}
// Throws during render so the host's BundleErrorBoundary reports the failure to
// the parent, which renders its themed error screen.
function ThrowingPage(): JSX.Element {
throw new Error("intentional data-app bundle render error");
}
type Probe = { id: string; run: () => void };
const BLOCKED_PROBES: Probe[] = [
{ id: "script", run: () => document.createElement("script") },
{ id: "window-open", run: () => window.open("https://blocked.test") },
{ id: "alert", run: () => window.alert("x") },
{ id: "history", run: () => window.history.pushState({}, "", "/x") },
{
id: "keydown-listener",
run: () => document.addEventListener("keydown", () => {}),
},
{ id: "websocket", run: () => new WebSocket("wss://blocked.test") },
{
id: "sendbeacon",
run: () => navigator.sendBeacon("https://blocked.test"),
},
];
function Sandbox() {
const { sandbox } = getTestEnv();
const [probeResults, setProbeResults] = useState<Record<string, string>>({});
const [innerHtml, setInnerHtml] = useState("pending");
const [blockedFetch, setBlockedFetch] = useState("pending");
const [allowedFetch, setAllowedFetch] = useState("pending");
const [blockedXhr, setBlockedXhr] = useState("pending");
const [allowedXhr, setAllowedXhr] = useState("pending");
useEffect(() => {
const results: Record<string, string> = {};
for (const probe of BLOCKED_PROBES) {
try {
probe.run();
results[probe.id] = "not blocked";
} catch {
results[probe.id] = "blocked";
}
}
setProbeResults(results);
// `innerHTML` is distorted through DOMPurify, which strips <script>.
try {
const el = document.createElement("div");
el.innerHTML = "<script>window.x=1</script><b>ok</b>";
setInnerHtml(el.querySelector("script") ? "not stripped" : "stripped");
} catch (err) {
setInnerHtml(`threw: ${describeError(err)}`);
}
if (sandbox) {
fetch(sandbox.blockedUrl)
.then((res) => setBlockedFetch(`ok: ${res.status}`))
.catch((err) => setBlockedFetch(`blocked: ${describeError(err)}`));
fetch(sandbox.allowedUrl)
.then((res) => setAllowedFetch(`ok: ${res.status}`))
.catch((err) => setAllowedFetch(`blocked: ${describeError(err)}`));
if (sandbox.xhrBlockedUrl) {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", sandbox.xhrBlockedUrl);
setBlockedXhr("not blocked");
} catch (err) {
setBlockedXhr(`blocked: ${describeError(err)}`);
}
}
if (sandbox.xhrAllowedUrl) {
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => setAllowedXhr(`ok: ${xhr.status}`));
xhr.addEventListener("error", () => setAllowedXhr("blocked: error"));
try {
xhr.open("GET", sandbox.xhrAllowedUrl);
xhr.send();
} catch (err) {
setAllowedXhr(`blocked: ${describeError(err)}`);
}
}
}
}, [sandbox]);
return (
<div data-testid="data-app-sandbox" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Sandbox</h1>
{BLOCKED_PROBES.map((probe) => (
<div key={probe.id} data-testid={`probe-${probe.id}`}>
{probeResults[probe.id] ?? "pending"}
</div>
))}
<div data-testid="probe-innerhtml">{innerHtml}</div>
<div data-testid="blocked-fetch-result">{blockedFetch}</div>
<div data-testid="allowed-fetch-result">{allowedFetch}</div>
<div data-testid="blocked-xhr-result">{blockedXhr}</div>
<div data-testid="allowed-xhr-result">{allowedXhr}</div>
</div>
);
}
function Isolation() {
const [cssInjected, setCssInjected] = useState("pending");
const [jsMarker, setJsMarker] = useState("pending");
useEffect(() => {
// `createElement("style")` is the one tag the data-app sandbox permits, so
// apps can style their own sandboxed document.
try {
const style = document.createElement("style");
style.textContent = `.isolation-css-probe { color: ${PROBE_COLOR}; } body { background-color: ${PROBE_COLOR}; }`;
document.head.appendChild(style);
setCssInjected("ok");
} catch (err) {
setCssInjected(`failed: ${describeError(err)}`);
}
try {
const globals = globalThis as Record<string, unknown>;
globals.__DATA_APP_ISOLATION_MARKER__ = "in-app";
setJsMarker(String(globals.__DATA_APP_ISOLATION_MARKER__));
} catch (err) {
setJsMarker(`failed: ${describeError(err)}`);
}
}, []);
return (
<div data-testid="data-app-isolation" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Isolation</h1>
<div data-testid="css-injected">{cssInjected}</div>
<div data-testid="isolation-css-probe" className="isolation-css-probe">
css probe
</div>
<div data-testid="js-marker">{jsMarker}</div>
</div>
);
}
const ROUTES: Record<string, ComponentType> = {
"/details": Details,
"/sandboxing": Sandbox,
"/isolation": Isolation,
"/query-states": QueryStates,
"/static-question": StaticQuestionPage,
"/combinators": Combinators,
"/actions": Actions,
"/clipboard": Clipboard,
"/missing-question": MissingQuestion,
"/throw": ThrowingPage,
};
function Shell() {
const { pathname, navigate } = useDataAppLocation();
const Page = ROUTES[pathname] ?? Overview;
return (
<div style={{ fontFamily: "sans-serif" }}>
<nav
style={{
padding: 16,
borderBottom: "1px solid #e0e0e0",
display: "flex",
gap: 16,
flexWrap: "wrap",
}}
>
<DataAppLink to="/">Overview</DataAppLink>
<DataAppLink to="/details">Details</DataAppLink>
<DataAppLink to="/sandboxing">Sandbox</DataAppLink>
<DataAppLink to="/isolation">Isolation</DataAppLink>
<button
type="button"
data-testid="navigate-to-details"
onClick={() => navigate("/details")}
>
navigate to details
</button>
</nav>
<div data-testid="current-pathname" style={{ padding: "0 16px" }}>
{pathname}
</div>
<Page />
</div>
);
}
export default function App() {
return (
<DataAppRouter>
<Shell />
</DataAppRouter>
);
}

View File

@@ -0,0 +1,7 @@
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app";
import App from "./App";
const factory: DataAppFactory = () => ({ component: App });
export default factory;

View File

@@ -0,0 +1,20 @@
// The type is the tests' contract and lives with them; this file is only how the
// app reads it back inside the sandbox realm.
import type { DataAppTestEnv } from "../../../../helpers/data-app-test-env";
declare global {
// eslint-disable-next-line no-var -- `var` (not `const`) so `globalThis.x` typechecks
var __METABASE_DATA_APP_TEST_ENV__: DataAppTestEnv | undefined;
}
export function getTestEnv(): DataAppTestEnv {
const env = globalThis.__METABASE_DATA_APP_TEST_ENV__;
if (!env) {
throw new Error(
"data-app test env was not injected — pass `testEnv` to H.mockDataApp",
);
}
return env;
}

Some files were not shown because too many files have changed in this diff Show More