Compare commits

...

208 Commits

Author SHA1 Message Date
John Swanson
df8499a664 ask whether to reuse h2 2026-02-03 08:56:18 -08:00
John Swanson
c33ed0162f better h2 querying 2026-02-03 08:35:56 -08:00
John Swanson
33b745bf37 Nicer output 2026-02-03 07:26:20 -08:00
John Swanson
59c2c7e0dd improved isolation for ./bin/mage dev-repl 2026-02-03 06:07:00 -08:00
John Swanson
b57d117c7e ./bin/mage dev-repl
This launches a metabase dev repl.

Configurable:
- database type
- database edition
- backend port
- config file
- token
- metabase edition (oss vs ee)

You can just run `./bin/mage dev-repl` and get an interactive CLI that
will ask you about these settings, launch the database, start the
frontend (on a random port), and start Metabase configured to talk to
that database/frontend.

Or you can specify options from the command line, like
`./bin/mage dev-repl -e ee -t all-features -d postgres`.
2026-02-02 13:08:42 -08:00
Eric Normand
40c0273582 Add test for lib.card/fallback-display-name (#68865) 2026-02-02 17:16:14 +00:00
Eric Normand
b5c0fdd4e3 Fully test source-model-card (#68872)
* Add tests for source-model-card

source-model-card is only called from one function, card-returned-columns.
So we have to test source-model-card (which is private) through card-returned-columns.

This kills all mutants but 3, which have to do with the self-reference check in source-model-card. Is it really
necessary? It is an impossible branch to reach.
2026-02-02 10:56:16 -06:00
Eric Normand
1296b4671b Test lib.card/saved-question-metadata (#68868)
* Test lib.card/saved-question-metadata

* kick ci
2026-02-02 10:52:42 -06:00
Alexander Polyankin
b9ba7de970 Convert Transform Runs page to TreeTable and add a sidebar (#68892) 2026-02-02 16:26:54 +00:00
Ryan Laurie
0fa535476d Add Growth Engineering to Teams (#68959)
add growth engineering to teams
2026-02-02 07:59:57 -07:00
github-automation-metabase
b0d28fcfa4 Auto-update documentation for master (#68941)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-02-02 09:10:33 -05:00
Nemanja Glumac
6b1ceaf93b Update the latest docker tag when the latest major version matches (#68946) 2026-02-02 14:51:46 +01:00
Ngoc Khuat
db7f048d82 Snowflake: fix flaky test (#68844) 2026-02-02 12:12:04 +00:00
Fraser
4842e16f7c Upgrade to eslint v9 (#68772) 2026-02-02 22:39:13 +13:00
Nemanja Glumac
cd1be87231 Upgrade Chrome setup action to version 2 (#68915) 2026-02-02 21:21:45 +13:00
Fraser
89a8970e4a Fix no unused expression violations (#68939) 2026-02-02 13:21:07 +13:00
Fraser
2e7aafe3a0 Fix rules of hooks violations (#68940) 2026-02-02 13:20:42 +13:00
metabase-bot[bot]
39310d4798 [chore] Update E2E test timings with recent data (#68936) 2026-02-02 09:10:32 +13:00
Ryan Laurie
23be6baa0e Entity Picker v26 (#66978)
* new pickers

implement all pickers

remove test code

remove dead code

type fixes

reorganize

type fixes

data picker plus plus

more fixes

type fixes

fix column test ids

type and testfixes

some e2e test updates and kick off ci again

improved default states

more e2e test updates

test updates

better loading state

lint fixes

better root namespace handling

fix more root collection nonsense

improve backparsing logic

fix closing multiple modals on esc or enter

more e2e test updates

e2e test updates 4

fix (some) backparsing and add tests

fix spacing

e2e test updates 5

more backparse fixes

weird snippet deal

more tenant shenanigans

sweet sweet unit tests

add back NewDashboardDialog

more e2e tests and start NewDashboardDialog unit test

lint fixes

fix infinite render loop 😅

various fixes

bug fixes

search improvements

better cache hitting

dialog unit tests

test fixes

mucho test fixes

* fix conflict resolution errors

* test and type fixes

* bug and test fixes

* New folder button for snippets and transforms, e2e tests

* fix some dashboard cases

* test fixes

* entity picker e2e tests

* much fixo

* more e2e fixes

* fix some flakes

* prettier

* fixes

* more e2e test updates

* fix sdk component tests

* some stabilization

* fix dashboard saving bug

* prettier

* address review comments

* test fixes

* fix sandbox tests

* obey the linter

* cant pass null namespace

* oh types

* timeline

* fix default expanded 😫

---------

Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
2026-01-31 02:57:52 +00:00
Nemanja Glumac
7ed4b012e5 Document rootless docker limitations (#68913) 2026-01-30 23:47:33 +01:00
Tyler Plude
e38d87f616 Metabot OSS Single Shot Text-2-Sql (#67883)
* Experiment with communicating with LLMs in Clojure

* Libor comments

* switch to new openai /v1/responses api, check for structured output support

* support for claude to show how different adapters are

* linter fixes

* update llm config edn

* init chat completion endpoint and env vars

* basic sqlgen BE endpoint

* BE test script

* basic table schema context extraction

* temp hack EE inline entrypoint to use OSS sql gen

* BE OSS async & streaming support

* BE OSS async & streaming support

* structured json response

* dialect instructions and sys prompt mustache

* hacky disable file logging via env var

* improved dialect mapping

* improved table metadata

* fix merge conflict

* abstract out sql generation to a seperate hook

* move MetabotPromptInput and MetabotInlineSQLPrompt components into OSS land

* always register metabot context rather than doing it conditionally in enterprise

* move OSS implementation into a seperate file

* oss implementation first pass

* add setting to know if the feature is enabled or not, optionally show ui based on if it is enabled, consolidate state into a single hook call to fix OSS issues, gets everything working

* clean up

* add test coverage

* revert metabot context changes, they did not end up being needed

* restrict @mentions to tables in OSS and models, tables, questions in EE

* send current sql value and no longer send buffer id

* add anthropic provider

* dumb module resolution workaround to get unit tests working correctly

* if there is only one search model provided preselect it

* tweak sys prompt; add table descriptions

* improved table & field metadata

* tweak model env var

* add openai settings

* fix CSP tiptap nonce console error

* fix oss not having access to default import of use-metabot-sql-suggestion hook in prod builds

* add backend support for source_sql

* OSS SQL Generation v1.6 (#68584)

* update layout

* impl table input bar

* persist state on open / close of inline sql prompt input

* ui improvements, referenced_entities now in req/res body

* support referenced_entities param in /generate-sql; add extract-tables endpoint

* use search, fix some ux/ui issue with dropdown

* fix sizing issue

* simplify generate-sql response format; properly union the referenced entities

* return table model spec in response

* updated backend tests

* improve table selection logic using new endpoint changes w/ richer data

* rate limiting; throttling; http timeouts; max resp tokens

---------

Co-authored-by: Sloan Sparger <sloansparger@gmail.com>

* small spacing / sizing corrections

* fix ee extracting tables when it should not, add test cases

* handle disabeld state and enter press better

* cleanup

* fix modules

* add init; fix kondo

* test fix

* fix modules

* Add snowplow events to oss sqlgen endpoints (#68822)

* Copy token_usage snowplow schema from ai-service

* Add :snowplow/token_usage to metabase.analytics.snowplow/schema->version

* Add basic anthropic cost estimation based on static tables

* Return :usage and :duration-ms from llm.anthropic/chat-completion

* Add snowplow token_usage tracking to oss /generate-sql endpoint

* Add snowplow token_usage tracking to oss /generate-sql-streaming endpoint

* Add snowplow simple_event tracking for sqlgen success and failure events

* Consolidate snowplow tracking tests

* Appease whitespace linter

* Don't use non-api analytics.setting namespace

* Ensure duration_ms is an integer in snowplow events

* Add note about oss instances to hashed_metabase_license_token schema description

Co-authored-by: Thomas Schmidt <thomas@metabase.com>

* Fix snowplow lint by adding old versions of token_usage schemas

The snowplow linter was complaining that these older schemas were missing. These versions had been deleted from the
ai-service repo, so weren't copied over. Restore them from the last version in ai-service prior to being deleted.

---------

Co-authored-by: Thomas Schmidt <thomas@metabase.com>

* Add README reminder to token_usage snowplow schema

See also https://github.com/metabase/ai-service/pull/605

* cleanup streaming

* rm stream test

* pr feedback & test fixes

* test fix

* [Metabot] OSS SQL generation admin UI changes (#68824)

* wip

* load models async, move model input to a select, show error if deprecated model

* move to password input for api key

* handle case where env vars have been set via env vars and can not be changed

* clean up oss view

* remove token checks for deciding to upsell metabot

* be tests for new models endpoint

* fix bug

* fix broken api tests

* ui feedback, token validation, style fixes

* unit tests

* fix cypress

* fix e2e test tag

* type fixes

* condo / eastwood fixes

* fix unit tests

* fix e2e test

* fix stale css var reference

* pr feedback

* fix var

* fix n+1 query; improve metadata fetching

* driver multimethod for dialect instruction resource; review PR cleanups & fixes

* pr feedback

* fix added in driver multimethod

* whitespace lint

* address my own comments

---------

Co-authored-by: Oleksandr Solovyov <o@solovyov.net>
Co-authored-by: Sloan Sparger <sloansparger@gmail.com>
Co-authored-by: appleby <86076+appleby@users.noreply.github.com>
Co-authored-by: Thomas Schmidt <thomas@metabase.com>
Co-authored-by: Sloan Sparger <sloansparger@users.noreply.github.com>
Co-authored-by: Noah Moss <noahbmoss@gmail.com>
2026-01-30 14:40:48 -08:00
Rodrigo López Dato
f6e9a91db7 Show Cloud region IDs next to names (#68918) 2026-01-30 18:49:02 -03:00
Uladzimir Havenchyk
423aafea99 replace moment with dayjs in cljs (#66914)
* replace moment with dayjs in cljs

* Fix CLJS compilation with DayJS port of metabase.util.time

* fix some tests

* fix time parsing in drill chart

* formatting

* make a better fix

* kondo

* fix sdk test

* indent

* drop moment from locales

* reorder imports

* ran bin/mage cljfmt-all to indent

* Add an ignore for 1 unsorted namespace

- cljfmt makes the namespaces unordered -> kondo complains

* typo

* attampt to move logic to clojurescript

* one more thing to remove business logic from metabase-lib

* address cljs review

---------

Co-authored-by: Braden Shepherdson <braden@metabase.com>
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
2026-01-30 23:38:35 +03:00
Riley Thompson
78ae095422 Athena and Databricks adjustments for data upload (#68821)
* fix upload

* use shared impl
2026-01-30 20:18:11 +00:00
bryan
297d612165 Simplify CI workflow trigger conditions (#68917)
Reverts 730265b198
2026-01-30 21:01:50 +01:00
Edward Paget
8ae78915ed feat: add library/transforms toggles to modal (#68852)
* feat: add library/transforms toggles to modal

In the 'Set up git sync' modal in data studio this adds pre-selected
toggles to sync the library and transforms. This also creates the
library if you select it to sync, but it has not yet been created.

* fixup! push the library/transforms behavior into CollectionList

* fixup! better types to reduce casting

* fixup! missing mock setup

* Update enterprise/frontend/src/metabase-enterprise/remote_sync/components/RemoteSyncAdminSettings/RemoteSyncSettingsForm.tsx

Co-authored-by: Anderson <ands.alves.nunes@gmail.com>

* fixup! separate library sync component

* fixup! prettier

---------

Co-authored-by: Anderson <ands.alves.nunes@gmail.com>
2026-01-30 19:31:09 +00:00
Nick Fitzpatrick
02b0bbaf76 updating frontend dev guid colors section (#68904) 2026-01-30 15:26:31 -04:00
Alexander Polyankin
2062c3fb13 Fix colors and strings in the deps sidebar (#68901)
Fix colors and ttag
2026-01-30 19:15:18 +00:00
Eric Normand
37c991c292 Mutation testing basics (#68857)
Initial commit of mutation testing library.
2026-01-30 10:52:47 -06:00
Riley Thompson
3142c39435 Run transforms with a temp table + rename to avoid blocking queries (#68756)
* add fix and test

* dont drop bigquery tables and add test

* use atomic renames when possible

* use helper functions

* use existing wait-for-table

* use shared temp-table-name

* small change

* remove bigquery hack, make more similar to python impl

* address comments

* use random suffix and table name for temp tables

* use table hash

* update temp-table-name

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

* mocked test to verify strategy selection

* fix test

* test description

* remove test

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 11:39:26 -05:00
Edward Paget
898b7b4c38 feat: apply remote sync settings to snippets (#68902)
Adds new spec-driven methods to detect writability of remote-sync
controlled methods. These use the existing sync-eligibility settings so
in the future we can straightforwardly apply these settings to
remote-sync controlled models (TODO: follow up to build them for the
other models).

Adds batched-versions of these checks to avoid n+1 queries when loading
can_write for models.

Adds tests of remote-sync checks for snippets.
2026-01-30 10:33:47 -06:00
Maksym Yakubych
c5ff6bbbbd fix: documents charts rendering when resizing is sluggish (#68204) 2026-01-30 15:31:04 +00:00
Noah Moss
50dac4248d Split out more granular error codes for metabot tools (#68627)
* Split out more granular 400 vs 404 error codes for metabot tools

* a couple new tests

* fix test

* fix tests, cleanup

* discrete error handling in resolve-datasource

* refactor

* undo some refactor to fix tests
2026-01-30 10:04:53 -05:00
Timofey Kachalov
df8fd2aa98 Add content translations for aggregations (#68733) 2026-01-30 17:58:24 +04:00
Oleksandr Solovyov
e597be0c10 make bin/mage run-tests able to run a single test (#67450) 2026-01-30 12:35:11 +00:00
Mahatthana (Kelvin) Nomsawadi
64fb9ea259 feat(sdk, embed-js): Add an alerts support to modular embeddings (#68777)
* Add an alerts button on the SDK (#67796)

* Prepare to add alerts to SDK

* Remove unrelated prop from storybook

* Add basic alerts button

* Only show alerts button with the proper flag

* Prevent download button from being shown without proper flag

* use CSS to hide things instead of JS

* Properly position all question buttons

* Use JS to hide empty divs as pure CSS is impossible

* Render alert modal ✌️

* Fix type

* Add todo

* Structure the components better

* Control alert modal state

* Add interactive question storybook

* Make interactive and static question button styles consistent

* Add alerts modal to interactive question

* Make alert button works with custom layouts

* Fix tests

* Fix unit tests

* Fix import order issue

* Remove outdated comment

* Add the same condition to display alert button from the core app

* Polish

* Modify question storybooks to show a question instead of a model

* Hide empty elements more reliably

* Add more conditions to display alert button

* Remove unused `classNames` prop

* Use the same style as the interactive question one

* Remove outdated diff

* Fix alerts button not working in custom layouts

* Remove another unused code

* Add a basic test

* Add unit tests

* fix visibility logic

* Fix E2E tests

* Make assertion on static question tests better

* Improve assertions

* Add interactive question unit tests

* Add e2e test for static question

* Add interactive question e2e tests

* Fix subcomponent

* clarify comment

* Hide other channels than email on the alert modal (#68184)

* Hide channel options on alerts modal

* Add unit tests

* Make the test name clearer

* Fix can't view created notifications the first time (#68197)

* Fix can't view created notifications

* Add tests

* Remove unnecessary return type

* Add a safety guard

* Hide Alert (and download) button on edit mode (#68189)

Hide some buttons on edit mode as it makes no sense there

* Alerts polishing (#68477)

* Misc. improvements

* Make it typesafe

* Add alerts to modular embedding (#68286)

* Add the option in modular embedding wizard

* Support `withAlerts` in modular embedding

* Close the modal after hiding the alert button

* Add E2E tests

* Fix type

* Fix `withAlerts` not working on modular embedding with `drills: false`

* It's 2026!

* 1 plugin to rule 'em all!

* Fix E2E after merging master

* Fix test name

* Add alerts analytics (#68531)

* Add alert setup analytics

* Track withAlerts on modular embedding

* Update snowplow schema

* Fix E2E tests

* Fix schema version

* Remove links from alerts test email (#68547)

* Remove links from alerts template

* Remove links from alert email computed part

* Add tests

* Make embedding util easier to use

* Make alerts and subscriptions payload lighter

* Remove unexpected pprint

* Remove links from alert email via tasks (#68610)

* Save notification card's disable_links when creating

* Add tests
2026-01-30 11:32:31 +00:00
William
52f689cb36 Fix cloud implicit join tests (#68762)
A previous pr broke some tests on master, and this fixes them.
2026-01-30 10:19:47 +00:00
Romeo Van Snick
c51ca23c95 Use getter function to move element (#68714) 2026-01-30 15:10:26 +11:00
Alexander Polyankin
0bacd6c5ca Dependency diagnostics sidebar updates (#68825) 2026-01-30 03:52:21 +00:00
Phoomparin Mano
f46fdd8d27 fix(sdk): fix failing tests for white-labeled colors (#68790) 2026-01-30 09:55:49 +07:00
Fraser
27fd3aac36 Delete fuzzing workflow (#68880) 2026-01-30 15:13:58 +13:00
bryan
92a60c8529 CI: query-processor module should trigger driver tests (#68877) 2026-01-30 01:14:36 +00:00
Anderson
ee030befe2 Fix empty Python Library not showing placeholder/comment (#68826) 2026-01-29 22:11:38 -03:00
Nick Fitzpatrick
5f956d51bd Remove brand-light and brand-lighter (#68290)
* brand key adjustments

* loki tests

* handle the forgotten brand-dark

* Update Loki Snapshots

* removing brand-hover from dynamic-css-vars

* Adjustments to calendar picker colors

* small adjustments on the permissions pages

* sdk: remove background-hover tests from sdk as dynamic css derives are replaced with color-mix

* stupid spelling

* building shared resources

* Adjusting unit test, loki images

* rebasing adjustments

* Update Loki Snapshots

* small color fixes while I wait for loki

* re-adding highlighted class

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Phoomparin Mano <phoomparin@gmail.com>
2026-01-29 20:06:28 -04:00
John Swanson
590c8945fc Analysts can do crud on segments and measures (#68501)
* Data Analyst CRUD on measures/segments

Data analysts with unrestricted view-data permissions on a table can now
create, update, and delete segments and measures for that table
(previously superuser-only).

`can-read?` was already granted by the `manage-table-metadata` permission.

Add permission tests for `can-write?` and `can-create?` methods.
2026-01-29 23:53:21 +00:00
metamben
c056b03d89 Deps breaking broken (#68866)
* Add rename /broken to /breaking and add new /broken

---------

Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
2026-01-30 02:47:55 +03:00
Maksym Yakubych
b4cf7383a4 fix: y axis not shown even if it would fit (#68861) 2026-01-29 15:09:10 -08:00
Edward Paget
9c003314be feat: enable transform setting on first import (#68807)
* feat: enable transform setting on first import

When a we import a remote that has transforms set the
`remote-sync-transforms` setting to true.

Update the remote sync object table clean up to be driven by the spec
configuration.

Add tests showing that transforms are consistently synced and erased
according to the following rules:

1. if the instance has `remote-sync-transforms` off and local transforms
and the remote does not have transforms content they are preserved after
an import
2. if the instance has `remote-sync-transforms` off and local transforms
and the remote has transforms content, the local transforms are
completely replaced by the remote.
3. if the instance has `remote-sync-transforms` on and the remote has
transforms content any local transforms are completely replaced by the
remote.
4. if the instance has `remote-sync-transforms` on has local transforms,
but the remote has no transforms, the local transforms are removed on import.

* fixup! relocate settings update

* fixup! don't let python libraries clash on import

* fixup! transactions for mysql
2026-01-29 22:16:41 +00:00
kevinkelleher12
06a24797b4 Remove Security and Appearance from embedding settings related links on OSS (#68151) 2026-01-29 20:53:58 +00:00
Edward Paget
ad8af02598 feat: transform setting into collection list (#68758)
* feat: transform setting into collection list

Move the setting for syncing transforms into the list of collections
available to remote sync, so conceptually the transforms root collection
looks like a standard syncable collection.

* Clean up FE a little bit

* undo unneeded changes

---------

Co-authored-by: Anderson <anderson@metabase.com>
2026-01-29 14:47:55 -06:00
Riley Thompson
798fc4aff6 Custom test on connection checkout for clickhouse (#68808)
* work around clickhouse bug

* driver change log

* use c3p0 test

* Add todo comment

* kondo

* more kondo
2026-01-29 20:32:51 +00:00
Nick Fitzpatrick
eb0a8d4900 Uxw 2736 improve transitions between main app data studio and admin (#68508)
* temp

* hook up mode switcher in other apps

* Ci issues

* adding get help, fixing e2e tests, other adjustments

* most unit tests

* rename to profile link?

* adjusting unit tests with routing

* partial feedback

* rename profile link

* base current app off routes array

* adjustments

* add non-admin user state, fix unit tests

* PR feedback

* fixing test

* fixing e2e

* only 1 app switcher in admin navbar

* e2e adjustments
2026-01-29 16:08:26 -04:00
Nathan Voxland
d1e940e66b Don't cascade delete transform runs on transform delete (#68642) 2026-01-29 13:16:28 -06:00
Jeff Bruemmer
781282dd40 docs - embedding edits (#68567)
* embedding edits

* edits

* Apply suggestion from @jeff-bruemmer

* edits

* Apply suggestion from @jeff-bruemmer

* update

* sso

* edits
2026-01-29 18:55:43 +00:00
bryan
730265b198 fix: re-run CI when break-quarantine-* or ci:all-cloud-drivers labels are changed (#68037) 2026-01-29 18:29:15 +00:00
Brad Anderson
0897ae0847 Fix Dependency Graph icon (#68855) 2026-01-29 12:57:07 -05:00
Timofey Kachalov
7b921d8f43 Deprecate isEEBuild helper (#68848)
Co-authored-by: Nicolò Pretto <info@npretto.com>
2026-01-29 21:34:11 +04:00
Stas Gavrylov
e22e0e5fb1 GDGT-1581: Add Python preview disclaimer (#68789)
* GDGT-1581: Update layout for Python preview message

* GDGT-1581: Add e2e test

* GDGT-1581: Rename 'Results' tab to 'Results preview'

* GDGT-1581: Adjust footer layout

* GDGT-1581: Add dismiss button to allow closing the footer

* GDGT-1581: Replace CSS with inline props

* GDGT-1581: Fix e2e test
2026-01-29 18:46:36 +02:00
Brad Anderson
c9bcbbfd73 UXW-2862 Fix test flake (#68784) 2026-01-29 11:41:36 -05:00
Brad Anderson
172e56f4b7 Fix data studio guard (#68781) 2026-01-29 11:16:02 -05:00
Eric Normand
1e3406219c Improved message for job failure due to routing (#68804) 2026-01-29 09:27:22 -06:00
Eric Normand
ad8376f6cc Don't double-convert expressions with convertTimezone() (#68740)
In the case statement, the default case did not TIMEZONE() to alter the timezone. The cases with TIMEZONE() returned a timestamp. The default case without TIMEZONE() returned timestamptz. Postgres reconciled the different types by converting all of them to timestamptz. So the timestamp was altered (by TIMEZONE()), but a new UTC timestamp was added (by the cast).

When the post-processor formats the rows, it checks that the metadata for the column has :converted_timezone. If it has that, it is formatted in the converted timezone, and since it's UTC, it will convert it again, a second time, which is incorrect.

The fix here is to check if the returned column is a timestamptz and strip it before formatting. We assume the database has already done the conversion. This avoids the second conversion.
2026-01-29 09:04:30 -06:00
John Swanson
bf81141508 allow getting transforms in root collection (#68731)
allow getting transforms in root collection
2026-01-29 15:00:57 +00:00
Noah Moss
05c2aff020 Split agent api out as a separate friend module to metabot-v3 (#68814)
* Split agent api out as a separate module

* remove old files

* cleanup

* fix module config
2026-01-29 09:57:08 -05:00
Aleksandr Lesnenko
bd3f1d4632 Transforms permissions (#66296)
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Brad Anderson <brad@metabase.com>
2026-01-29 09:29:01 -05:00
Sébastien
9743b3cbee Added link to doc for full app embedding (#68773)
* added link to doc for full app embedding

* Apply suggestions from code review

Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>

---------

Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
2026-01-29 15:01:05 +01:00
github-automation-metabase
50c809f3fa Auto-update documentation for master (#68820)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-01-29 08:27:11 -05:00
Timofey Kachalov
0559e0beee Hide Guest embedding status bar when guest embedding is disabled (#68836) 2026-01-29 16:06:34 +04:00
Dan Stone
cf347ce318 Throw db-routing error on anonymous transform call (#68788)
Fixes GDGT-1588
2026-01-29 11:29:25 +00:00
Stas Gavrylov
eea96b2477 GDGT-1356: Alert Notification Cron Syntax is misleading (#68834)
* GDGT-1356: Adjust day of week parsing in cron string

* GDGT-1356: Add unit test
2026-01-29 13:14:35 +02:00
da
390bcd144e Change log level from info to debug for ignored params on qp (#68748)
reducing a bit more the verbosity of our logs, this time on cards with old metadata
2026-01-29 05:47:36 -03:00
Jakub Chodorowicz
383f6ba968 gdgt-1377 deselect and hide from bulk view tables not matching selection (#68469) 2026-01-29 09:26:54 +01:00
Aleksandr Lesnenko
450f5cc9ba data studio upsell (#68527)
* data studio upsell

* refactor: fix route guards

* fix: lint comments

* fix: data studio layout tests

* fix: remove local stub for api

* fix: moar lint errors

* fix: comments

---------

Co-authored-by: Gabriel Moreno <gabriel@metabase.com>
2026-01-28 19:57:24 -04:00
metamben
353260e583 Keep field-values for detail-only fields (#67540)
* Keep field-values for detail-only fields

Closes #10851

* Make sure old-preview-display is set

* Fix test
2026-01-29 02:49:33 +03:00
Maksym Yakubych
b300a6118f fix: search in mini data picker only shows loading on the first load (#68365) 2026-01-28 17:53:29 -05:00
Maz Ameli
86410d0667 Boat instead of bird (#67612)
* boat instead of bird

* update tests
2026-01-28 22:41:26 +00:00
Nemanja Glumac
5d84cc6c3f Add a note about the Metabase config (#68803) 2026-01-28 22:51:19 +01:00
Edward Paget
ef4136d69e fix: clean up style issues in new remote-sync code (#68753)
* fix: clean up style issues in new remote-sync code

Fixes a grab bag of small style/docstring issues I found after the
parent PRs were merged.

* fixup! lint

* fixup! keep the newsnippetpage as always dirty
2026-01-28 15:42:02 -06:00
John Swanson
16e013a90d flake: fix temporal pollution (#68745)
flake: frozen clock
2026-01-28 21:31:04 +00:00
bryan
595e171f34 ci: add H2 OSS driver tests to catch OSS-only failures (#68649) 2026-01-28 21:25:05 +00:00
John Swanson
f01589a9d6 Fix migration for data studio (#68585)
* Fix migration for data studio

We usually auto-populate some other fields in permissions based on the value of object - perm_value, perm_type, and collection_id.

This obviously doesn't happen in a migration! So set them manually here.

* just roll forward
2026-01-28 13:15:30 -08:00
John Swanson
bf6c8b3459 Fix throw-if-last-admin! (#68198)
[UXW-341: Reactive old admin account is not possible](https://linear.app/metabase/issue/UXW-341/reactive-old-admin-account-is-not-possible)

Co-authored-by: Ed Paget <edpaget@users.noreply.github.com>
2026-01-28 13:14:31 -08:00
metamben
65189a0b69 Push the analysis of the dependents to the job (#68749)
* Push the analysis of the dependents to the job

Fixes QUE-3001

The analysis findings of dependents are marked as stale (if they exists) and the analysis job picks them up with
higher priority. Also, the job is triggered right after the analysis of the dependency and doesn't stop until all
stale findings are refreshed.

* Fix boolean type in migration

* Set *warn-on-reflection* to true

* Stick to SQL change

* Fix tests

* Remove race condition

* Process review feedback

* fixes

* fix tests

---------

Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
2026-01-28 20:52:58 +00:00
John Swanson
d950e70482 No special Synced Collection on remote sync setup (#68499)
* No special Synced Collection on remote sync setup

[UXW-2684: Remove creation of special "Synced Collection" that gets created when Remote Sync is setup](https://linear.app/metabase/issue/UXW-2684/remove-creation-of-special-synced-collection-that-gets-created-when)

Co-authored-by: edpaget<ed@metabase.com>

* fix e2e tests
2026-01-28 12:40:50 -08:00
John Swanson
54e150e856 flake: wait for card query after revert (#68796)
The revert operation triggers a chain: revert API -> reload card page.

Test was only waiting for the revert API, then sometimes it would manage
to click the History tab *before* the page reload. Then the page reloads
and we're not on the History tab anymore.

Waiting for the card query request to finish means that we've reloaded
the page and ready to roll.
2026-01-28 11:54:05 -08:00
Riley Thompson
5959ca6c82 ClickHouse to always support schemas (#68517)
* set schemas to true and add migration

* just enable schemas

* remove migration

* add test

* fix schema test
2026-01-28 14:03:56 -05:00
Noah Moss
6b330140ff Semantic layer agent API endpoints (#68260)
* New endpoint implementations

* add new schemas for new endpoints

* new field values endpoints

* simple search endpoint + bastic test

* WIP - query construction endpoint

* Query construction tests, convert :output errors to real 404

* Basic query execution endpoint that just wraps qp/process-query

* Add query response schema

* Add descriptions to malli specs

* Add more tests, simplify some schemas

* Misc fixes, malli description improvements, try to fix decoding/encoding stuff but will need to revisit

* fix whitespace

* Greatly simplify schema sharing w/existing tool endpoints

* cleanup ns

* Fix some tests

* Fix up metric field values test

* add agent-client test helper, and new agent-api-user-binding-test

* simplify base64 query encoding

* small cleanups

* Address feedback

* add comment

* bump ci

* bump ci

* fix modules

* switch to agent-api feature flag
2026-01-28 14:00:28 -05:00
Nathan Voxland
24b7d886aa Paginate caching dashboard (#68242) 2026-01-28 12:52:39 -06:00
kevinkelleher12
b27e93965d Make sunburst charts reflect colors set in the appearance tab (#68641) 2026-01-28 14:12:28 -04:00
Cam Saul
f563a91095 Add additional tables to Lib test metadata (#68754)
* Add additional tables to Lib test metadata

* Cljfmt

* Test fixes 🔧

* Tell generators how to generate booleans
2026-01-28 09:02:39 -08:00
Alexander Polyankin
b0cfcca1e5 Dependency diagnostics UI polish (#68761) 2026-01-28 16:52:26 +00:00
Maksym Yakubych
0421a12c9c fix: documents ignore base path when linking to dashboards (#68633) 2026-01-28 16:31:27 +00:00
Noah Moss
cb42387606 Ensure we catch semantic search errors with failover and prometheus metrics (#68639)
* Ensure we catch semantic search errors before RRF + add a new prometheus metric

* Revert "Ensure we catch semantic search errors before RRF + add a new prometheus metric"

This reverts commit d972fdf6df.

* Move fallback logic to semantic-search.core to not be metabot-specific, add test
2026-01-28 10:23:42 -05:00
Nick Fitzpatrick
731f564a04 Uxw 2696 handle charts where manual y axis hides all data (#68156)
* off grid detection

* woof

* fixing bad counts

* adjusting no data points check

* expose initalUiState in visualizer

* cleaning up dashcard functions

* adjust broken behavior for existing tests

* e2e tests

* PR feedback

* remove buttons to open settings and auto adjust. Adding tests for pinned cards and documents

* removing unneeded code, pr feedback, and adding loki screenshots

* fix silly thing
2026-01-28 15:13:59 +00:00
Timofey Kachalov
d41abaea80 Fix flaky content-translations.cy.spec.ts e2e (#68776) 2026-01-28 16:52:12 +04:00
Sébastien
13af38d9ca Fixed date range parameter widget can overflow in embed flows (#68724)
* fixed date param ovrflowing in embed

* added loki test
2026-01-28 11:59:31 +01:00
Romeo Van Snick
7d107764fe Fix flake in transform repro (#68759)
* Fix flake

* fix typo
2026-01-28 21:34:05 +11:00
Anderson
b871ae4909 Make segments read-only for published tables also on admin (#68651)
* Make segments read-only for published tables also on admin

* improve test coverage

* group tests
2026-01-28 03:25:53 +00:00
github-automation-metabase
114541e4fc Auto-update documentation for master (#68763)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-01-27 21:35:42 -05:00
bryan
0982712225 Enable fake-sync for Snowflake driver tests (#68322) 2026-01-27 15:47:40 -07:00
Edward Paget
965ce2a47b feat: add remote-sync support for PythonLibrary (#68515)
Add support for syncing PythonLibrary entities (Python transform libraries)
with a Git repository when remote-sync-transforms setting is enabled.

Implements serdes serialization for pythonlibraries using `path` as the
entity id.

Adds FE changes to refresh on dirty state and show changes in the AllChangesView
2026-01-27 16:09:31 -06:00
Noah Moss
7316888913 Semantic layer agent API groundwork + auth (#68163)
* Add support for versioned endpoints to deftool

* Create new API namespace + add custom jwt middleware

* add kondo module dependency

* revert the deftool stuff which isn't needed now

* fix lint issues

* fix jwt case insensitivity issue; more structured errors for auth issues

* small jwt cleanups

* Add support for session token exchange

* cleanup, lint, tests

* more test cases, cleanup

* bump ci

* Restructure session logic to use normal middleware, add test for session expiration

* fix up module config

* share more code with jwt sso module + remove some now-redundent tests

* try to fix tests by using existing SSO JWT test setup

* fix whitespace

* fix up module config
2026-01-27 16:49:55 -05:00
John Swanson
fe290f97e7 Fix collisions in test log file uploads (#68739)
1. if the `log-artifact-suffix` is empty, just do
`test-logs-{github-job}`.

2. include a `log-artifact-suffix` argument for the driver tests

This way we don't get 409 conflict errors when uploading logs on test
failures for driver tests.
2026-01-27 12:22:57 -08:00
Gabriel Moreno
794819ead2 Data Studio Owners, Alignment & Dependency diagnostics icon (#68707)
* refactor: remove avatars and "no owner" fallback from transforms list

* refactor: right-align rows column on data structure

* fix: search_check icon fill color
2026-01-27 16:20:55 -04:00
Riley Thompson
2ef8d50387 Fix redshift sync failing with duplicate key error (#68097)
* select distinct

* filter out duplicates

* skip duplicate cols entirely

* clean up test

* address comments
2026-01-27 19:34:40 +00:00
William
73766cf355 implicit joins get implicit fields added (#68394)
Say you are doing an implicit join from orders to products in order to summarize by products.category. Previously, with implicit joins, we join with the actual products table instead of a select blah from products wrapper, and so the various fields returned by the join were returned under their original name. However, the code would abbreviate excessively long column names from the joined table and break the query.

That difference was due to add-implicit-fields running before add-implicit-joins. Explicit joins got implicit fields added, while implicit joins didn't get those fields. This pr runs add-implicit-fields a second time after add-implicit-joins, which makes implicit and explicit joins compile to the same (correct) sql.
2026-01-27 13:01:52 -06:00
John Swanson
38f42a9386 Don't automatically pull when in read-write mode (#68500)
Don't automatically pull when in read-write mode

Co-authored-by: edpaget<ed@metabase.com>
2026-01-27 10:43:18 -08:00
Oleksandr Yakushev
0ec522227b [match-lite] Make match-lite recursive by default (#68658)
Make match-lite recursive by default
2026-01-27 20:27:01 +02:00
John Swanson
cb42b96ca8 flake: empty schemas in target schemas (#68742)
The test was missing an explicit database selection step which sometimes
resulted in the Save button being disabled.

Also removed the unnecessary sync_schema call — the syncable_schemas
endpoint queries Postgres directly via the driver, so no metadata
sync is needed.
2026-01-27 09:58:51 -08:00
Oleksandr Yakushev
52f48e2400 Refactor match-lite and introduce :or clause (#67345)
* Refactor match-lite

* Add support for :or syntax
2026-01-27 19:23:18 +02:00
John Swanson
36a5234efc Do not delete built-in transform tags on sync (#68727)
* Do not delete built-in transform tags on sync

I found that some tests were failing intermittently - sometimes
TransformTags were present, and sometimes they were missing. I tracked
it down to this test, which enables and then disables remote syncing
transforms.

The issue is that if we sync transforms (... and transform tags), then
unsync them, then we delete everything that was synced - including the
built-in tags.

We should exclude these from deletion.

* Fix serialization API tests
2026-01-27 08:29:58 -08:00
dpsutton
065c23b648 prometheus metrics for jvm startup (#68728)
logs:
```
2026-01-27 15:02:23,442 INFO util.queue :: Starting listener field-value-invalidate with 1 threads 🎧
2026-01-27 15:02:23,448 INFO util.queue :: Starting listener search-index-update with 1 threads 🎧
2026-01-27 15:02:23,448 INFO core.core :: Metabase Initialization COMPLETE
2026-01-27 15:02:23,453 INFO core.core :: Metabase Initialization COMPLETE in 4.8 s (JVM uptime: 57.8 s)
```

metrics:

```
❯ http get :9191/metrics | grep startup
metabase_startup_jvm_to_complete_millis 57764.0
metabase_startup_init_duration_millis 4816.0
```

Note that there two different notions of startup time: from jvm
instantiation, and from before init! was called. This maintains the two
separately so we can diagnose if class load time gets longer
independently of amount of work we do during startup itself
2026-01-27 10:03:20 -06:00
Edward Paget
b8ed3bd4d0 feat: support remote sync for NativeQuerySnippets (#68497)
* feat: support remote sync for NativeQuerySnippets

When the Library collection is remote-synced, all NativeQuerySnippets
and their collections (snippets namespace) are automatically tracked
for remote sync. No separate setting is needed - syncing is tied
directly to the Library collection's is_remote_synced status.

Update the FE to use the declarative approach to refresh dirty state
when snippets are modified and update the library dirty state indicator
to turn on when any snippet or snippet collection is modified.

* fix: deletion of snippets/transforms collections

Fixes a bug where collections in the special namespaces were not cleaned
up after changing branches.

* fixup! don't double create entries

* fixup! transaction scope for test on mysql
2026-01-27 08:38:10 -06:00
Rodrigo López Dato
5486894ab8 Allow [::1] and 127.0.0.1 origins by default for cross-origin requests + support IPv6 origins (#68697)
Allow [::1] origin by default for cross-origin requests
2026-01-27 10:46:56 -03:00
Sébastien
8bafab3e0f Skipping first sso request if possible in EAJS/Modular Embedding (#68679)
skipping first sso request if possible in eajs
2026-01-27 14:04:41 +01:00
Romeo Van Snick
0b8c5cd5a3 Show empty schemas in transform target and table metadata (#68592)
* Show empty schemas when picking a transform target

* Use listSyncableDatabaseSchemas endpoint in data structure

* Handle empty schemas

* Add repro for metabase#68378

* Move transforms tests to new folder

* Split off transforms reproductions into separate specs
2026-01-27 08:15:42 +00:00
github-automation-metabase
9c95c11b7b Update translations 2026-01-27 (#68720)
* Update translation sources 2026-01-27

* Update translations

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-01-27 06:21:24 +00:00
github-automation-metabase
3a8eae8a39 Auto-update documentation for master (#68713)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-01-26 23:38:22 -05:00
Ngoc Khuat
b98eb79b38 Flip mariadb during table editing support check (#68171) 2026-01-27 03:27:25 +00:00
Fraser
7d6d5b35a3 Use named exports in common components code (#67779) 2026-01-27 13:34:04 +13:00
Fraser
bc82a001ee Use named exports in visualizations folder (#67058) 2026-01-27 13:26:47 +13:00
kevinkelleher12
0b7737980d Reduce bundle size by removing import of enormous character sizes constant (#68700) 2026-01-26 14:50:03 -07:00
Nathan Voxland
f0a7607b2b Only validate map URLs that changed (#68030) 2026-01-26 14:18:59 -06:00
Nathan Voxland
09d1a5d630 Don't duplicate uploads in admin page (#68077) 2026-01-26 13:53:29 -06:00
dpsutton
c3898889fd try to prevent deadlocks in impersonation tests (#68690) 2026-01-26 13:39:25 -06:00
Edward Paget
233dbfecdc feat: toggle transform dirty status on setting change (#68684) 2026-01-26 12:47:26 -06:00
dpsutton
503108d247 Better logging around token checks (#68625)
* Better logging around token checks

We used to just log we were checking but not give error responses. So it
would look like a ton of token checks for no real reason.

Now includes error responses if any, and also include logs when engaging
the token check circuit breaker, and when it is "half open", when it
lets one real token check happen every 30 seconds to see if the service
is back.

```
2026-01-23 12:26:47,175 INFO premium-features.token-check :: Checking with the MetaStore to see whether token 'f325...d01a' is valid...
2026-01-23 12:26:47,186 INFO premium-features.token-check :: Reporting Metabase stats: {:embedding-dashboard-count 0, :enabled-embedding-sdk false, :internal-users 1, :embedding-question-count 0, :query_executions_static_embed 0, :query_executions_public_link 0, :domains 1, :metabot-usage-date 2026-01-22, :enabled-embedding-static false, :enabled-embedding-simple false, :metabot-queries 0, :external-users 0, :metabot-tokens 0, :query_executions_simple_embed 0, :enabled-embedding-interactive false, :query_executions_internal 0, :metabot-users 0, :query_executions_sdk_embed 0, :query_executions_interactive_embed 0, :users 1}
2026-01-23 12:26:49,190 INFO premium-features.token-check :: Error checking token: Connect timed out †
2026-01-23 12:26:49,194 INFO premium-features.token-check :: Checking with the MetaStore to see whether token 'f325...d01a' is valid...
2026-01-23 12:26:49,201 INFO premium-features.token-check :: Reporting Metabase stats: {:embedding-dashboard-count 0, :enabled-embedding-sdk false, :internal-users 1, :embedding-question-count 0, :query_executions_static_embed 0, :query_executions_public_link 0, :domains 1, :metabot-usage-date 2026-01-22, :enabled-embedding-static false, :enabled-embedding-simple false, :metabot-queries 0, :external-users 0, :metabot-tokens 0, :query_executions_simple_embed 0, :enabled-embedding-interactive false, :query_executions_internal 0, :metabot-users 0, :query_executions_sdk_embed 0, :query_executions_interactive_embed 0, :users 1}
2026-01-23 12:26:51,205 INFO premium-features.token-check :: Engaging circuit breaker in token check ††
2026-01-23 12:26:51,206 INFO premium-features.token-check :: Error checking token: Connect timed out
2026-01-23 12:27:44,548 INFO premium-features.token-check :: In token check circuit breaker but attempting check
2026-01-23 12:27:44,551 INFO premium-features.token-check :: Checking with the MetaStore to see whether token 'f325...d01a' is valid...
2026-01-23 12:27:44,565 INFO premium-features.token-check :: Reporting Metabase stats: {:embedding-dashboard-count 0, :enabled-embedding-sdk false, :internal-users 1, :embedding-question-count 0, :query_executions_static_embed 0, :query_executions_public_link 0, :domains 1, :metabot-usage-date 2026-01-22, :enabled-embedding-static false, :enabled-embedding-simple false, :metabot-queries 0, :external-users 0, :metabot-tokens 0, :query_executions_simple_embed 0, :enabled-embedding-interactive false, :query_executions_internal 0, :metabot-users 0, :query_executions_sdk_embed 0, :query_executions_interactive_embed 0, :users 1}
2026-01-23 12:27:46,574 INFO premium-features.token-check :: Engaging circuit breaker in token check
2026-01-23 12:27:46,575 INFO premium-features.token-check :: Error checking token: Connect timed out

...
2026-01-23 12:32:50,432 INFO premium-features.token-check :: In token check circuit breaker but attempting check †††
2026-01-23 12:32:50,433 INFO premium-features.token-check :: Checking with the MetaStore to see whether token 'f325...d01a' is valid...
2026-01-23 12:32:50,444 INFO premium-features.token-check :: Reporting Metabase stats: {:embedding-dashboard-count 0, :enabled-embedding-sdk false, :internal-users 1, :embedding-question-count 0, :query_executions_static_embed 0, :query_executions_public_link 0, :domains 1, :metabot-usage-date 2026-01-22, :enabled-embedding-static false, :enabled-embedding-simple false, :metabot-queries 0, :external-users 0, :metabot-tokens 0, :query_executions_simple_embed 0, :enabled-embedding-interactive false, :query_executions_internal 0, :metabot-users 0, :query_executions_sdk_embed 0, :query_executions_interactive_embed 0, :users 1}
2026-01-23 12:32:52,450 INFO premium-features.token-check :: Engaging circuit breaker in token check
2026-01-23 12:32:52,450 INFO premium-features.token-check :: Error checking token: Connect timed out
```

The highlights are now we include:
- † `Error checkign token: Connect timed out` the error response when
appropriate
- †† `Engaging circuit breaker in token check` when we engage the
circuit breaker to no longer do real token checks
- ††† `In token check circuit breaker but attempting check` indicating
we are doing a real one from the circuit breaker to determine if the
service is restored

* Whew. Lots of stuff

add a namespace to the circuit breaker keyword:
`:token-check/circuit-breaker`. Change the keyword holding
`:token-check/app-db-not-ready` to cause. I had used reason but nothing
checked that.

this is because want to log when a token check fails, but not when it is
- failure due to circuit breaker
- failure due to appdb not ready yet.

token checks can happen anywhere and with an enormous frequency. they
fail before the appdb is setup (we can't check settigns yet) and of
course they fail during the circuit breaker opening. that's the point of
the circuit breaker. So we don't have to log these.

We _do_ want to log other failures.

We also want to do some prometheus metrics. Here's a journey:
You previously couldn't import the prometheus analytics namespace
because analytics.settings required appearance.settings. It does so
because a docstring in analytics settings used to include Metabase in
the string. I removed that so now it says "in order to help us
improve". And then things would be smooth sailing.

Except we had an issue.

```
026-01-23 20:50:03,514 INFO analytics.prometheus :: Starting prometheus metrics collector
2026-01-23 20:50:03,539 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
thread229238 we made the system!
thread229238 and now we are done with the lock!
2026-01-23 20:50:03,607 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,611 ERROR premium-features.token-check :: Error validating token: Failed to initialize Prometheus on port 9,191
2026-01-23 20:50:03,611 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,612 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,614 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,616 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,617 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,618 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,619 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,619 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
2026-01-23 20:50:03,653 INFO core.core :: Setting up prometheus metrics
```

We have locking around the system but it is starting tons of times! Lots
of debugging, swearing that this can't happen. but then i finally threw
in my favorite trick: `(clojure.repl/pst (Exception. "how did we get
here"))`. And it turns out we are doing this:

1. token check fails, so we call `prometheus/inc!`
2. inc is helpful, sees system isn't yet setup, and sets it up for us
3. Setup metric system
4. setup metrics in system
5. we hit the following code which gets initial values for labels

```clojure
(defn- initial-labelled-metric-values []
  (for [metric (keys (methods known-labels))
        labels (known-labels metric)]
    {:metric metric
     :labels labels
     :value  (initial-value metric labels)}))
```
6. We hit the following search code about which search we have

```clojure
(defmethod analytics/initial-value :metabase-search/engine-active
  [_ {:keys [engine]}]
  (if (search.engine/supported-engine? (keyword "search.engine" engine))
  1 0))
```

7. This takes us to semantic search:

```clojure
(defmethod search.engine/supported-engine? :search.engine/semantic [_]
  (try
    (supported?)
    (catch Exception e
      (log/warn e "Semantic search engine not supported")
      false)))
```

8. `supported?` here checks to see if you support a particular search
impelementation, which of course depends on _YOUR FEATURES_.
9. Checking features fails, so we should increment our prometheus
counter
10. the system is not yet initialized yet. Let's initialize it. (this is
also step 2)

So I have added a simple `inc-if-initialized!` function. It's not
important if the system is down. It's ok if these events are lost. The
system will be initialized in core anyways.

```
❯ http get :9191/metrics | grep metabase_token
 HELP metabase_token_check_attempt_total Total number of token checks. Includes a status label.
 TYPE metabase_token_check_attempt_total counter
metabase_token_check_attempt_total{status="failure",} 8.0
```

logs:

```
2026-01-23 22:58:26,668 INFO premium-features.token-check :: Checking with the MetaStore to see whether token '82bf...c3fe' is valid...
2026-01-23 22:58:28,706 INFO premium-features.token-check :: Error checking token: Connect timed out
2026-01-23 22:58:28,750 INFO core.core :: Setting up prometheus metrics
2026-01-23 22:58:28,751 INFO analytics.prometheus :: Starting prometheus metrics collector
2026-01-23 22:58:28,789 INFO premium-features.token-check :: Checking with the MetaStore to see whether token '82bf...c3fe' is valid...
2026-01-23 22:58:30,793 INFO premium-features.token-check :: Error checking token: Connect timed out
2026-01-23 22:58:30,798 INFO analytics.prometheus :: Starting prometheus metrics web-server on port 9191
...

2026-01-23 22:58:53,441 INFO premium-features.token-check :: Checking with the MetaStore to see whether token '82bf...c3fe' is valid... {mb-quartz-job-type=SyncAndAnalyzeDatabase}
...
2026-01-23 22:58:55,445 INFO premium-features.token-check :: Engaging circuit breaker in token check {mb-quartz-job-type=SyncAndAnalyzeDatabase}
2026-01-23 22:58:55,445 INFO premium-features.token-check :: Error checking token: Connect timed out {mb-quartz-job-type=SyncAndAnalyzeDatabase}
```

```
Exception how here??
	metabase.analytics.prometheus/setup! (prometheus.clj:584)
	metabase.analytics.prometheus/setup! (prometheus.clj:576)
	metabase.analytics.prometheus/inc! (prometheus.clj:668)
	metabase.analytics.prometheus/inc! (prometheus.clj:658)
	metabase.analytics.prometheus/inc! (prometheus.clj:665)
	metabase.analytics.prometheus/inc! (prometheus.clj:658)
	metabase.premium-features.token-check/error-catching-token-checker/reify--98262 (token_check.clj:431)
	metabase.premium-features.token-check/check-token (token_check.clj:488)
	metabase.premium-features.token-check/check-token (token_check.clj:484)
	metabase.premium-features.token-check/check-token (token_check.clj:486)
	metabase.premium-features.token-check/check-token (token_check.clj:484)
	metabase.premium-features.token-check/eval98325/*token-features*98348--98349 (token_check.clj:534)
	metabase.premium-features.token-check/eval98325/fn--98353 (token_check.clj:529)
	metabase.premium-features.token-check/has-feature? (token_check.clj:575)
	metabase.premium-features.token-check/has-feature? (token_check.clj:569)
	clojure.lang.Var.invoke (Var.java:386)
	metabase.premium-features.defenterprise/check-feature (defenterprise.clj:41)
	metabase.premium-features.defenterprise/check-feature (defenterprise.clj:35)
	metabase.premium-features.defenterprise/dynamic-ee-oss-fn/fn--71956 (defenterprise.clj:53)
	metabase.search.semantic.core/eval180600/fn--180601 (core.clj:58)
	clojure.lang.MultiFn.invoke (MultiFn.java:229)
	metabase.search.core/eval139302/fn--139304 (core.clj:77)
	clojure.lang.MultiFn.invoke (MultiFn.java:234)
	metabase.analytics.prometheus/initial-labelled-metric-values/iter--97802--97808/fn--97809/iter--97804--97810/fn--97811 (prometheus.clj:521)
	clojure.lang.LazySeq.force (LazySeq.java:50)
	clojure.lang.LazySeq.realize (LazySeq.java:89)
	clojure.lang.LazySeq.seq (LazySeq.java:106)
	clojure.lang.RT.seq (RT.java:555)
	clojure.core/seq--5488 (core.clj:139)
	clojure.core/concat/fn--5579 (core.clj:727)
	clojure.lang.LazySeq.force (LazySeq.java:50)
	clojure.lang.LazySeq.realize (LazySeq.java:89)
	clojure.lang.LazySeq.seq (LazySeq.java:106)
	clojure.lang.Cons.next (Cons.java:41)
	clojure.lang.RT.next (RT.java:733)
	clojure.core/next--5472 (core.clj:64)
	clojure.core/next--5472 (core.clj:64)
	metabase.analytics.prometheus/setup-metrics! (prometheus.clj:546)
	metabase.analytics.prometheus/setup-metrics! (prometheus.clj:533)
	metabase.analytics.prometheus/make-prometheus-system (prometheus.clj:60)
	metabase.analytics.prometheus/make-prometheus-system (prometheus.clj:55)
	metabase.analytics.prometheus/setup!/fn--97866 (prometheus.clj:591)
	metabase.analytics.prometheus/setup! (prometheus.clj:590)
	metabase.analytics.prometheus/setup! (prometheus.clj:576)
	metabase.analytics.prometheus/inc! (prometheus.clj:668)
	metabase.analytics.prometheus/inc! (prometheus.clj:658)
	metabase.analytics.prometheus/inc! (prometheus.clj:665)
	metabase.analytics.prometheus/inc! (prometheus.clj:658)
	metabase.premium-features.token-check/error-catching-token-checker/reify--98262 (token_check.clj:431)
	metabase.premium-features.token-check/check-token (token_check.clj:488)
```

* module config fixup
2026-01-26 12:26:48 -06:00
bryan
a45d9203f1 Fix flaky GeoJS-dependent tests by mocking external service (#68623) 2026-01-26 10:34:20 -07:00
William
163180e8d9 Delete from cache in batches of 1000 (#68622)
There's apparently an instance on cloud that is trying to remove 70k queries from the cache at once, and postgres
doesn't allow 70k params to a query.  Batching the deletes in groups of 1000 should fix that.
2026-01-26 11:31:02 -06:00
Timofey Kachalov
b81aa809ad feat(sdk,embed-js): Add content translations for Collection/MetabaseBrowser components (#68608) 2026-01-26 21:07:55 +04:00
Jakub Chodorowicz
78efb4247b Gdgt 1557 migrate eslint from deprecated rulesdir to custom plugin (#68570) 2026-01-26 17:27:36 +01:00
Copilot
e51a02cd56 Refactor .styled files to CSS modules with CSS variables (#66851)
* Refactor batch 1: Convert simple styled files to CSS modules (10 files)

Co-authored-by: iethree <30528226+iethree@users.noreply.github.com>

* Refactor batch 2: Convert more simple styled files to CSS modules (8 files)

Co-authored-by: iethree <30528226+iethree@users.noreply.github.com>

* Refactor batch 3: Convert files with color() functions to CSS modules (5 files)

Co-authored-by: iethree <30528226+iethree@users.noreply.github.com>

* misc fixed

* lint fix

* Revert breakpoint changes

* prettier

* color fixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: iethree <30528226+iethree@users.noreply.github.com>
Co-authored-by: Ryan Laurie <iethree@gmail.com>
2026-01-26 08:14:38 -07:00
John Swanson
f8d4a239b3 populate here and below for transforms (#68614)
[UXW-2828: collection/:id/items API does not return transforms](https://linear.app/metabase/issue/UXW-2828/collectioniditems-api-does-not-return-transforms)
2026-01-26 05:30:47 -08:00
Sébastien
ae05f0bab7 Fixed missing bg color for dashboards (#68673)
fixed missing bg color for dashboards
2026-01-26 13:20:41 +01:00
Fraser
e2132ed193 use named exports in admin folder (#67771) 2026-01-26 23:12:47 +13:00
Fraser
50e9d524d6 Use named exports in query_builder code (#67780) 2026-01-26 23:12:34 +13:00
Fraser
fbd16c232b Use Metabase Bot GitHub App to create PR (#68664) 2026-01-26 23:12:17 +13:00
Romeo Van Snick
123f2dafd2 MBQL lib helpers for e2e tests (#68278)
* Rename file containing jsx to .jsx

* Add e2e helpers to build metadata provider

* Split up metadata helpers

* WIP

* Add more direct createTestQuestion helper

* Replace test

* Build cljs for tests

* Export type

* Only require entities in the state for getMetadata

* Just pass state to getMetadata

* Move EntitiesSchema to metabase/schema

* Wrap mbql e2e helpers in promise

* Split off getMetadataWithoutSettings

* Use getMetadataWithoutSettings to get metadata from state

* Build entities state manually

* Simplify wrapper

* Fix metadataProvider

* Add createTestJsQuery wrapper for cypress

* Use plain promises and fetch

* Allow passing GetMetadataOpts to createTest(Native)JsQuery directly

* Improve createTestQuestion types

* Make use of createTestJsQuery in test

* Reduce nest in createTestQuestion

* Use simplified createTestQuestion

* Split up mbql helpers

* Fix card metadata call

* Use correct card endpoint

* Pass missing options

* Convert another more complex test to createTestQuestion

* Fix types

* Use static ids for defaults to avoid import loop

* Fetch more tables by default

* Build cljs in embedding tests

* Build cljs in component tests

* Prepare backend env

* Add plugin so we can avoid building CLJS everywher

* Avoid building cljs where not needed

* Fix types

* Rename query helpers

* Split up helpers

* Compile cljs in stress tests
2026-01-26 19:54:16 +11:00
metabase-bot[bot]
16504fd3c8 [chore] Update E2E test timings with recent data (#68665)
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-01-26 21:50:07 +13:00
Romeo Van Snick
997b8d898e Respect abbreviate date style and be defensive around missing settings (#68661)
* Respect date_abbreviate when formatting date

* Respect abbreviate setting in month format too

* Add unit tests for abbreviated formats

* Add unit test for empty formatting settings

* Add repro for metabase#68574

* Add formatting settings unit tests
2026-01-26 15:50:42 +11:00
Alexander Polyankin
ceb518ab4b Dependency diagnostics - update the url, reset page on filter change, fix visibility checks (#68650) 2026-01-25 21:53:08 -05:00
Fraser
0ed6c4deed Link Claude Bugfix PR to Linear issue (#67710) 2026-01-26 10:44:24 +13:00
Sloan Sparger
6c09fca355 Fix NativeQueryEditor's action buttons (#68638)
fix position of buttons for native query editor
2026-01-24 20:34:41 +00:00
Aleksandr Lesnenko
049526c61c Cleanup (#68648)
cleanup
2026-01-24 09:40:18 -06:00
William
a799178420 Python transforms have dependencies (#68572)
The existing transform dependencies code only worked with query-based transforms (mbql or native).  This extends that
code to work with python transforms as well.
2026-01-24 02:34:24 -06:00
Maksym Yakubych
f7b82d5c54 fix: word wrap overflows row when reordering in dashboard table card (#68566) 2026-01-24 00:37:00 -03:00
Anderson
a51d1179d3 Disallow editing transforms when remote sync is read-only (#68380)
* Read-only: Adjust transform header menu options / Remove edit definition button / Run tab updates

* Make Settings tab read-only

* fix remote sync setup in e2e test

* Do not allow creating a new transform

* Make Python library not editable

* Hide edit definition in mbql transform view page

* Fix e2e

* Group e2e tests to improve performance
2026-01-24 01:52:27 +00:00
metamben
cbed2e45a8 Follow up on QUE-2999: move filtering to the database (#68635) 2026-01-23 23:45:05 +00:00
Timothy S. Dean
418d7a0317 Tweak data loading behavior for a test after MySQL 9.6.0 release (#68634)
Closes DEV-1289

### Description

Within the last day or two, `metabase.query-processor-test.remapping-test/self-referencing-test` began failing on MySQL Latest. I hypothesize this has to do with the [latest release](https://dev.mysql.com/doc/relnotes/mysql/9.6/en/news-9-6-0.html) fixing an unknown bug around batch inserts in InnoDB and the deprecation of a recusive [sic] fk-related flag.

The error is thrown not in the test conditions, but in the test setup -- MySQL apparently now checks foreign key constraints row-per-row even on a batch insert.

This adds a flag that the test can set which changes the test data-loading behavior to disable the FK check before insertion and re-enable it after.
2026-01-23 15:56:20 -07:00
Gabriel Moreno
3bce633480 Library empty state: display content in the page body (#68628)
* feat: create library empty state

* refactor: use new designs

* refactor: fix library creation

* fix: paper shadow

* fix: e2e
2026-01-23 18:49:35 -04:00
Gabriel Moreno
48327532c5 More Data Studio UX improvements & QoL fixes (#68546)
* fix(UXW-2752): remove unwanted padding on data structure fields tab group

* fix(UXW-2707): remove double border on job transforms section

* feat(UXW-2718): add horizontal layout for schedule controls

* feat(UXW-2709): fix spacing in the segments empty state

* fix(UXW-2780): long text overflow in user input

* fix(UXW-2710): set proper placeholder text on empty python library first load

* feat(UXW-2767): rename 'New collection' to 'Transform folder'

* refactor: blow grid & use `Ellipsified` instead
2026-01-23 18:03:17 -04:00
Anderson
26f71cfeb5 Fix SQL editor scrolling on Transform pages (#68107)
* Fix SQL editor scrolling on Transform pages

* Fix e2e test for question editor

* Fix python transform scrolling

* Fix lint

* Fix view-only transform content height
2026-01-23 18:42:24 -03:00
Braden Shepherdson
3d4e44b776 [deps] After a sync, re-analyze all dependents of updated tables (#68621)
Closes QUE-2984.

Adds an event handler for when a DWH sync is completed, which checks all its tables and fields for
their latest update times, and re-analyzes all cards which depend on tables updated since the last analysis.
2026-01-23 21:01:41 +00:00
Brad Anderson
86f3e524ba Fix UXW-2759 - Snippet edits in data studio are not saved (#68565) 2026-01-23 15:29:42 -05:00
Nathan Voxland
056d61238e If a dashboard is back to just one tab, auto-assign all cards to that tab (#68201) 2026-01-23 14:22:12 -06:00
Maksym Yakubych
926610da14 fix: custom Logo isn't loaded properly on User Invite Email (#68476) 2026-01-23 11:44:43 -08:00
Alexander Polyankin
75251f933a Fix text wrapping in dependencies (#68615) 2026-01-23 19:28:48 +00:00
Nemanja Glumac
d959d54aec Remove cache cleanup CI action (#68521) 2026-01-23 20:25:23 +01:00
Edward Paget
0933b7378f feat: support remote-syncing measures (#68392)
* feat: support remote-syncing measures

Add remote-sync support for measures, following the same pattern as segments:

- Add :model/Measure spec to remote-sync-specs with hybrid identity,
  parent-table eligibility, and event tracking
- Update measure permissions to check remote-sync/table-editable? for
  can-write? and can-create? (replacing superuser-only derive)
- Add batched hydration for :can_write on measures
- Add measures to serdes/descendants for Table so they export with parent
- Add tests for measure import/export tracking in impl_test.clj

On the Frontend creates a new data-driven approach to setting up the
middleware listener for changes that cause us to refresh dirty-state.

* fixup! typecheck
2026-01-23 13:17:01 -06:00
Edward Paget
e3b200b319 fix: flakey ec keypair test (#68591)
* fix: flakey ec keypair test

* fixup! do crypto in a delay
2026-01-23 13:15:00 -06:00
Edward Paget
fb9c0e4499 fix: derive events remote-sync needs in oss code (#68590) 2026-01-23 13:14:51 -06:00
Nemanja Glumac
40522b6cba Remove unused lodash types (#68593) 2026-01-23 19:26:18 +01:00
metamben
b6edd15de2 Stop reporting errors on archived or inaccessible entities (#68613)
Closes QUE-2999
2026-01-23 12:52:32 -05:00
lbrdnk
16042f45c7 Use macaw options, add parsed-query wrapped (#68466)
* Hoist macaw impl from query analyzer

* Use macaw-opts in bigquery module

* Use macaw-options in sqlserver

* Use macaw-options in sql driver

* Remove require

* Comment

* Test

* Update test

* Update macaw version

* Best effort support for bigquery

* Make test driver specific

* Remove final-not-reserved

* Not allowed on clickhouse only

* Clickhouse exception test

* best effort opts on api

* Remove final-non-reserved

* Remove requires

* driver.u/parsed-query

* Use parsed-query

* parsed-query

* ch test

* throw

* Lint against incorrect Macaw usage (#68568)

* Remove now unused function

* Shutdown loud exceptions for now

* Shutdown ch test

* Update test

---------

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
2026-01-23 18:37:06 +01:00
Riley Thompson
b28ab0ec1d Revert ClickHouse from 0.9.5 to 0.8.4 (#68502)
* revert to 0.8.4

* disable test
2026-01-23 17:18:22 +00:00
github-actions[bot]
add8906504 Fix: Keyboard navigation annoyance in command palette (#67317)
Co-authored-by: Claude Code[bot] <noreply@anthropic.com>
Co-authored-by: Brad Anderson <brad@metabase.com>
2026-01-23 11:33:33 -05:00
dpsutton
3345365546 handle empty error message (#68581)
Fixes https://github.com/metabase/metabase/issues/68578

This issue is not about the stack overflow per-se but blowing up on a
string includes:

```javascript
{
  "app": "metabase",
  "time": "2026-01-22T12:08:53.660601485Z",
  "stream": "stderr",
  "app_version": "v1.57.10-X01",
  "message": "Exception in thread \"Thread-41\" java.lang.NullPointerException: Cannot invoke \"Object.toString()\" because \"s\" is null
at clojure.string$includes_QMARK_.invokeStatic(string.clj:377)
at clojure.string$includes_QMARK_.invoke(string.clj:373)
at metabase.app_db.cluster_lock$retryable_QMARK_.invokeStatic(cluster_lock.clj:27)
at metabase.app_db.cluster_lock$retryable_QMARK_.invoke(cluster_lock.clj:19)
at metabase.app_db.cluster_lock$do_with_cluster_lock.invokeStatic(cluster_lock.clj:87)
at metabase.app_db.cluster_lock$do_with_cluster_lock.invoke(cluster_lock.clj:65)
at metabase.app_db.cluster_lock$do_with_cluster_lock.invokeStatic(cluster_lock.clj:79)
at metabase.app_db.cluster_lock$do_with_cluster_lock.invoke(cluster_lock.clj:65)
at metabase.search.task.search_index$init_BANG_.invokeStatic(search_index.clj:36)
at metabase.search.task.search_index$init_BANG_.invoke(search_index.clj:32)
at clojure.lang.AFn.run(AFn.java:22)
at java.base/java.lang.Thread.run(Unknown Source)",
  "namespace": "hosting-eb9b3875e12d465a",
  "container": "metabase",
  "cluster": "metabase-1",
  "_p": "F"
}
```

The error message itself comes from a stackoverflow which perhaps does
not have a message? Either way, it's possible for this to be missing so
be a bit defensive
2026-01-23 10:04:56 -06:00
Sébastien
862dc50c64 Fix ChartTypeDropdown overflowing (#68455)
* Forcing overflow policy on ChartTypeDropdown

* tpyo

* refactoring to enable testing this with loki

* fixed story

* loki

* screenshot

* removed useless unit test
2026-01-23 16:08:37 +01:00
Nicola Mometto
0941091421 ensure common.py library is always available (#68603)
* fix: ensure common python library is always defined

* chore: e2e test

* fix: empty rollback

* fix: empty source

* fix: text
2026-01-23 15:01:18 +00:00
Alexander Polyankin
0bd69823a4 Show view_count for models and metrics in dependencies (#68588) 2026-01-23 17:42:57 +03:00
Alexander Polyankin
6a01533ca7 Use MBQL lib for matching field refs in ValueSourceModal in parameters (#68431) 2026-01-23 17:42:06 +03:00
Nemanja Glumac
7d18a1b5a6 Unset the premium token for E2E tests (#68533) 2026-01-23 13:54:21 +01:00
github-automation-metabase
0bf7e41984 Auto-update documentation for master (#68589)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-01-23 07:47:03 -05:00
Sébastien
95a25e4a3e fix proper bg color for embedded dashboards with custom theme (#68539) 2026-01-23 13:00:22 +01:00
Dan Stone
a2c4eef9de Revert the revert of column validation for incremental transforms (#68460)
Filter extract-columns endpoint to only return numeric and temporal columns
The extract-columns-from-query function is used to populate the incremental
filter dropdown for native SQL transforms. Previously it returned all columns
without filtering on type, leading to undefined behaviour when unsupported
column types were selected.

This change filters the results to only include columns with types supported
for incremental checkpointing:
- Temporal types (timestamp, timestamp with timezone)
- Numeric types (integer, float, decimal)

Text, boolean, and other unsupported types are now filtered out, matching
the behaviour of MBQL and Python transforms which already filter columns
client-side.

Updated documentation to clarify this endpoint is specifically for incremental
transform checkpoint column selection.

Also validates that transform POST/PUT conform to this rule and you are unable to update the transform to have
a filter column of an unsupported type.

Reverts the revert of https://github.com/metabase/metabase/pull/67686
2026-01-23 11:24:09 +00:00
Chris Truter
2f81937643 Fix data format for TODOs in style guides (#68598) 2026-01-23 13:13:30 +02:00
Sloan Sparger
48bf58b162 Cleanup from #68433 (#68481)
fix ai feedback that came after PR close :'/
2026-01-23 02:33:48 +00:00
Egor Grushin
761a7cf01e GDGT 1533 improve discoverability of python library in transforms UI (#68523) 2026-01-23 00:55:37 +00:00
Timothy S. Dean
8a76f71003 Remove find-column-for-name (#68382)
Remove find-column-for-name
2026-01-22 16:06:41 -07:00
Maksym Yakubych
fd90d13ae3 fix: custom brand color not consistently applied on Enterprise UI elements (#68484) 2026-01-22 22:56:00 +00:00
Maz Ameli
a62aab3f1b Rename Data Studio Library +New menu action labels (#68162)
* Rename Data Studio Library +New menu action labels

Update the menu item labels in the Data Studio Library's +New button:
- "Publish a table" → "Published table"
- "New snippet" → "Snippet"
- "New snippet folder" → "Snippet folder"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* prettier

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 22:39:54 +00:00
metamben
bafd49ee17 Add options to the /graph/dependents endpoint (#68573) 2026-01-22 21:58:19 +00:00
Edward Paget
1f0b461d6f feat: support remote sync transforms in FE (#68306)
* feat: support remote sync transforms in FE

1. Updates the DataStudioLayout to show transform dirty state

   We get the orange dot next to Transforms if transforms or transform
   collections are dirty.

2. Update the all changes view to include transforms

   Also includes their collection heirarchy if the transforms sync
   setting is enabled

3. Add the transforms sync setting

   To both the remote sync admin page and the Set up git sync modal in
   the data studio

* fixup! refresh transforms after sync

* fixup! data studio test feedback

* fixup! refactor failing test to troubleshoot debugging
2026-01-22 14:33:00 -06:00
metamben
1712274b85 Normalize transforms metadata (#68490) 2026-01-22 23:22:39 +03:00
Timofey Kachalov
ea203102e6 Don't show CORS settings in OSS/Starter editions (#68564) 2026-01-23 00:14:05 +04:00
Edward Paget
3c72591b17 feat: add Slack Connect SSO provider (#67955)
* feat: add Slack Connect SSO provider

Add enterprise Slack Connect SSO integration using the OIDC infrastructure:

- Slack Connect provider: OAuth2/OIDC flow for Slack workspace authentication
- Settings: configuration options for Slack client ID, secret, and auth modes
- User provisioning: automatic user creation from Slack identity
- API endpoints: SSO initiation and callback handling
- Analytics: usage tracking for Slack Connect feature

Slack Connect allows users to authenticate via their Slack workspace,
with support for both SSO-only and link-account authentication modes.

* fix: session reset after auth

* fixup! avoid email clash

* fixup! wrong value in sso settings

* fixup! cleanup tests

* fixup! fix test

---------

Co-authored-by: John Swanson <john.swanson@metabase.com>
2026-01-22 10:57:12 -08:00
github-automation-metabase
9a19a5e4f0 [chore] Update llms.txt for master (#68561)
Update llms.txt and llms-full.txt (master)

Regenerated llms.txt to include all markdown files from the docs/ directory.
Also generated llms-full.txt files for the embedding section.

🤖 Generated automatically by llms-txt-generate workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-01-22 18:20:04 +00:00
Sloan Sparger
f387d4aca4 [Metabot] Add close button back to data studio sidebar (#68487)
allow metabot to be closed in data studio
2026-01-22 11:33:21 -06:00
github-automation-metabase
1bb6f6a74a Update translations 2026-01-22 (#68557)
* Update translation sources 2026-01-22

* Update translations

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-01-22 17:19:01 +00:00
Nicolò Pretto
2ad4292465 fix(embed-homepage): rename static embed to guest embed (#68338) 2026-01-22 16:37:02 +00:00
github-automation-metabase
37c3758748 Auto-update documentation for master (#68524)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-01-22 09:26:48 -05:00
Jeff Bruemmer
ae9cad5c58 docs - note on ee vs oss path (#68210) 2026-01-22 09:09:05 -05:00
John Swanson
bd14e14974 Reenable deactivated tenants when autoprovisioning (#67947)
Previously even when autoprovisioning was on, you could not log into a
deactivated tenant (either as a new user or an existing user with that
tenant).

This changes the behavior to be more consistent. Just as a deactivated
user can log in when provisioning is on (and their user gets
reactivated), a deactivated tenant's users can still log in, but doing
so reactivates the tenant.
2026-01-22 05:10:23 -08:00
Jakub Chodorowicz
85319217cd Gdgt 1273 add snowplow events to bulk metadata features (#68409)
* add data studio library filtering events

* add bulk properties and unpublish event

* add bulk sync events

* add search event

* add assertions on events in e2e tests

* add search debounce

* remove unncessery comment

* update event names

* track filter searches with data_studio_table_picker_search_performed

* refactor tracking function

* guard and log event only for ConcreteTableId

* log search when started fetching

* fix events not fired

* fix event not fired on first search
2026-01-22 12:30:26 +01:00
Nicola Mometto
240acb4782 add started-at filter to task runs page (#68279)
* WIP: add started-at filter

* fix: kondo

* fix tests

* GDGT-1520 add a started at filter

* GDGT-1520 add a comment

* GDGT-1520 fix typing issues

* GDGT-1520 addressing PR comments

* GDGT-1520 addressing PR comments

* GDGT-1520 fix a tests after changing a text

---------

Co-authored-by: Egor Grushin <egor@metabase.com>
2026-01-22 09:49:15 +00:00
Jakub Chodorowicz
e6bc40c223 add analytics skill (#68419) 2026-01-22 09:36:42 +01:00
bryan
76fcb4d573 Fix transforms module to trigger driver tests + promote pg (#68432) 2026-01-21 16:30:15 -07:00
Arkadi Yaroslavtsev
a92317dd27 Add Anchor Links to Documents (#67940)
* Add AnchorLinkButton component for document anchor links

- Add AnchorLinkButton component that copies URL to clipboard on click
- Shows 'Copied!' tooltip feedback using Mantine useClipboard hook
- Follows same pattern as CommentsButton for consistent styling
- Add unit tests for the component

* Add AnchorLinkMenu component for left-side anchor link hover

- Add AnchorLinkMenu floating menu for document blocks
- Uses createPortal for positioning (same pattern as CommentsMenu)
- Positioned on left side of blocks using floating-ui
- Shows/hides based on hover state via show prop
- Add unit tests for the component

* Add AnchorLinkMenu to document node views

- Add documentWithAnchor URL helper to generate shareable anchor URLs
- Update Heading, Paragraph, and Blockquote node views to render AnchorLinkMenu
- AnchorLinkMenu appears on the left side on hover
- CommentsMenu on the right side remains unchanged
- Each node view now uses two useFloating instances for positioning

* Add scroll-to-anchor for document URLs with hash

- Add E2E tests for anchor links functionality
- Implement scroll-to-anchor when navigating to document with #blockId hash
- Tests cover: hover shows anchor icon, copy to clipboard, scroll to block
- Regression test ensures CommentsMenu still works on right side

* Fix scroll-to-anchor dependency arrays for navigation edge cases

- Add documentId to scroll effect dependencies to re-run when navigating
  between documents with the same anchor hash
- Add location.hash to reset effect dependencies to properly reset flag
  when navigating via back button or changing anchors

* Fix accidental logo replacement and E2E blockquote test

- Restore original Metabase logo SVG (was replaced with red star debug graphic)
- Fix E2E test: use find('blockquote') instead of findByRole('blockquote')
  since native blockquote elements have no implicit ARIA role

* Fix scroll-to-anchor reliability and empty line behavior

- Implement retry mechanism for scroll-to-anchor to ensure target element exists
- Add highlight animation when scrolling to anchor
- Hide anchor link and comment menus on empty lines (Heading, Paragraph, Blockquote)

* Fix selector mismatch for scroll-to-anchor

- Add data-node-id attribute to NodeViewWrappers in Heading, Paragraph, and Blockquote
  extensions so they can be queried by DocumentPage
- Update DocumentPage to query using [data-node-id] selector instead of internal _id attribute
- Remove unused ID_ATTRIBUTE_NAME import in DocumentPage

* Fix highlight animation color variable

- Replace non-existent --mb-color-brand-alpha-25 with --mb-color-brand-light
- Ensure highlight animation is visible when scrolling to anchor

* added anchor links to lists and code blocks. some bugs fixes.

* make sure scroll-to-anchor observer is correctly initialized

- Refactor scroll-to-anchor logic to ensure smoother navigation
- Optimize highlight animation visibility during scrolling
- Adjust anchor link behavior on empty lines for better user experience

* small fixes

* clean up

* fixes and cleanup

* e2e test fixes

* fixed e2e test

* Leave anchor links only for header blocks

* Fix alignment

* Review fixes

* Fix unit tests

---------

Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
2026-01-22 02:03:04 +03:00
bryan
087a317f70 Fix ci-report output formatting issues (#68505) 2026-01-21 15:50:57 -07:00
Denis Berezin
c63fd746d8 Uxw 2633 copy cells from table (#68429)
* UXW-2633 fix sorted table values copy-paste

* Add e2e test
2026-01-22 01:24:12 +03:00
dpsutton
f29a446aab fix materialized views test (#68509)
https://github.com/metabase/metabase/pull/68190 did:

> Treats a 0 value of n_live_tup as NULL, due to confusion reported when
> auto analyze thresholds have not been met on a table.

I think what's happening is this is trying to suss out "i don't know"
and not give misleading information. When there are no stats pg would
report there are 0 rows rather than saying i don't know. So we toss away
these unreliable 0 count estimations.

This test flakes 7% of the time. Our hypothesis is that stats are empty
7% of the time so this row count estimation now gets removed.

By default the schema is `:estimated_row_count (mt/malli=? [:maybe
:int])` which was being clobbered with
`{:estimated_row_count (mt/malli=? int?)}` So the fix is to just not
clobber
2026-01-21 16:07:45 -06:00
Edward Paget
d26bf4e454 feat: support transforms in remote sync (#68255)
* feat: support transforms in remote sync

1. Serialize Transforms as Collection Items

   Transforms were previously serialized as a top-level item, but this
   moves them to be serialized as a child of the collection they are
   member of. This also fixes an error in their serdes/dependencies
   implementation where it did not include the parent collection leading
   to deserialization issues.

2. Enforce Remote-Sync write rules

   Adds implementations of the model interface permissions methods for
   transforms that enforce the can-write? = false and can-create? =
   false rules when remote-sync is in read-only mode.

3. Add a setting to control whether transforms are synced

   Transforms are globally synced not synced via collections, so this
   adds a new boolean setting that controls whether they are synced on
   instances in read-write mode. Like all other content transforms are
   always synced from the remote source if the instance is in read-only
   mode.

4. Add a new spec-based configuration system for models

   Models we want to make remote-syncable have a complicated, but still
   data-driven system to allow you to specify their remote-sync behavior
   and have that automatically setup in a single place the
   `metabase-enterprise.remote-sync.spec` namespace.

* fixup! test with wrong folder

* fixup! not handling archived transform collections

* fixup! clean up aliases
2026-01-21 15:29:27 -06:00
Denis Berezin
7aa18ae448 Fix flaky test that uses readonly db that lacks permissions (#68417) 2026-01-22 00:15:27 +03:00
Timothy S. Dean
97da3d7c7b Migration follow-up fix to #68100 (#68498)
Move a migration from the v59 file to the v56 file, as we intend to backport it that far.
2026-01-21 14:15:12 -07:00
Aleksandr Lesnenko
10e8a68cf5 Data Analysts user group (#67770)
* basic analyst role

* analyst role

* Backend for data analyst role

* Fixup frontend to work with Backend

The dedicated analyst endpoints have been removed:
- `GET /api/user/analysts` -> Use `GET /api/user?is_data_analyst=true`
- `POST /api/user/analysts/:id` -> Use `PUT /api/user/:id` with `{ is_data_analyst: true }`
- `DELETE /api/user/analysts/:id` -> Use `PUT /api/user/:id` with `{ is_data_analyst: false }`

These hooks no longer exist:
- useListAnalystsQuery
- useAddAnalystMutation
- useRemoveAnalystMutation

New Patterns:

Listing analysts:
```
// Before
const { data } = useListAnalystsQuery();

// After
const { data } = useListUsersQuery({ is_data_analyst: true });
```

Adding an analyst:
```
// Before
await addAnalyst(userId).unwrap();

// After
await updateUser({ id: userId, is_data_analyst: true }).unwrap();
```

Removing an analyst:
```
// Before
await removeAnalyst(userId).unwrap();

// After
await updateUser({ id: userId, is_data_analyst: false }).unwrap();
```

Type Changes:

The field is_analyst has been renamed to is_data_analyst everywhere:

- User.is_data_analyst (was is_analyst)
- UpdateUserRequest.is_data_analyst (was is_analyst)
- ListUsersRequest.is_data_analyst (new field for filtering)

* kondo

* circular dep

* fix backend not compiling

* use the actual role

* analyst management, glossary access for admins

* various data studio access

* access to library

* drop custom analyst seats management ui

* automatic perms group

* fix tests

* specs

* permissions

* fix lints, add migration rollbacks

* data analysts can ALWAYS view table metadata

implemented for:

- search
- `table/can-read?`

* Also remove `can_access_data_model`

* Revert "Also remove `can_access_data_model`"

This reverts commit 03af432ccb.

* fix more tests

* more tests

* snapshots

* remove redundant checks

* everyone can see

* hide data analyst group for OSS

* Make data analysts a normal group on OSS

* Automatically provide data analysts with perms

Data analysts automatically get manage-table-metadata permissions on
every table/database.

* only disable setting some perms for data analysts

* kondo

* oops, one more kondo

* test fixes

* specs, fixes

* more specs, fixes

* cleanup

* more test fixes

* fix module config

* ci

* doh, fix test

* more test whack-a-mole

* move data analyst group to the top of perms views

* review

* ci

* Turn DA into a regular group when feature missing

instead of just on OSS.

The goal is:
- OSS: a previously existing Data Analysts group is a regular group with
configurable permissions. The real Data Analysts group is invisible and
has no members.
- EE without the token: same as above
- EE with the token: the real Data Analysts group is visible.

* oops, remove unnecessary require

* another one

* fix membership API

* fix a couple more tests

* macro gets a !

* fix `fetch-groups-test`

* ci

* ci

* ci

* ci

* fix collection perm in migration

---------

Co-authored-by: John Swanson <john.swanson@metabase.com>
2026-01-21 12:43:35 -08:00
Dan Meyer
209a34b102 ci: Enable fail-fast for BigQuery and Snowflake driver tests (#68199)
Add :fail-fast? true to BigQuery and Snowflake test-args, matching the
existing Redshift configuration. This stops test execution on first
failure, saving CI time and resources.
2026-01-21 20:27:03 +00:00
William
a6df84c136 Bump toucan2 version to fix dashboards sometimes failing to load (#68494)
If the first table mentioned by a dashboard has no fields (presumably because the table and its field are all
inactive), /api/dashboard/<id>/query_metadata would fail due to a toucan2 bug.  This bumps toucan2 to fix the bug and
adds a test to verify that this case now works.
2026-01-21 20:18:30 +00:00
Riley Thompson
adb9a5cbf4 Bump dependency analysis version (#68488)
bump dependency analysis version
2026-01-21 19:08:45 +00:00
3153 changed files with 189437 additions and 142475 deletions

View File

@@ -29,7 +29,7 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
**Comments:**
- `TODO` format: `;; TODO (Name M/D/YY) -- description`
- `TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
## Code Organization

View File

@@ -0,0 +1,504 @@
---
name: analytics-events
description: Add product analytics events to track user interactions in the Metabase frontend
allowed-tools: Read, Write, Edit, Grep, Glob
---
# Frontend Analytics Events Skill
This skill helps you add product analytics (Snowplow) events to track user interactions in the Metabase frontend codebase.
## Quick Reference
Analytics events in Metabase use Snowplow with typed event schemas. All events must be defined in TypeScript types before use.
**Key Files:**
- `frontend/src/metabase-types/analytics/event.ts` - Event type definitions
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry
- `frontend/src/metabase/lib/analytics.ts` - Core tracking functions
- Feature-specific `analytics.ts` files - Tracking function wrappers
## Quick Checklist
When adding a new analytics event:
- [ ] Define event type in `frontend/src/metabase-types/analytics/event.ts`
- [ ] Add event to appropriate union type (e.g., `DataStudioEvent`, `SimpleEvent`)
- [ ] Create tracking function in feature's `analytics.ts` file
- [ ] Import and call tracking function at the interaction point
- [ ] Use `trackSimpleEvent()` for basic events (most common)
## Event Schema Types
### 1. Simple Events (Most Common)
Use `SimpleEventSchema` for straightforward tracking. It supports these standard fields:
```typescript
type SimpleEventSchema = {
event: string; // Required: Event name (snake_case)
target_id?: number | null; // Optional: ID of affected entity
triggered_from?: string | null; // Optional: UI location/context
duration_ms?: number | null; // Optional: Duration in milliseconds
result?: string | null; // Optional: Outcome (e.g., "success", "failure")
event_detail?: string | null; // Optional: Additional detail/variant
};
```
**When to use:** 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
### 2. Custom Schemas (legacy, no events are being added)
Consider adding new event schema only in very special cases.
**Examples:** `DashboardEventSchema`, `CleanupEventSchema`, `QuestionEventSchema`
## Step-by-Step: Adding a Simple Event
### Example: Track when a user applies filters in a table picker
#### Step 1: Define Event Types
Add event type definitions to `frontend/src/metabase-types/analytics/event.ts`:
```typescript
export type DataStudioTablePickerFiltersAppliedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_applied";
}>;
export type DataStudioTablePickerFiltersClearedEvent = ValidateEvent<{
event: "data_studio_table_picker_filters_cleared";
}>;
```
#### Step 2: Add to Union Type
Find or create the appropriate union type and add your events:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent
| DataStudioGlossaryEditedEvent
| DataStudioGlossaryDeletedEvent
| DataStudioTablePickerFiltersAppliedEvent // <- Add here
| DataStudioTablePickerFiltersClearedEvent; // <- Add here
```
#### Step 3: Create Tracking Functions
In your feature's `analytics.ts` file (e.g., `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`):
```typescript
import { trackSimpleEvent } from "metabase/lib/analytics";
export const trackDataStudioTablePickerFiltersApplied = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_applied",
});
};
export const trackDataStudioTablePickerFiltersCleared = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_cleared",
});
};
```
#### Step 4: Use in Components
Import and call the tracking function at the interaction point:
```typescript
import {
trackDataStudioTablePickerFiltersApplied,
trackDataStudioTablePickerFiltersCleared,
} from "metabase-enterprise/data-studio/analytics";
function FilterPopover({ filters, onSubmit }) {
const handleReset = () => {
trackDataStudioTablePickerFiltersCleared(); // <- Track here
onSubmit(emptyFilters);
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
trackDataStudioTablePickerFiltersApplied(); // <- Track here
onSubmit(form);
}}
>
{/* form content */}
</form>
);
}
```
## Using SimpleEventSchema Fields
### Example: Event with target_id
```typescript
// Type definition
export type DataStudioLibraryCreatedEvent = ValidateEvent<{
event: "data_studio_library_created";
target_id: number | null;
}>;
// Tracking function
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
trackSimpleEvent({
event: "data_studio_library_created",
target_id: Number(id),
});
};
// Usage
trackDataStudioLibraryCreated(newLibrary.id);
```
### Example: Event with triggered_from
```typescript
// Type definition
export type NewButtonClickedEvent = ValidateEvent<{
event: "new_button_clicked";
triggered_from: "app-bar" | "empty-collection";
}>;
// Tracking function
export const trackNewButtonClicked = (location: "app-bar" | "empty-collection") => {
trackSimpleEvent({
event: "new_button_clicked",
triggered_from: location,
});
};
// Usage
<Button onClick={() => {
trackNewButtonClicked("app-bar");
handleCreate();
}}>
New
</Button>
```
### Example: Event with event_detail
```typescript
// Type definition
export type MetadataEditEvent = ValidateEvent<{
event: "metadata_edited";
event_detail: "type_casting" | "semantic_type_change" | "visibility_change";
triggered_from: "admin" | "data_studio";
}>;
// Tracking function
export const trackMetadataChange = (
detail: "type_casting" | "semantic_type_change" | "visibility_change",
location: "admin" | "data_studio"
) => {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: location,
});
};
// Usage
trackMetadataChange("semantic_type_change", "data_studio");
```
### Example: Event with result and duration
```typescript
// Type definition
export type MoveToTrashEvent = ValidateEvent<{
event: "moved-to-trash";
target_id: number | null;
triggered_from: "collection" | "detail_page" | "cleanup_modal";
duration_ms: number | null;
result: "success" | "failure";
event_detail: "question" | "model" | "metric" | "dashboard";
}>;
// Tracking function
export const trackMoveToTrash = (params: {
targetId: number | null;
triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
durationMs: number | null;
result: "success" | "failure";
itemType: "question" | "model" | "metric" | "dashboard";
}) => {
trackSimpleEvent({
event: "moved-to-trash",
target_id: params.targetId,
triggered_from: params.triggeredFrom,
duration_ms: params.durationMs,
result: params.result,
event_detail: params.itemType,
});
};
// Usage with timing
const startTime = Date.now();
try {
await moveToTrash(item);
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "success",
itemType: "question",
});
} catch (error) {
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "failure",
itemType: "question",
});
}
```
## Naming Conventions
### Event Names (snake_case)
```typescript
// Good
"data_studio_library_created"
"table_picker_filters_applied"
"metabot_chat_opened"
// Bad
"DataStudioLibraryCreated" // Wrong case
"tablePickerFiltersApplied" // Wrong case
"filters-applied" // Use underscore, not hyphen
```
### Event Type Names (PascalCase with "Event" suffix)
```typescript
// Good
DataStudioLibraryCreatedEvent
TablePickerFiltersAppliedEvent
MetabotChatOpenedEvent
// Bad
dataStudioLibraryCreated // Wrong case
DataStudioLibraryCreated // Missing "Event" suffix
```
### Tracking Function Names (camelCase with "track" prefix)
```typescript
// Good
trackDataStudioLibraryCreated
trackTablePickerFiltersApplied
trackMetabotChatOpened
// Bad
DataStudioLibraryCreated // Missing "track" prefix
track_library_created // Wrong case
logLibraryCreated // Use "track" prefix
```
## Common Patterns
### Pattern 1: Feature-Specific Union Types
Group related events together:
```typescript
export type DataStudioEvent =
| DataStudioLibraryCreatedEvent
| DataStudioTablePublishedEvent
| DataStudioGlossaryCreatedEvent;
export type MetabotEvent =
| MetabotChatOpenedEvent
| MetabotRequestSentEvent
| MetabotFixQueryClickedEvent;
// Then add to SimpleEvent union
export type SimpleEvent =
| /* other events */
| DataStudioEvent
| MetabotEvent
| /* more events */;
```
### Pattern 2: Conditional Tracking
Track different events based on user action:
```typescript
const handleSave = async () => {
if (isNewItem) {
await createItem(data);
trackItemCreated(newItem.id);
} else {
await updateItem(id, data);
trackItemUpdated(id);
}
};
```
## Common Pitfalls
### Don't: Add custom fields to SimpleEvent
```typescript
// WRONG - SimpleEvent doesn't support custom fields
export const trackFiltersApplied = (filters: FilterState) => {
trackSimpleEvent({
event: "filters_applied",
data_layer: filters.dataLayer, // ❌ Not in SimpleEventSchema
data_source: filters.dataSource, // ❌ Not in SimpleEventSchema
with_owner: filters.hasOwner, // ❌ Not in SimpleEventSchema
});
};
// RIGHT - Use only standard SimpleEventSchema fields
export const trackFiltersApplied = () => {
trackSimpleEvent({
event: "filters_applied",
});
};
// Or use event_detail for a single variant
export const trackFilterApplied = (filterType: string) => {
trackSimpleEvent({
event: "filter_applied",
event_detail: filterType, // ✓ "data_layer", "data_source", etc.
});
};
```
### Don't: Forget to add event to union type
```typescript
// Define the event
export type NewFeatureClickedEvent = ValidateEvent<{
event: "new_feature_clicked";
}>;
// ❌ WRONG - Forgot to add to SimpleEvent union
// Event won't be recognized by TypeScript
// ✓ RIGHT - Add to appropriate union
export type SimpleEvent =
| /* other events */
| NewFeatureClickedEvent;
```
### Don't: Mix up event name formats
```typescript
// WRONG
event: "dataStudioLibraryCreated" // camelCase
event: "data-studio-library-created" // kebab-case
event: "Data_Studio_Library_Created" // Mixed case
// RIGHT
event: "data_studio_library_created" // snake_case
```
### Don't: Track PII or sensitive data
```typescript
// WRONG - Don't track user emails, names, or sensitive data
trackSimpleEvent({
event: "user_logged_in",
event_detail: user.email, // ❌ PII
});
// RIGHT - Track non-sensitive identifiers only
trackSimpleEvent({
event: "user_logged_in",
target_id: user.id, // ✓ Just the ID
});
```
### Don't: Forget to track both success and failure
```typescript
// WRONG - Only tracking success
try {
await saveData();
trackDataSaved();
} catch (error) {
// ❌ No tracking for failure case
}
// RIGHT - Track both outcomes
try {
await saveData();
trackDataSaved({ result: "success" });
} catch (error) {
trackDataSaved({ result: "failure" });
}
```
## Testing Analytics Events
While developing, you can verify events are firing:
1. **Check browser console** - When `SNOWPLOW_ENABLED=true` in dev, events are logged
2. **Use shouldLogAnalytics** - Set in `metabase/env` to see all analytics in console
3. **Check Snowplow debugger** - Browser extension for Snowplow events
Example console output:
```
[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied
```
## File Organization
### Where to put tracking functions:
```
Feature-specific analytics functions:
frontend/src/metabase/{feature}/analytics.ts
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
Event type definitions (all in one place):
frontend/src/metabase-types/analytics/event.ts
Core tracking utilities:
frontend/src/metabase/lib/analytics.ts
```
## Real-World Examples
See these files for reference:
- **Simple events**: `enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts`
- **Events with variants**: `frontend/src/metabase/dashboard/analytics.ts`
- **Complex events**: `frontend/src/metabase/query_builder/analytics.js`
- **Event type examples**: `frontend/src/metabase-types/analytics/event.ts`
## Workflow Summary
1. **Identify the user interaction** to track
2. **Decide on event name** (snake_case, descriptive)
3. **Define event type** in `event.ts` using `ValidateEvent`
4. **Add to union type** (create feature union if needed)
5. **Create tracking function** in feature's `analytics.ts`
6. **Import and call** at the interaction point
7. **Test** that events fire correctly
## Tips
- **Be specific** - `filters_applied` is better than `action_performed`
- **Use past tense** - `library_created` not `create_library`
- **Group related events** - Create feature-specific event union types
- **Track meaningful actions** - Not every click needs tracking
- **Consider the data** - What would you want to analyze later?
- **Stay consistent** - Follow existing naming patterns in the codebase
- **Document context** - Use `triggered_from` to track where the action happened

View File

@@ -54,7 +54,7 @@ Use this to scan through changes efficiently:
- [ ] Public vars in `src` or `enterprise/backend/src` have useful docstrings
- [ ] Docstrings use Markdown conventions
- [ ] References use `[[other-var]]` not backticks
- [ ] `TODO` comments include author and date: `;; TODO (Name 1/1/25) -- description`
- [ ] `TODO` comments include author and date: `;; TODO (Name 2025-01-01) -- description`
### Code Organization
@@ -118,7 +118,7 @@ Quick scan for common issues:
| `update-db`, `save-model` | Missing `!` for side effects: `update-db!`, `save-model!` |
| `snake_case_var` | Should use kebab-case |
| Public var without docstring | Add docstring explaining purpose |
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 1/1/25) -- description` |
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 2025-01-01) -- description` |
| `(defn foo ...)` in namespace used elsewhere | Should be `(defn ^:private foo ...)` |
| Function > 20 lines | Consider breaking up into smaller functions |
| `/api/dashboards/:id` | Use singular: `/api/dashboard/:id` |

View File

@@ -65,6 +65,7 @@
:metabase/missing-test-expr-requires-in-cljs {:level :warning}
:metabase/modules {:level :warning} ; more config in `.clj-kondo/config/modules/config.edn`
:metabase/namespace-name {:level :warning}
:metabase/no-jsqlparser-imports {:level :warning}
:metabase/require-shape-checker {:level :warning}
:metabase/test-helpers-use-non-thread-safe-functions {:level :warning} ; more config in `.clj-kondo/config/test-helpers/config.edn`
:metabase/tests-must-live-in-known-modules {:level :warning}
@@ -219,7 +220,8 @@
;; misc config for built-in linters
;;
:invalid-arity {:skip-args [metabase.lib.util.match/match]} ; TODO (cam): can we fix these?
:invalid-arity {:skip-args [metabase.lib.util.match/match
metabase.lib.util.match/match-lite]} ; TODO (cam): can we fix these?
:refer-all {:level :warning, :exclude [clojure.test]}
:unused-referred-var {:exclude {compojure.core [GET DELETE POST PUT]}}
@@ -238,7 +240,6 @@
(metabase.lib.util.match/match)
(metabase.lib.util.match/match-one)
(metabase.lib.util.match/match-lite)
(metabase.lib.util.match/match-lite-recursive)
(metabase.lib.util.match/replace)
(metabase.lib.util.match/replace-in)
(metabase.driver-api.core/match)
@@ -299,7 +300,8 @@
metabase-enterprise.transforms.interface/execute! {:message "Use metabase-enterprise.transforms.execute/execute! instead of metabase-enterprise.transforms.interface/execute!"}
next.jdbc/active-tx? {:message "Use metabase.app-db.core/in-transaction? to see if we are in a transaction"}
toucan2.core/debug {:message "Don't commit code using t2/debug"}
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}}
toucan2.tools.debug/debug {:message "Don't commit code using t2/debug"}
macaw.core/parsed-query {:message "Use metabase.driver.util/parsed-query instead of macaw.core/parsed-query"}}
:discouraged-namespace
{camel-snake-kebab.core {:message "CSK is not Turkish-safe, use the versions in metabase.util instead."}
@@ -522,8 +524,10 @@
metabase-enterprise.advanced-permissions.models.permissions.application-permissions-test/with-new-group-and-current-graph hooks.common/with-two-top-level-bindings
metabase-enterprise.audit-app.pages-test/with-temp-objects hooks.common/with-one-binding
metabase-enterprise.cache.config-test/with-temp-persist-models hooks.common/with-seven-bindings
metabase-enterprise.dependencies.api-test/with-dependents-test! hooks.common/with-two-bindings
metabase-enterprise.metabot-v3.tools.dependencies-test/with-dependent-transforms! hooks.common/with-two-bindings
metabase-enterprise.semantic-search.db.connection/with-migrate-tx hooks.common/let-one-with-optional-value
metabase-enterprise.serialization.api-test/with-serialization-test-data! hooks.common/with-three-bindings
metabase-enterprise.serialization.test-util/with-temp-dpc hooks.toucan2.tools.with-temp/with-temp
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding

View File

@@ -276,6 +276,7 @@
events
models
premium-features
request
settings
util}}
@@ -383,7 +384,6 @@
:uses #{api
app-db
collections
config
eid-translation
events
lib-be
@@ -776,6 +776,23 @@
settings
util}}
llm
{:team "Metabot"
:api #{metabase.llm.api
metabase.llm.init}
:uses #{analytics
api
driver
lib
lib-be
models
premium-features
request
settings
sync
util
warehouse-schema}}
logger
{:team "DevEx"
:api #{metabase.logger.api
@@ -809,6 +826,7 @@
lib-be
models
permissions
remote-sync
search
util}}
@@ -862,6 +880,7 @@
models
permissions
premium-features
remote-sync
util}}
notification
@@ -879,6 +898,7 @@
config
dashboards
driver
embedding
events
models
parameters
@@ -969,7 +989,8 @@
:api #{metabase.premium-features.api
metabase.premium-features.core
metabase.premium-features.init}
:uses #{api
:uses #{analytics
api
app-db
classloader
config
@@ -1200,7 +1221,8 @@
:api #{metabase.request.core
metabase.request.init
metabase.request.schema}
:uses #{api
:uses #{analytics
api
config
embedding
permissions ; FIXME
@@ -1456,6 +1478,7 @@
models
permissions
premium-features
query-processor
request
task
util}}
@@ -1499,7 +1522,6 @@
:uses #{analytics
api
collections
config
events
models
util}}
@@ -1649,7 +1671,6 @@
:uses #{analytics
api
app-db
config
database-routing
driver
events
@@ -1777,6 +1798,7 @@
metabase-enterprise.advanced-permissions.common
metabase-enterprise.advanced-permissions.models.permissions.group-manager}
:uses #{api
audit-app
lib
permissions
premium-features
@@ -1787,6 +1809,17 @@
warehouses
enterprise/impersonation}}
enterprise/agent-api
{:team "Metabot"
:api #{metabase-enterprise.agent-api.api}
:uses #{api
auth-identity
query-processor
request
server
util
enterprise/metabot-v3}}
enterprise/ai-entity-analysis
{:team "Metabot"
:api #{metabase-enterprise.ai-entity-analysis.api}
@@ -2079,44 +2112,45 @@
util}}
enterprise/metabot-v3
{:team "Metabot"
:api #{metabase-enterprise.metabot-v3.api
metabase-enterprise.metabot-v3.client
metabase-enterprise.metabot-v3.core
metabase-enterprise.metabot-v3.init
metabase-enterprise.metabot-v3.tools.api}
:uses #{activity-feed
api
app-db
audit-app
collections
config
documents
driver
legacy-mbql
lib
lib-be
models
native-query-snippets
permissions
premium-features
pulse
query-processor
request
search
server
settings
store-api
sync
system
task
users-rest
util
warehouse-schema
warehouses-rest
enterprise/dependencies
enterprise/transforms
enterprise/transforms-python}}
{:team "Metabot"
:api #{metabase-enterprise.metabot-v3.api
metabase-enterprise.metabot-v3.client
metabase-enterprise.metabot-v3.core
metabase-enterprise.metabot-v3.init
metabase-enterprise.metabot-v3.tools.api}
:friends #{enterprise/agent-api}
:uses #{activity-feed
api
app-db
audit-app
collections
config
documents
driver
legacy-mbql
lib
lib-be
models
native-query-snippets
permissions
premium-features
pulse
query-processor
request
search
server
settings
store-api
sync
system
task
users-rest
util
warehouse-schema
warehouses-rest
enterprise/dependencies
enterprise/transforms
enterprise/transforms-python}}
enterprise/permission-debug
{:team "UX West"
@@ -2247,6 +2281,7 @@
native-query-snippets
permissions
premium-features
remote-sync
util}}
enterprise/sso
@@ -2329,9 +2364,11 @@
lib
lib-be
models
permissions
premium-features
queries
query-processor
remote-sync
request
revisions
search
@@ -2350,8 +2387,11 @@
app-db
config
driver
events
lib
lib-be
models
permissions
query-processor
settings
util

View File

@@ -99,12 +99,48 @@
:message "Metabase namespaces should have the form metabase[-enterprise].<module>.* [:metabase/namespace-name]"
:type :metabase/namespace-name)))))))
(defn- ns-form-node->import-node [ns-form-node]
(some (fn [node]
(when (and (hooks/list-node? node)
(let [first-child (first (:children node))]
(and (hooks/keyword-node? first-child)
(= (hooks/sexpr first-child) :import))))
node))
(:children ns-form-node)))
(defn- jsqlparser-import?
"Check if a symbol or string represents a jsqlparser import."
[s]
(str/starts-with? (str s) "net.sf.jsqlparser"))
(defn- lint-jsqlparser-imports [ns-form-node]
(when-let [import-node (ns-form-node->import-node ns-form-node)]
(doseq [node (rest (:children import-node))
:when (not (contains? (hooks.common/ignored-linters node) :metabase/no-jsqlparser-imports))]
(cond
;; Handle (net.sf.jsqlparser.foo Bar Baz) form
(hooks/list-node? node)
(let [package-node (first (:children node))
package-name (when package-node (str (hooks/sexpr package-node)))]
(when (jsqlparser-import? package-name)
(hooks/reg-finding! (assoc (meta node)
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
:type :metabase/no-jsqlparser-imports))))
;; Handle net.sf.jsqlparser.foo.Bar form (single class import)
(hooks/token-node? node)
(when (jsqlparser-import? (hooks/sexpr node))
(hooks/reg-finding! (assoc (meta node)
:message "Don't import net.sf.jsqlparser classes directly. This code should be agnostic to the parsing library. [:metabase/no-jsqlparser-imports]"
:type :metabase/no-jsqlparser-imports)))))))
(defn lint-ns [x]
(doto (:node x)
lint-require-shapes
lint-requires-on-new-lines
(lint-modules (modules/config x))
lint-namespace-name)
lint-namespace-name
lint-jsqlparser-imports)
x)
(comment

View File

@@ -154,7 +154,6 @@
metabase.lib.util.match/match [[:inner 0]]
metabase.lib.util.match/match-one [[:inner 0]]
metabase.lib.util.match/match-lite [[:inner 0]]
metabase.lib.util.match/match-lite-recursive [[:inner 0]]
metabase.lib.util.match/replace [[:inner 0]]
metabase.lib.util.match/replace-in [[:inner 0]]
metabase.models.interface/define-batched-hydration-method [[:inner 0]]

View File

@@ -1,6 +0,0 @@
frontend/src/cljs
frontend/src/cljs_release
e2e/support/cypress_sample_database.js
e2e/support/cypress_sample_instance_data.js
frontend/src/metabase-types/openapi/types.gen.ts
e2e/embedding-sdk-host-apps

View File

@@ -8,7 +8,13 @@ const path = require("path");
// can use this flag to enable it. This is set to true in CI
const shouldLintCssModules =
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
const plugins = ["react", "no-only-tests", "ttag", "i18next"];
const plugins = [
"react",
"no-only-tests",
"ttag",
"i18next",
"metabase",
];
if (shouldLintCssModules) {
plugins.push("postcss-modules");
}
@@ -176,11 +182,11 @@ module.exports = {
{
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
rules: {
"jtag-missing-key": "error",
"no-unconditional-metabase-links-render": "error",
"no-color-literals": "error",
"no-literal-metabase-strings": "error",
"no-oss-reinitialize-import": "error",
"metabase/jtag-missing-key": "error",
"metabase/no-unconditional-metabase-links-render": "error",
"metabase/no-color-literals": "error",
"metabase/no-literal-metabase-strings": "error",
"metabase/no-oss-reinitialize-import": "error",
"depend/ban-dependencies": [
"error",
{
@@ -214,9 +220,9 @@ module.exports = {
"rspack.embedding-sdk-bundle.config.js",
],
rules: {
"no-color-literals": "off",
"no-unconditional-metabase-links-render": "off",
"no-literal-metabase-strings": "off",
"metabase/no-color-literals": "off",
"metabase/no-unconditional-metabase-links-render": "off",
"metabase/no-literal-metabase-strings": "off",
},
},
{
@@ -337,7 +343,7 @@ module.exports = {
"import/no-default-export": "off",
"import/order": "off",
"import/no-unresolved": "off",
"no-color-literals": "off",
"metabase/no-color-literals": "off",
},
},
{

View File

@@ -16,7 +16,7 @@ runs:
using: "composite"
steps:
- name: Install Chrome v111
uses: browser-actions/setup-chrome@v1
uses: browser-actions/setup-chrome@v2
with:
# https://chromium.cypress.io/linux/stable/111.0.5563.146
chrome-version: 1097615

View File

@@ -12,6 +12,10 @@ inputs:
logs-artifact-suffix:
required: false
description: Suffix to append to the test logs artifact name to avoid conflicts in matrix jobs
edition:
required: false
default: 'ee'
description: Edition to test (oss or ee)
runs:
using: "composite"
@@ -28,7 +32,7 @@ runs:
env:
MB_EDITION: ee
- name: Test database driver
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
run: clojure -X:dev:ci:${{ inputs.edition }}:${{ inputs.edition }}-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
shell: bash
id: run-test
# Trunk Quarantine controls the final state of the run in the CI!
@@ -52,7 +56,7 @@ runs:
if: always() && steps.run-test.outcome != 'success'
uses: actions/upload-artifact@v4
with:
name: ${{ format('test-logs-{0}{1}', github.job, (inputs.logs-artifact-suffix == '' || inputs.logs-artifact-suffix == null) && '' || format('-{0}', inputs.logs-artifact-suffix)) }}
name: ${{ format('test-logs-{0}{1}', github.job, inputs.logs-artifact-suffix && format('-{0}', inputs.logs-artifact-suffix) || '') }}
path: logs/test-log.json
retention-days: 1
# Failing to upload the artifact should never affect the outcome of the whole test run.

40
.github/scripts/find-linear-issue.js vendored Normal file
View File

@@ -0,0 +1,40 @@
async function findLinearIssue({ githubIssueUrl, linearApiKey }) {
const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: linearApiKey,
},
body: JSON.stringify({
query: `
query {
attachmentsForURL(url: "${githubIssueUrl}") {
nodes {
issue { identifier }
}
}
}
`,
}),
});
const data = await response.json();
if (!response.ok) {
return { success: false, message: `Linear API error: ${response.status}` };
}
if (!data.data?.attachmentsForURL?.nodes?.length) {
return {
success: false,
message: `No Linear issue linked to ${githubIssueUrl}`,
};
}
return {
success: true,
issue: data.data.attachmentsForURL.nodes[0].issue.identifier,
};
}
module.exports = { findLinearIssue };

13
.github/team.json vendored
View File

@@ -86,14 +86,11 @@
"eliotmetabase",
"npfitz",
"johnswanson",
"iethree",
"nvoxland",
"edpaget",
"deniskaber",
"alxnddr",
"bpander",
"andsalves",
"gantoreno",
"kevinkelleher12",
"undrfined"
]
@@ -112,6 +109,16 @@
"uladzimirdev",
"egorgrushin"
]
},
{
"name": "Growth Eng",
"label": ".Team/GrowthEng",
"members": [
"iethree",
"bpander",
"gantoreno",
"losrebellos"
]
}
]
}

View File

@@ -70,6 +70,31 @@ jobs:
core.setFailed(error.message);
}
- name: Find linked Linear issue
id: find_linear
continue-on-error: true
uses: actions/github-script@v7
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
ISSUE_NUMBER: ${{ steps.get_issue.outputs.issue_number }}
with:
script: |
const { findLinearIssue } = require('${{ github.workspace }}/.github/scripts/find-linear-issue.js');
const githubIssueUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${process.env.ISSUE_NUMBER}`;
const result = await findLinearIssue({
githubIssueUrl,
linearApiKey: process.env.LINEAR_API_KEY
});
if (result.success) {
console.log(`Found Linear issue: ${result.issue}`);
core.setOutput('linear_issue', result.issue);
} else {
console.log(`Could not find Linear issue: ${result.message}`);
}
- name: Set up git configuration
run: |
git config --global user.name "Claude Code[bot]"
@@ -259,6 +284,7 @@ jobs:
core.setOutput('reviewers', issueCreator);
- name: Create Pull Request
id: create_pr
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
@@ -279,6 +305,8 @@ jobs:
body: |
This PR automatically fixes the bug reported in #${{ env.ISSUE_NUMBER }}.
${{ steps.find_linear.outputs.linear_issue && format('Closes {0}', steps.find_linear.outputs.linear_issue) || '' }}
## Bug Report
**Original Issue:** #${{ env.ISSUE_NUMBER }}
**Reporter:** @${{ env.ISSUE_AUTHOR }}
@@ -299,7 +327,6 @@ jobs:
- If issues are found, either fix them or close this PR
---
Closes #${{ env.ISSUE_NUMBER }}
🤖 Generated with [Claude Code](https://claude.ai/code)
@@ -309,6 +336,7 @@ jobs:
automated-fix
ci skip
reviewers: ${{ steps.prepare_reviewers.outputs.reviewers }}
assignees: ${{ github.actor }}
- name: Log existing branch update
if: steps.check_changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true'

View File

@@ -1,33 +0,0 @@
name: Cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -146,6 +146,7 @@ jobs:
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests
# test-args: >-
@@ -509,6 +510,37 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-h2-oss:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.h2-should-run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI: 'true'
name: H2 (OSS)
steps:
- uses: actions/checkout@v4
- name: Test H2 driver (OSS)
id: test-driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-h2-oss'
edition: 'oss'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mysql-mariadb:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.mysql-mariadb-should-run == 'true' }}
@@ -1019,6 +1051,7 @@ jobs:
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
logs-artifact-suffix: ${{ matrix.version.name }}-${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
@@ -1224,6 +1257,7 @@ jobs:
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/transforms-python-test]
:fail-fast? true
# DS 2025-09-26 temporarily disabled while we diagnose flakes
# - name: Transforms Python Tests

View File

@@ -120,6 +120,10 @@ jobs:
- name: Prepare Cypress environment
id: cypress-prep
uses: ./.github/actions/prepare-cypress
- name: Compile CLJS
run: yarn build-pure:cljs
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start

View File

@@ -104,6 +104,9 @@ jobs:
if: github.run_attempt != '1'
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
- name: Compile CLJS
run: yarn build-pure:cljs
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start

View File

@@ -1,56 +0,0 @@
name: Fuzzing
on:
workflow_dispatch:
push:
branches:
- "master"
- "release-**"
paths:
- "frontend/src/metabase/querying/expressions/**"
pull_request:
paths:
- "frontend/src/metabase/querying/expressions/**"
jobs:
fe-fuzz-lexifier:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/pratt/fuzz.lexifier.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the lexifier
fe-fuzz-compiler:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/fuzz.compiler.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the custom expression compiler
fe-fuzz-string-quoting:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- run: yarn test-unit frontend/src/metabase/querying/expressions/fuzz.string.unit.spec.ts
env:
MB_FUZZ: 1
name: Run fuzz testing on the custom expression string quoter

View File

@@ -326,10 +326,11 @@ jobs:
const { getExtraTagsForVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
const latestMajorVersion = '${{ vars.LATEST_MAJOR_VERSION }}';
const extra_tags = getExtraTagsForVersion({ version });
const extra_tags = getExtraTagsForVersion({ version, latestMajorVersion });
console.log({ extra_tags });
console.log({ extra_tags, latestMajorVersion });
core.setOutput('extra_tags', JSON.stringify(extra_tags));
add-extra-tags:
@@ -347,9 +348,11 @@ jobs:
with:
version: ${{ inputs.version }}
tag_name: ${{ matrix.tag }}
tag_ee: ${{ contains(matrix.tag, 'v1') }}
tag_oss: ${{ contains(matrix.tag, 'v0') }}
dot-x-tag: true
# For version tags (v0.x, v1.x), apply to matching edition only
# For non-version tags like "latest", apply to both editions
tag_ee: ${{ contains(matrix.tag, 'v1') || !startsWith(matrix.tag, 'v') }}
tag_oss: ${{ contains(matrix.tag, 'v0') || !startsWith(matrix.tag, 'v') }}
dot-x-tag: ${{ startsWith(matrix.tag, 'v') }}
trigger-cloud-issues:
if: ${{ !inputs.auto }}

View File

@@ -23,9 +23,16 @@ jobs:
env:
DAYS_BACK: ${{ inputs.days_back || 7 }}
steps:
- name: Generate GitHub App Token
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -41,7 +48,7 @@ jobs:
const result = await collectAndAverageTimings({
github,
context,
token: '${{ secrets.GITHUB_TOKEN }}',
token: '${{ steps.app-token.outputs.token }}',
daysBack: ${{ env.DAYS_BACK }},
existingTimingsPath: 'e2e/support/timings.json'
});
@@ -59,7 +66,7 @@ jobs:
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
branch: ${{ env.BRANCH_NAME }}
base: master
commit-message: |
@@ -89,4 +96,7 @@ jobs:
Co-Authored-By: GitHub Actions <actions@github.com>
labels: |
.CI & Tests
no-backport
add-paths: e2e/support/timings.json
team-reviewers: |
devexp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -4,8 +4,5 @@
"jake.autoDetect": "off",
"gulp.autoDetect": "off",
"npm.autoDetect": "off",
"eslint.options": {
"rulePaths": ["frontend/lint/eslint-rules/"]
},
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -19,6 +19,10 @@ For detailed guidance on writing and reviewing code and documentation, see the s
- **[docs-write](.claude/skills/docs-write/SKILL.md)** - Documentation writing with Metabase style guide
- **[docs-review](.claude/skills/docs-review/SKILL.md)** - Documentation review checklist
## Frontend
- **[analytics-events](.claude/skills/analytics-events/SKILL.md)** - Add product analytics events to track user interactions
## Tool Preferences
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.

78
bb.edn
View File

@@ -139,6 +139,27 @@
(let [[db version] (:arguments parsed)]
(start-db/start-db (:db-info (current-task)) db version)))}
dev-repl
{:doc "Launch Metabase dev environment (frontend + backend) with interactive configuration"
:examples [["./bin/mage dev-repl" "Interactive TUI to configure and launch"]
["./bin/mage dev-repl -e ee -t all-features -d postgres"
"Launch with specific config (skip prompts)"]
["./bin/mage dev-repl --no-frontend" "Backend only (skip frontend dev server)"]
["./bin/mage dev-repl --fresh" "Recreate database container from scratch"]]
:requires [[mage.dev-repl :as dev-repl]]
:options [["-e" "--edition EDITION" "ee or oss"]
["-t" "--token TOKEN" "Token type: all-features, starter-cloud, pro-cloud, pro-self-hosted, none"]
["-d" "--db DB" "Database: postgres, mysql, mariadb, h2"]
["-c" "--config FILE" "Config file path (default: dev/config.yml)"]
["-v" "--db-version VERSION" "Database version: latest or oldest (default: latest)"]
["-p" "--port PORT" "Backend port (default: 3000)" :parse-fn Integer/parseInt]
["-f" "--fresh" "Recreate database container (destroy existing data)"]
["-F" "--no-frontend" "Skip starting the frontend dev server"]]
:db-info {:postgres {:ports {:oldest 5432 :latest 5433} :eol-url "https://endoflife.date/api/postgres.json"}
:mysql {:ports {:oldest 3308 :latest 3309} :eol-url "https://endoflife.date/api/mysql.json"}
:mariadb {:ports {:oldest 3306 :latest 3307} :eol-url "https://endoflife.date/api/mariadb.json"}}
:task (task! (dev-repl/dev-repl parsed (select-keys (current-task) [:db-info])))}
nrepl
{:doc "Starts the babashka nrepl: helpful for mage development"
:requires [[babashka.nrepl.server :as nrepl.server]
@@ -209,18 +230,20 @@
(nrepl-eval "metabase.comments.core" code)))}
run-tests
{:doc "Quickly run tests locally. Pass dirs/files as arguments to run them."
{:doc "Quickly run tests locally. Pass dirs, files, namespaces, or single tests as arguments."
:examples [["./bin/mage run-tests" "launch interactive test picker"]
["./bin/mage run-tests test/metabase/db" "run tests in test/metabase/db"]
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and src/metabase/api_keys"]
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]]
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and test/metabase/api_keys"]
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]
["./bin/mage run-tests metabase.config-test" "run all tests in the metabase.config-test namespace (by name)"]
["./bin/mage run-tests metabase.config-test/config-test" "run a single test by namespace/test-name"]]
;; TODO: add option to select files as well as dirs from fzf with styles
:options [["-s" "--selecting STYLE" "Style of test selection" :default "all"]
;; all, file, dir
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
:arg-schema [:sequential
{:details "If none are given, you will be prompted to select directories recursively from test/ and enterprise/backend/test/"}
[:string {:name "file-or-dir" :description "Files or directories (recursive) to test"}]]
[:string {:name "file-dir-ns-or-test" :description "Files, directories (recursive), namespaces, or namespace/test-name to test"}]]
:requires [[mage.quick-test-runner :as qtr]]
:task (task! (qtr/go parsed))}
@@ -307,32 +330,39 @@
:examples [["./bin/mage -driver-decisions --git-ref=master --is-master-or-release=false --pr-labels= --skip=false" "Determine driver decisions for a PR"]
["./bin/mage -driver-decisions --pr-labels=ci:all-cloud-drivers" "Run all cloud drivers via label"]
["./bin/mage -driver-decisions --github-output-only" "Output only GITHUB_OUTPUT format (for CI)"]]
:requires [[clojure.string :as str]
[table.core :as table]
[mage.modules]]
:usage-fn
(fn [_]
(->>
["# How we decide to run a driver's tests. For each driver, we check these conditions in order (first match wins):"
" |----------|-------------------------------------|--------|-------------------------------------------|"
" | Priority | Condition | Result | Reason |"
" |----------|-------------------------------------|--------|-------------------------------------------|"
" | 1 | Global skip (no backend changes) | SKIP | workflow skip (no backend changes) |"
" | 2 | Driver is :h2 | RUN | H2 always runs |"
" | 3 | Driver is quarantined | SKIP | driver is quarantined |"
" | 3(a) | ...and has break-quarantine-X label | RUN | anti-quarantine label present |"
" | 4 | On master or release branch | RUN | master/release branch |"
" | 5 | Cloud driver + ci:all-cloud-drivers | RUN | ci:all-cloud-drivers label |"
" | 6 | Cloud driver + its files changed | RUN | driver files changed |"
" | 7 | Cloud driver, no relevant changes | SKIP | no relevant changes for cloud driver |"
" | 8 | Driver deps affected by changes | RUN | driver module affected by shared code |"
" | 9 | Self-hosted driver, not affected | SKIP | driver module not affected |"
" |----------|-------------------------------------|--------|-------------------------------------------|"
""] (str/join "\n")))
(fn [_current-task]
(let [driver-triggers (vec (sort mage.modules/default-modules-which-trigger-drivers))]
(str
"# How we decide to run a driver's tests. For each driver, we check these conditions in order (first match wins):\n"
(with-out-str
(table/table [["Priority" "Condition" "Result" "Reason"]
["1" "Global skip (no backend changes)" "SKIP" "workflow skip (no backend changes)"]
["2" "Driver is :h2 or :postgres" "RUN" "H2/Postgres always run"]
["3" "Driver is quarantined" "SKIP" "driver is quarantined"]
["3(a)" "...and has break-quarantine-X label" "RUN" "anti-quarantine label present"]
["4" "On master or release branch" "RUN" "master/release branch"]
["5" "Cloud driver + ci:all-cloud-drivers" "RUN" "ci:all-cloud-drivers label"]
["6"
"Cloud driver + its files changed"
"RUN"
(str "any of " (pr-str driver-triggers) " affected by cloud-driver module changes")]
["7" "Cloud driver + query-processor updated" "RUN" "query-processor module updated"]
["8" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
["9"
(str (pr-str driver-triggers) " module deps affected")
"RUN"
(str "any of " (pr-str driver-triggers) " affected by module changes")]
["10" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
:options [[nil "--git-ref REF" "The git ref to compute changed modules against"]
[nil "--is-master-or-release MASTER_OR_RELEASE"]
[nil "--pr-labels PR_LABELS" "Comma delimited labels on the pr"]
[nil "--skip SKIP"]
[nil "--github-output-only" "Only output GITHUB_OUTPUT format (no human-readable output)"]]
:requires [[clojure.string :as str]
[mage.modules]]
:task (task! (mage.modules/-main parsed))}
-install-merge-drivers

View File

@@ -80,11 +80,11 @@
hiccup/hiccup {:mvn/version "2.0.0"} ; HTML templating
inflections/inflections {:mvn/version "0.15.0"} ; Clojure/Script library used for prularizing words
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
io.github.camsaul/toucan2 {:mvn/version "1.0.569"}
io.github.camsaul/toucan2 {:mvn/version "1.0.570"}
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
:git/url "https://github.com/eerohele/pp"}
io.github.metabase/macaw {:mvn/version "0.2.30"} ; Parse native SQL queries
io.github.metabase/macaw {:mvn/version "0.2.33"} ; Parse native SQL queries
io.netty/netty-codec-http {:mvn/version "4.2.9.Final"} ; override of transitive dep from aws s3
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} ; prometheus jvm collector
junegunn/grouper {:mvn/version "0.1.1"} ; Batch processing helper
@@ -362,6 +362,8 @@
"-Dmb.dangerous.unsafe.enable.testing.h2.connections.do.not.enable=true"
;; Ignore connection URI in favor of the MB_DB_FILE
"-Dmb.db.connection.uri="
;; Unset the premium token so that tests can set it programmatically
"-Dmb.premium.embedding.token="
;; Ignore config.yml
"-Dmb.config.file.path=__cypress__"
;; We use Snowplow micro in e2e tests
@@ -825,6 +827,12 @@
:exec-fn dev.h2/shell
:jvm-opts ["-Dfile.encoding=UTF-8"]}
;; connect to an H2 TCP listener:
;; clojure -M:connect-to-h2-tcp --port 9092 --db-file /path/to/metabase
:connect-to-h2-tcp
{:extra-paths ["dev/src"]
:main-opts ["-m" "dev.h2"]}
:generate-automagic-dashboards-pot
{:main-opts ["-m" "metabase.xrays.automagic-dashboards.dashboard-templates"]}

487
dev/src/dev/coverage.clj Normal file
View File

@@ -0,0 +1,487 @@
(ns dev.coverage
(:require
[clojure.set :as set]
[clojure.string :as str]
[clojure.test :as t]
[metabase.util.malli.registry :as m.registry]
[rewrite-clj.node :as n]
[rewrite-clj.zip :as rz]))
(set! *warn-on-reflection* true)
(defn- test-results [fq-test-name]
(let [test-var (find-var fq-test-name)
results (atom false)]
(binding [t/report (fn [m]
(case (:type m)
:fail (reset! results true)
:error (reset! results true)
#_else nil))]
(t/run-test-var test-var))
(if @results
:killed
:survived)))
(defn ns-coverage [code-namespaces test-namespaces]
(let [coverage-pairs (atom #{})
current-test (atom nil)
original-fns (atom {})]
;; Store original functions and wrap them (both public and private)
(doseq [ns-sym code-namespaces]
(require ns-sym :reload)
(let [ns-obj (find-ns ns-sym)]
(doseq [[var-name var-obj] (ns-interns ns-obj)]
(when (and (var? var-obj)
(or (fn? @var-obj)
(instance? clojure.lang.MultiFn @var-obj)))
(let [original-fn @var-obj
full-name (symbol (str ns-sym) (str var-name))]
;; Store original function
(swap! original-fns assoc full-name original-fn)
;; Replace with tracking wrapper
(alter-var-root var-obj
(constantly
(fn [& args]
(when-let [test @current-test]
(swap! coverage-pairs conj [full-name test]))
(apply original-fn args)))))))))
;; Run tests, tracking which test is running
(doseq [test-ns test-namespaces]
(require test-ns :reload)
(let [test-vars (->> (ns-interns test-ns)
vals
(filter (fn [v] (:test (meta v)))))]
(doseq [^clojure.lang.Var test-var test-vars]
(let [test-name (symbol (str test-ns) (name (.sym test-var)))]
(reset! current-test test-name)
(t/test-var test-var)))))
;; Restore original functions
(doseq [ns-sym code-namespaces]
(let [ns-obj (find-ns ns-sym)]
(doseq [[var-name var-obj] (ns-interns ns-obj)]
(when-let [original-fn (get @original-fns (symbol (str ns-sym) (str var-name)))]
(alter-var-root var-obj (constantly original-fn))))))
;; Return set of [function, test] tuples
(let [all-fns (set (keys @original-fns))
coverage @coverage-pairs
covered-fns (set (map first coverage))]
{:coverage-pairs coverage
:coverage (reduce #(merge-with into %1 %2) {} (for [[f t] coverage]
{f #{t}}))
:all-fns all-fns
:uncovered-fns (set/difference all-fns covered-fns)
:covered-fns covered-fns})))
(def mutation-rules
"Map of symbols to their mutations"
{'and 'or
'or 'and
'boolean 'not
'not 'boolean
'empty? 'seq
'seq 'empty?
'empty 'not-empty
'not-empty 'empty
'<= '<
'< '<=
'>= '>
'> '>=
'= 'not=
'not= '=
'for 'doseq
'+ '-
'- '+
'* '/
'/ '*
'true 'false
'false 'true
'nil? 'some?
'some? 'nil?
'inc 'dec
'dec 'inc
'when 'when-not
'when-not 'when
'if 'if-not
'if-not 'if
'do 'comment
'let 'comment})
(defn find-function-source
"Find the source code for a fully-qualified function name"
[fq-name]
(let [ns-sym (symbol (namespace fq-name))
fn-name (symbol (name fq-name))]
(when-let [var-obj (ns-resolve ns-sym fn-name)]
(when-let [file (:file (meta var-obj))]
(when (and file (not= file "NO_SOURCE_PATH"))
(when-let [line (:line (meta var-obj))]
{:file file
:line line
:var var-obj
:ns ns-sym
:name fn-name}))))))
(defn read-function-from-file
"Read the function definition from its source file"
[{:keys [file line]}]
(let [;; Try different possible file paths
possible-paths [(str "src/" file)
file
(str "test/" file)]
source-file (first (filter #(and % (.exists (java.io.File. ^String %))) possible-paths))]
(when-not source-file
(throw (ex-info (str "Could not find source file for: " file)
{:file file
:tried possible-paths})))
(let [source (slurp source-file)
zloc (rz/of-string source)]
;; Find the defn at the specified line
(loop [loc zloc]
(when-not (rz/end? loc)
(if (and (rz/list? loc)
(let [first-child (rz/down loc)]
(when (and first-child (rz/sexpr-able? first-child))
(let [first-sym (rz/sexpr first-child)]
;; Match various function definition forms
(or (contains? #{'defn 'defn- 'defmethod} first-sym)
;; Match qualified defn macros like mu/defn, s/defn, etc.
(and (symbol? first-sym)
(re-matches #".*defn-?" (name first-sym)))))))
(= line (get (meta (rz/node loc)) :row)))
(rz/string loc)
(recur (rz/next loc))))))))
(defn- schema-keyword? [k]
(or (contains? #{:-
:string, :int, :double, :boolean, :keyword, :symbol, :nil, :uuid, :inst
:vector, :set, :map, :tuple
:map-of, :map-of-schema
:sequential, :cat
:enum :ref :optional :some :maybe :min :max :re}
k)
(m.registry/schema k)))
(defn generate-all-mutations-for-loc
"Generate all possible mutations at a single location"
[zloc]
(when (rz/sexpr-able? zloc)
(let [sexpr (rz/sexpr zloc)]
(cond
;; Symbol mutations (like and -> or, plus always try nil)
(symbol? sexpr)
(let [nil-mutation {:mutation (rz/root-string (rz/replace zloc (n/token-node nil)))
:description (str "Replace " sexpr " with nil")
:original sexpr
:replacement nil
:position (meta (rz/node zloc))}]
(if-let [replacement (get mutation-rules sexpr)]
[{:mutation (rz/root-string (rz/replace zloc (n/token-node replacement)))
:description (str "Replace " sexpr " with " replacement)
:original sexpr
:replacement replacement
:position (meta (rz/node zloc))}
nil-mutation]
[nil-mutation]))
;; Keyword mutations (like :id -> :id__)
;; auto-resolved keywords don't work quite right when using rz/sexpr
;; so read the string representation instead
;; this will expand the namespace to a fully-qualified form
;; this requires that *ns* is bound correctly
(keyword? sexpr)
(let [keyword-string (rz/string zloc)
sexpr (read-string keyword-string)]
(when (and (not (schema-keyword? sexpr))
(not= sexpr :else))
(let [mutated-keyword (keyword (str (name sexpr) "__"))]
[{:mutation (rz/root-string (rz/replace zloc (n/token-node mutated-keyword)))
:description (str "Replace " sexpr " with " mutated-keyword)
:original sexpr
:replacement mutated-keyword
:position (meta (rz/node zloc))}])))
;; Number mutations (0 -> 1, other numbers -> 0)
(number? sexpr)
(let [replacement (if (zero? sexpr) 1 0)]
[{:mutation (rz/root-string (rz/replace zloc (n/token-node replacement)))
:description (str "Replace " sexpr " with " replacement)
:original sexpr
:replacement replacement
:position (meta (rz/node zloc))}])
:else nil))))
(defn walk-and-mutate
"Walk the AST and generate all possible mutations"
[zloc]
(loop [loc zloc
mutations []]
(if (rz/end? loc)
mutations
(let [new-mutations (or (generate-all-mutations-for-loc loc) [])]
(recur (rz/next loc) (into mutations new-mutations))))))
(defn generate-mutations
"Generate all mutations for a function given its fully-qualified name"
[fq-name]
(binding [*ns* (find-ns (symbol (namespace fq-name)))]
(when-let [fn-info (find-function-source fq-name)]
(when-let [fn-source (read-function-from-file fn-info)]
(let [zloc (rz/of-string fn-source)
mutations (walk-and-mutate zloc)]
{:function fq-name
:original-source fn-source
:mutations mutations
:count (count mutations)})))))
(defn- reduced-if [f p?]
(fn [& args]
(let [r (apply f args)]
(if (p? r)
(reduced r)
r))))
(defn test-mutations
"Test all mutations of a function with specific tests.
Returns a map with:
- :killed - mutations that caused test failures
- :survived - mutations that passed all tests
- :total - total number of mutations
- :kill-rate - percentage of mutations killed"
[fq-name test-names]
(when-let [mutation-data (generate-mutations fq-name)]
(let [ns-sym (symbol (namespace fq-name))
var-obj (find-var fq-name)
original-fn @var-obj
original-meta (meta var-obj)
results (atom {:killed [] :survived []})]
;; Test each mutation
(doseq [[idx mutation] (map-indexed vector (:mutations mutation-data))]
(try
;; Evaluate the mutated function to replace the original
(binding [*ns* (find-ns ns-sym)]
(eval (read-string (:mutation mutation))))
;; Run tests until one fails (early termination)
(let [result (loop [test-names test-names]
(cond
(empty? test-names)
:survived
(= :killed (test-results (first test-names)))
:killed
:else
(recur (rest test-names))))]
(swap! results update result conj mutation))
(catch Exception _e
;; Mutation caused compilation/runtime error - count as killed
(swap! results update :killed conj mutation))
(finally
;; Restore the original function AND var metadata
(alter-var-root var-obj (constantly original-fn))
(alter-meta! var-obj (constantly original-meta)))))
(assoc @results :original-source (:original-source mutation-data)))))
(defn- index-by [f coll]
(into {} (for [v coll] [(f v) v])))
(defn test-namespace [target-ns test-ns-s]
(let [coverage (ns-coverage [target-ns] test-ns-s)
covered (into {}
(for [[f tests] (:coverage coverage)
:let [results (test-mutations f tests)]]
[f (assoc results :function f :tests tests)]))
uncovered (into {}
(for [f (:uncovered-fns coverage)
:let [fn-info (find-function-source f)
source (when fn-info (read-function-from-file fn-info))]]
[f {:function f :original-source source}]))]
(merge uncovered covered)))
(defonce state (atom nil))
(defn init! [target-ns test-ns-s]
(reset! state (test-namespace target-ns test-ns-s)))
(defn add-test [fq-name test-name]
(swap! state update-in [fq-name :tests] (fnil conj #{}) test-name))
(defn retest-function! [fq-name]
(let [entry (get @state fq-name)
test-names (:tests entry)
results (test-mutations fq-name test-names)
new-entry (assoc results :function fq-name :tests test-names)]
(swap! state assoc fq-name new-entry)
results))
(defn generate-report [target-ns test-ns-s out-file]
(let [results (test-namespace target-ns test-ns-s)
fns (vals results)
uncovered (sort-by :function (filter #(not (contains? % :tests)) fns))
partially-covered (sort-by :function (filter #(seq (:survived %)) fns))
fully-covered (sort-by :function (filter #(and (contains? % :tests)
(empty? (:survived %))) fns))
s #_:clj-kondo/ignore ;; it's wrapped in `with-out-str`
(with-out-str
(println "# Mutation Testing Namespace Report")
(println)
(println "Namespace:" target-ns)
(println)
(println "Test namespaces:" (str/join ", " (sort test-ns-s)))
(when (seq uncovered)
(println)
(println "## Uncovered Functions")
(println)
(println "These functions are never executed when the tests are run.")
(doseq [{:keys [function original-source]} uncovered]
(println)
(println "###" function)
(println)
(println "```")
(println original-source)
(println "```")))
(when (seq partially-covered)
(println)
(println "## Partially Covered Functions")
(println)
(println "These functions are executed but have mutations that survive.")
(doseq [{:keys [function survived original-source]} partially-covered]
(println)
(println "###" function)
(println)
(println "Original code")
(println)
(println "```")
(println original-source)
(println "```")
(doseq [{:keys [description mutation]} survived]
(println)
(println "#### Mutation:" description)
(println)
(println "```")
(println mutation)
(println "```"))))
(when (seq fully-covered)
(println)
(println "## Fully Covered Functions")
(println)
(println "These functions are fully covered by the test namespaces.")
(doseq [{:keys [original-source function]} fully-covered]
(println)
(println "###" function)
(println)
(println "Original code")
(println)
(println "```")
(println original-source)
(println "```"))))]
(spit out-file s)))
(comment
;; Step 1: Manually specify namespaces instead of using discovery
(def target-ns 'metabase.lib.card)
(def test-ns 'metabase.lib.card-test)
(def results (test-namespace target-ns [test-ns]))
(generate-report 'metabase.lib.card ['metabase.lib.card-test] "mutation-testing-report.lib.card.md")
(clojure.pprint/pprint results)
(:fully-covered results)
(println (:uncovered-fns results))
(first (:partially-covered results))
(metabase.util.malli.fn/instrument-ns? *ns*)
(doseq [{:keys [function survived]} (vals (:partially-covered results))]
(println)
(println)
(println "#" function)
(doseq [{:keys [description mutation]} survived]
(println)
(println "## Description:" description)
(println)
(println "```")
(println mutation)
(println "```")))
;; Load them
(require target-ns :reload)
(require test-ns :reload)
(def c (ns-coverage [target-ns] [test-ns]))
(clojure.pprint/pprint c)
(contains? (:all-fns c) 'metabase.queries.models.card/has-temporal-dimension?)
(metabase.lib.card/source-card-is-model? {:id 1})
(generate-mutations 'metabase.lib.card/source-card-is-model?)
(set (map long (map :hits c)))
(doseq [l
(take 10 c)]
(prn l))
(def fs (split-covered-functions c target-ns))
(pr-str (namespace (rz/string (rz/of-string "::jj/k"))))
(count (:hit fs))
;; Generate mutations for a function
(def mutations (generate-mutations 'metabase.queries.models.card/some-function-name))
;; See how many mutations were generated
(:count mutations)
;; Look at the mutations
(doseq [m (:mutations mutations)]
(println (:description m)))
(generate-mutations 'metabase.queries.models.card/source-card-id)
;; Get tests that call a specific function
(def fn-name 'metabase.queries.models.card/source-card-id)
(def tests-for-fn
(->> (:coverage c)
(filter (fn [[fn test]] (= fn fn-name)))
(map second)
vec))
;; Test all mutations with those tests
(def mutation-results (test-mutations fn-name tests-for-fn))
;; See the results
(println "Total mutations:" (:total mutation-results))
(println "Killed:" (:killed-count mutation-results))
(println "Survived:" (:survived-count mutation-results))
(println "Kill rate:" (* 100 (:kill-rate mutation-results)) "%")
(test-results 'metabase.lib.card-test/source-card-is-model?-test)
(t/run-test 'metabase.lib.card-test/source-card-is-model?-test)
;; Look at mutations that survived (potential test gaps)
(doseq [m (:survived mutation-results)]
(println "SURVIVED:" (:description m))))
(comment
(def results (test-namespace 'metabase.lib.stage '[metabase.lib.stage-test]))
(test-mutations 'metabase.lib.card/source-card-is-model? #{'metabase.lib.card-test/source-card-is-model?-test})
(binding [*ns* (find-ns 'metabase.lib.card)]
(eval (read-string "(mu/defn source-card-is-model? :- :boolean\n \"Is the query's source-card a model?\"\n [query :- ::lib.schema/query]\n (= (source-card-type query) :model__))"))))

View File

@@ -1,5 +1,6 @@
(ns dev.h2
(:require
[clojure.tools.cli :as cli]
[environ.core :as env]
[metabase.app-db.data-source :as mdb.data-source]
[metabase.app-db.env :as mdb.env])
@@ -24,9 +25,51 @@
(def ^:private tcp-listener (atom nil))
(defn tcp-listen
"Starts a TCP server for the H2 app-db database on port 9092."
[]
(when @tcp-listener
(throw (ex-info "TCP listener is already running" {:port 9092})))
(reset! tcp-listener (.start (Server/createTcpServer (into-array ["-tcp" "-tcpAllowOthers" "-tcpPort" "9092"]))))
(println (str "H2 TCP server started (no username or password): jdbc:h2:tcp://localhost:9092/" (#'metabase.app-db.env/env->db-file metabase.app-db.env/env))))
"Starts a TCP server for the H2 app-db database on the specified port."
[port]
(when-not (= "h2" (:mb-db-type env/env))
(throw (ex-info "The database type is not H2" {:db-type (:mb-db-type env/env)})))
(when-let [listener @tcp-listener]
(throw (ex-info "TCP listener is already running" {:port (:port listener)})))
(let [server (.start (Server/createTcpServer (into-array ["-tcp" "-tcpAllowOthers" "-tcpPort" (str port)])))]
(reset! tcp-listener {:server server :port port})
(println (str "H2 TCP server started (no username or password): jdbc:h2:tcp://localhost:" port "/"
(#'metabase.app-db.env/env->db-file metabase.app-db.env/env)))))
(defn connect-tcp
"Open an H2 shell connected to a TCP listener."
[{:keys [port db-file]}]
(let [url (str "jdbc:h2:tcp://localhost:" port "/" db-file)]
(println "Connecting to database at URL" url)
(org.h2.tools.Shell/main (into-array String ["-url" url]))))
(def ^:private connect-cli-spec
[["-h" "--help" "Show this help text"]
["-p" "--port PORT" "H2 TCP listener port"
:parse-fn #(Integer/parseInt %)]
["-f" "--db-file PATH" "H2 database file path"]])
(defn -main
"CLI entry point for connecting to an H2 TCP listener."
[& args]
(let [{:keys [options errors summary]} (cli/parse-opts args connect-cli-spec)
{:keys [help port db-file]} options]
(when help
#_:clj-kondo/ignore
(do
(println "Usage: clojure -M:connect-to-h2-tcp --port PORT --db-file PATH")
(println "Options:")
(println summary))
(System/exit 0))
(when (seq errors)
#_:clj-kondo/ignore
(doseq [error errors]
(println error))
(System/exit 1))
(when-not (and port db-file)
#_:clj-kondo/ignore
(do
(println "Both --port and --db-file are required.")
(println "Usage: clojure -M:connect-to-h2-tcp --port PORT --db-file PATH"))
(System/exit 1))
(connect-tcp {:port port :db-file db-file})))

View File

@@ -306,6 +306,7 @@
{:current-user-id api/*current-user-id*
:current-user-perms @api/*current-user-permissions-set*
:is-superuser? api/*is-superuser?*
:is-data-analyst api/*is-data-analyst?*
:is-impersonated-user? (perms-util/impersonated-user?)
:is-sandboxed-user? (perms-util/impersonated-user?)
:archived false

View File

@@ -64,6 +64,8 @@
(def cli-spec [["-h" "--help" "Show this help text"]
["-H" "--hot" "Enable hot reloading"] ;
[nil "--h2-tcp-port PORT" "Start an H2 TCP listener on the specified port"
:parse-fn #(Integer/parseInt %)]
["-p" "--port PORT" "Port to run the nREPL server on"
:default 50605
:parse-fn #(Integer/parseInt %)]])
@@ -75,9 +77,10 @@
Command Line Args:
`--hot` - Checks for modified files and reloads them during a request."
`--hot` - Checks for modified files and reloads them during a request.
`--h2-tcp-port` - Starts an H2 TCP listener on the specified port."
[& args]
(let [{:keys [help hot port]} (:options (cli/parse-opts args cli-spec))]
(let [{:keys [help hot h2-tcp-port port]} (:options (cli/parse-opts args cli-spec))]
(when help
#_:clj-kondo/ignore
(do
@@ -89,6 +92,8 @@
#_:clj-kondo/ignore
(println "Enabling hot reloading of code. Backend code will reload on every request.")
(alter-var-root #'*enable-hot-reload* (constantly true)))
(when h2-tcp-port
((requiring-resolve 'dev.h2/tcp-listen) h2-tcp-port))
(future
#_:clj-kondo/ignore
(println "Starting Metabase cider repl on port" port)

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ Since we host your Metabase for you, you may need to add our IP addresses to the
Here are the IP addresses we use in each region.
## United States (North Virginia)
## United States (North Virginia) - `us-east-1`
```sh
18.207.81.126
@@ -24,7 +24,7 @@ Here are the IP addresses we use in each region.
50.17.234.169
```
## Europe (Frankfurt)
## Europe (Frankfurt) - `eu-central-1`
```sh
18.192.2.142
@@ -32,7 +32,7 @@ Here are the IP addresses we use in each region.
3.65.184.173
```
## Asia Pacific (Singapore)
## Asia Pacific (Singapore) - `ap-southeast-1`
```sh
54.179.42.215
@@ -40,7 +40,7 @@ Here are the IP addresses we use in each region.
18.139.48.211
```
## Asia Pacific (Sydney)
## Asia Pacific (Sydney) - `ap-southeast-2`
```sh
13.238.131.1
@@ -48,7 +48,7 @@ Here are the IP addresses we use in each region.
54.252.39.14
```
## South America (São Paulo)
## South America (São Paulo) - `sa-east-1`
```sh
18.228.120.123

View File

@@ -224,6 +224,7 @@ config:
remote-sync-auto-import-rate: 5
remote-sync-check-changes-cache-ttl-seconds: 60
remote-sync-task-time-limit-ms: 300000
remote-sync-transforms: false
report-timezone: null
reset-token-ttl-hours: 48
retry-initial-interval: 500
@@ -271,6 +272,12 @@ config:
site-url: null
slack-app-token: null
slack-bug-report-channel: metabase-bugs
slack-connect-attribute-team-id: https://slack.com/team_id
slack-connect-authentication-mode: sso
slack-connect-client-id: null
slack-connect-client-secret: null
slack-connect-enabled: false
slack-connect-user-provisioning-enabled: true
smtp-override-enabled: false
source-address-header: X-Forwarded-For
sql-jdbc-fetch-size: 500

View File

@@ -107,7 +107,7 @@ Allowed iframe hosts.
- Default: `true`
- [Configuration file name](./config-file.md): `anon-tracking-enabled`
Enable the collection of anonymous usage data in order to help Metabase improve.
Enable the collection of anonymous usage data in order to help us improve.
### `MB_API_KEY`
@@ -1385,6 +1385,14 @@ Time-to-live in seconds for the remote changes check cache. Default is 60 second
The maximum amount of time a remote sync task will be given to complete.
### `MB_REMOTE_SYNC_TRANSFORMS`
- Type: boolean
- Default: `false`
- [Configuration file name](./config-file.md): `remote-sync-transforms`
Whether to sync transforms via remote-sync. When enabled, all transforms, transform tags, and transform jobs are synced as a single unit (all-or-nothing).
### `MB_REPORT_TIMEZONE`
- Type: string
@@ -1831,6 +1839,66 @@ Bot user OAuth token for connecting the Metabase Slack app. This should be used
The name of the channel where bug reports should be posted.
### `MB_SLACK_CONNECT_ATTRIBUTE_TEAM_ID`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `https://slack.com/team_id`
- [Configuration file name](./config-file.md): `slack-connect-attribute-team-id`
Slack OIDC claim for the team/workspace ID.
### `MB_SLACK_CONNECT_AUTHENTICATION_MODE`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `sso`
- [Configuration file name](./config-file.md): `slack-connect-authentication-mode`
Controls whether Slack can be used for SSO login or just account linking. Valid values: "sso" (default) or "link-only".
### `MB_SLACK_CONNECT_CLIENT_ID`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `slack-connect-client-id`
Client ID for your Slack app. Get this from https://api.slack.com/apps.
### `MB_SLACK_CONNECT_CLIENT_SECRET`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `slack-connect-client-secret`
Client Secret for your Slack app.
### `MB_SLACK_CONNECT_ENABLED`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: boolean
- Default: `false`
- [Configuration file name](./config-file.md): `slack-connect-enabled`
Is Slack Connect authentication configured and enabled?
### `MB_SLACK_CONNECT_USER_PROVISIONING_ENABLED`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: boolean
- Default: `true`
- [Configuration file name](./config-file.md): `slack-connect-user-provisioning-enabled`
When a user logs in via Slack Connect, create a Metabase account for them automatically if they don't have one.
### `MB_SLACK_FILES_CHANNEL [DEPRECATED]`
> DEPRECATED: 0.54.0

View File

@@ -33,6 +33,8 @@ This runs both the [frontend](#frontend) and [backend](#backend). Alternatively,
To use any other database beside the default ones please take a look at [Building Drivers](#building-drivers) further down in this document.
To skip the setup wizard and pre-configure users, database connections, and settings, add a [Metabase config file](https://www.metabase.com/docs/latest/configuring-metabase/config-file) to the directory where you run Metabase.
### Frontend
Metabase depends on third-party libraries to run, so you'll need to keep those up to date. The Clojure CLI will automatically fetch the dependencies when needed. With JavaScript dependencies, however, you'll need to kick off the installation process manually.

View File

@@ -6,6 +6,8 @@ title: Driver interface changelog
## Metabase 0.59.0
- Added `metabase.driver/llm-sql-dialect-resource` multimethod. Returns the resource path for dialect-specific LLM prompt instructions, or nil if no dialect-specific instructions exist for this driver.
- Added `sql-jdbc.execute/db-type-name` multimethod. Override this if something more than the default is needed in your sql-jdbc-based driver. See the `:mysql` implementation as an example.
## Metabase 0.58.0

View File

@@ -138,6 +138,8 @@ Please note that some users experienced Mongo connection timeouts even with this
### Running tests that depend on Docker images
Due to the requirement to use privileged ports for some tests in hosted environments, it is not possible to utilize **podman** or the **rootless Docker**. Stick to the classic Docker, or use OrbStack.
A large portion of our tests depend on the external services that are available through the Docker images. At the time of this writing, those are the three supported external QA databases, Webmail, Snowplow and LDAP servers. See `e2e/test/scenarios/docker-compose.yml` for up to date information. The default cypress command will spin up all necessary Docker containers for the tests to function properly. You can manually set up the e2e environment without them but be aware that you will run into test failures.
### Running tests with Snowplow involved

View File

@@ -20,8 +20,9 @@ In cases where types are only used by some local components, they should be defi
## Redux
We use Redux for global state. You will find domain-specific actions, reducers, and selectors generally grouped with the components that use them. e.g:
- [query_builder](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/query_builder)
- [dashboard](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/dashboard)
- [query_builder](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/query_builder)
- [dashboard](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/dashboard)
Use Redux global state as little as possible, and wherever possible, prefer local component state, or narrowly-defined context.
@@ -40,6 +41,7 @@ Our UI library in `metabase/ui` is built on top of [`Mantine`](https://mantine.d
## Styling
You'll note several styling patterns in the codebase. Currently you should prefer
1. [Mantine Style Props](https://mantine.dev/styles/style-props/) for most simple styling
2. [CSS Modules](https://github.com/css-modules/css-modules) for more complex styling
@@ -49,7 +51,7 @@ Familiarize yourself with Mantine's Layout components. You can often save a lot
## Colors
You may not define new bespoke colors or use any color literals like `black` or `white`. Colors can be referenced directly in components using mantine color props like `c="text-primary"` or in css modules using variables `color: var(--mb-color-text-primary);` Using these colors ensures consistent visual design and user experience across both dark and light modes. The full palette is defined in `metabase/lib/colors.ts`.
Colors should only be used in the form of mantine color props (primarily `c` and `bg`), or in css modules using variables eg: `color: var(--mb-color-text-primary);`. Using these colors ensures consistent visual design and user experience across both dark and light modes. Literal color values such as `black` or `#FFF` are not allowed, as well as using `color-mix` in your CSS modules to adjust the color or transparency of a variable. The full list of keys can be found in `frontend/src/metabase/lib/colors/types/color-keys.ts`, with their light and dark values found in `frontend/src/metabase/lib/colors/constants/themes`. If you find yourself requiring a color that does not already exist but is present in designs, reach out to the design team for guidance.
## Unit testing
@@ -170,7 +172,6 @@ const output = t`${name} is going to the ${place} with ${anotherName}`;
// 😍
const output = c("{0} and {2} are people's names, and {1} is a place")
.t`${name} is going to the ${place} with ${anotherName}`;
```
## Style Guide
@@ -213,6 +214,7 @@ render () {
);
}
```
- Avoid complex logical expressions inside of if statements. Often extracting logic to a well-named boolean variable can make code much easier to read.
```javascript
@@ -228,6 +230,7 @@ if (isMultilineText) {
// ...
}
```
- Use ALL_CAPS for constants
```javascript
@@ -240,6 +243,7 @@ const OBJECT_CONFIG_CONSTANT = {
abc: 123,
};
```
- Avoid magic strings and numbers
```javascript
@@ -250,7 +254,9 @@ const options = _.times(10, () => ...);
export const MAX_NUM_OPTIONS = 10;
const options = _.times(MAX_NUM_OPTIONS, () => ...);
```
- prefer declarative over imperative patterns where possible. You should write code with other engineers in mind as other engineers will spend more time reading than you spend writing (and re-writing). Code is more readable when it tells the computer "what to do" versus "how to do." Avoid imperative patterns like for loops:
```javascript
// don't do this
let foo = [];

View File

@@ -280,3 +280,33 @@ app.get("/sso/metabase", async (req, res) => {
}
});
```
## Embedding Metabase in a different domain
{% include plans-blockquote.html feature="Authenticated modular embeds" convert_pro_link_to_embbedding=true is_plural=true %}
This section applies only to **authenticated embeds**. Guest embeds work cross-domain without additional configuration.
If you want to embed Metabase in another domain (say, if Metabase is hosted at `metabase.yourcompany.com`, but you want to embed Metabase at `yourcompany.github.io`), you'll need to [allow your domain in CORS](#allow-your-domain-in-cors).
### Allow your domain in CORS
Go to **Admin settings** > **Embedding** > **Modular embedding** and add your embedding domain under **Cross-Origin Resource Sharing (CORS)** (such as `https://*.example.com`).
### Configure session cookies when testing locally
When you use `useExistingUserSession: true` during development on a different domain, the browser must send the existing Metabase session cookie cross-origin into the iframe. To allow this, you'll need to set the session cookie's SameSite value to "none".
You can set session cookie's SameSite value in **Admin settings** > **Embedding** > **Security** > **SameSite cookie setting**.
SameSite values include:
- **Lax** (default): Allows Metabase session cookies to be shared on the same domain. Used for production instances on the same domain.
- **None (requires HTTPS)**: Use "None" when your app and Metabase are hosted on different domains. Incompatible with Safari and iOS-based browsers.
- **Strict** (not recommended): Does not allow Metabase session cookies to be shared with embedded instances. Use this if you do not want to enable session sharing with embedding.
You can also set the [`MB_SESSION_COOKIE_SAMESITE` environment variable](../configuring-metabase/environment-variables.md#mb_session_cookie_samesite).
If you're using Safari, you'll need to [allow cross-site tracking](https://support.apple.com/en-tj/guide/safari/sfri40732/mac). Depending on the browser, you may also run into issues when viewing embedded items in private/incognito tabs.
Learn more about [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite).

View File

@@ -15,8 +15,12 @@ To restrict data in guest embeds for specific people or groups, use [locked para
## Turning on guest embedding in Metabase
1. Go to **Settings > Admin settings > Embedding > Guest embeds**.
2. Toggle **Enable guest embedding**.
The path to embedding settings depends on your Metabase version:
- **OSS**: **Admin settings > Embedding**
- **Starter/Pro/Enterprise**: **Admin settings > Embedding > Guest embeds**
Toggle **Enable guest embeds**.
## Creating a guest embed
@@ -240,14 +244,14 @@ The end user won't see the "category" filter, but the dashboard will only show d
## Disabling embedding for a question or dashboard
You can find a list of all guest embeds of questions and dashboards from **Admin settings** > **Embedding** > **Guest embeds**.
1. Visit the embeddable question or dashboard.
2. Click the **sharing icon** (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Guest embedding**
5. Click **Unpublish**.
Admins can find a list of embedded items in **Admin settings > Embedding** (on Pro and Enterprise plans, check the **Guest embeds** tab).
## Removing the "Powered by Metabase" banner
![Powered by Metabase](./images/powered-by-metabase.png)
@@ -258,7 +262,7 @@ The banner appears on guest embeds created with Metabase's open-source version.
Your embedding secret key is used to sign JWTs for all of your embeds.
1. Go to **Admin** > **Embedding** > **Guest embeds**.
1. Go to **Admin settings > Embedding**. On Pro and Enterprise plans, check the **Guest embeds** tab.
2. Under **Regenerate secret key**, click **Regenerate key**.
This key is shared across all guest embeds. Whoever has access to this key could get access to all embedded artifacts, so keep this key secure. If you regenerate this key, you'll need to update your server code with the new key.

View File

@@ -46,9 +46,9 @@ With SSO, Metabase can know who's viewing what, which unlocks a lot of power. Yo
### Guest embedding
[Guest embeds](./guest-embedding.md) are a secure way to embed charts and dashboards.
[Guest embeds](./guest-embedding.md) are a secure way to embed charts and dashboards. Guest embedding works on all Metabase plans, including OSS and Starter.
**When to use guest embeds**: simple embedding use cases where you don't want to offer ad-hoc querying or chart drill-through. To filter data relevant to the viewer, you can use guest embeds with [locked parameters](./static-embedding-parameters.md#locked-parameters-limit-the-values-available-to-other-editable-parameters).
**When to use guest embeds**: simple embedding use cases where you don't want to offer ad-hoc querying or chart drill-through. To filter data relevant to the viewer, you can use guest embeds with [locked parameters](./guest-embedding.md#locked-parameters).
## Public links and embeds

View File

@@ -15,12 +15,19 @@ Modular embedding lets you embed and customize Metabase [components](./component
If you're using React, check out the [Modular embedding SDK](./sdk/introduction.md).
> **Want guest embeds?** For modular embedding without authentication (available on all plans), see [Guest Embeds](./guest-embedding.md).
## Enable modular embedding
1. In Metabase, go to **Admin settings > Embedding**.
2. Toggle on **Enable modular embedding**.
**For guest embeds**: You're done! Skip to [Create a new embed](#create-a-new-embed) or see the [Guest embedding guide](./guest-embedding.md) for detailed setup instructions.
**For authenticated embeds** (Pro/Enterprise only), some more steps:
3. Under **Cross-Origin Resource Sharing (CORS)**, add the URLs of the websites where you want to embed Metabase (such as `https://*.example.com`). For testing embeds, you can use `localhost` which is always included in CORS policy.
4. If you're embedding Metabase components in a domain that's different from your Metabase's domain (including when you're testing the app locally but use Metabase Cloud), go to **Admin settings > Embedding > Security** and set **SameSite cookie** to **None**.
4. If embedding on a different domain, see [Embedding Metabase in a different domain](./authentication.md#embedding-metabase-in-a-different-domain).
## Create a new embed
@@ -46,7 +53,9 @@ You can also go to **Admin settings > Embedding > Modular embedding** and click
{% include plans-blockquote.html feature="Authenticated modular embedding" convert_pro_link_to_embbedding=true %}
How you authenticate the embed determines how cool the embed can get. This page covers the SSO setup, which lets you do everything. This setup requires you to have [set up SSO for your Metabase](./authentication.md). You can check out a [comparison between SSO and guest](./introduction.md#comparison-of-embedding-types), or jump straight to the [guest embed docs](./guest-embedding.md). But we recommend setting up modular embedding with SSO.
With SSO, Metabase can know who is viewing the embed, and it can unlock all of its bells and whistles (see this [comparison between SSO and guest embeds](./introduction.md#comparison-of-embedding-types)).
This page covers the [SSO setup for your Metabase](./authentication.md). If you don't need to set up SSO, check out the [guest embed docs](./guest-embedding.md).
### 3. Customize your embed
@@ -177,48 +186,4 @@ To define the configuration that applies to every embed on the page, use the `de
## Authentication
### Use API keys to test embeds
> API keys can only be used for testing embeds locally. To make your embeds production-ready or deploy them to another domain, you'll need to implement SSO.
To use an API key to test your embeds:
1. Create an [API key](../people-and-groups/api-keys.md)
2. Add `apiKey: "YOUR_API_KEY"` to `defineMetabaseConfig()`:
```html
<script>
defineMetabaseConfig({
instanceUrl: "https://your-metabase-url",
apiKey: "mb_hopeyouhaveaniceday",
});
</script>
```
API keys should only be used for testing with trusted people. Anyone with access to the front-end can grab the API key and use it to make requests against the Metabase API. For this reason, we only allow using API keys on localhost.
## Embedding Metabase in a different domain
{% include plans-blockquote.html feature="Authenticated modular embeds" convert_pro_link_to_embbedding=true is_plural=true %}
If you want to embed Metabase in another domain (say, if Metabase is hosted at `metabase.yourcompany.com`, but you want to embed Metabase at `yourcompany.github.io`), you can tell Metabase to set the session cookie's SameSite value to "none".
You can set session cookie's SameSite value in **Admin settings** > **Embedding** > **Security** > **SameSite cookie setting**.
SameSite values include:
- **Lax** (default): Allows Metabase session cookies to be shared on the same domain. Used for production instances on the same domain.
- **None (requires HTTPS)**: Use "None" when your app and Metabase are hosted on different domains. Incompatible with Safari and iOS-based browsers.
- **Strict** (not recommended): Does not allow Metabase session cookies to be shared with embedded instances. Use this if you do not want to enable session sharing with embedding.
You can also set the [`MB_SESSION_COOKIE_SAMESITE` environment variable](../configuring-metabase/environment-variables.md#mb_session_cookie_samesite).
If you're using Safari, you'll need to [allow cross-site tracking](https://support.apple.com/en-tj/guide/safari/sfri40732/mac). Depending on the browser, you may also run into issues when viewing embedded items in private/incognito tabs.
Learn more about [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite).
## Setting up SSO
{% include plans-blockquote.html feature="Authenticated modular embeds" convert_pro_link_to_embbedding=true is_plural=true %}
For production use, you'll need to set up SSO authentication. See [Modular embedding - authentication](./authentication.md) for details on setting up JWT or SAML authentication.
For authentication setup including API keys for local testing, SSO with JWT/SAML, and cross-domain configuration, see [Modular embedding - authentication](./authentication.md).

View File

@@ -14,6 +14,8 @@ What is embedding, and how does it work?
Embed individual dashboards, questions, or the query builder in your app with an interactive wizard and simple drop-in script, with minimal or no coding required. Control component UI and theming. Integrate your app's auth with Metabase SSO.
If you're on Metabase OSS or Starter, you can only embed components without SSO. See [Guest embeds](./guest-embedding.md).
### [Modular embedding SDK](./sdk/introduction.md)
With the Modular embedding SDK, you can embed individual Metabase components with React (like standalone charts, dashboards, the query builder, and more). You can manage access and interactivity per component, and you have advanced customization for seamless styling.
@@ -24,11 +26,7 @@ Jump to a SDK quickstart with a sample React application.
### [Guest embedding](./guest-embedding.md)
Guest embedding is a secure way to embed charts and dashboards. Guest embeds are view only; people won't be able to drill-through charts and tables.
### [Parameters for guest embeds](./static-embedding-parameters.md)
You can pass parameters between Metabase and your website via the embedding URL to specify how Metabase items should look and behave inside the iframe on your website.
Guest embedding is a secure way to embed charts and dashboards. Guest embeds are view-only; people won't be able to drill-through charts and tables.
### [Translating guest embeds](./translations.md)

View File

@@ -1,8 +1,8 @@
{
"rules": {
"no-unscoped-text-selectors": 2,
"metabase/no-unscoped-text-selectors": 2,
"import/no-commonjs": 0,
"no-color-literals": 0,
"metabase/no-color-literals": 0,
"no-console": 0,
"@typescript-eslint/no-namespace": "off",
"cypress/no-assigning-return-values": "error",
@@ -49,9 +49,9 @@
]
}
],
"no-direct-helper-import": 2,
"no-unsafe-element-filtering": ["warn"],
"no-unordered-test-helpers": 2
"metabase/no-direct-helper-import": 2,
"metabase/no-unsafe-element-filtering": ["warn"],
"metabase/no-unordered-test-helpers": 2
},
"env": {
"cypress/globals": true,

View File

@@ -17,6 +17,9 @@ const options = {
GENERATE_SNAPSHOTS: true,
JAR_PATH: undefined,
...booleanify(process.env),
// If this token is present in your env when Cypress runs, it's a mistake.
// Cypress needs to start from a clean slate in order to set the token programmatically.
MB_PREMIUM_EMBEDDING_TOKEN: undefined,
};
process.env = unBooleanify(options);
@@ -135,9 +138,11 @@ const cleanup = async (exitCode: string | number = SUCCESS_EXIT_CODE) => {
"🧹 Containers are running in background. If you wish to stop them, run:\n`docker compose -f ./e2e/test/scenarios/docker-compose.yml down`",
);
typeof exitCode === "number"
? process.exit(exitCode)
: process.exit(SUCCESS_EXIT_CODE);
if (typeof exitCode === "number") {
process.exit(exitCode);
} else {
process.exit(SUCCESS_EXIT_CODE);
}
};
init()

View File

@@ -125,22 +125,22 @@ describe("snapshots", () => {
// groups
cy.request("POST", "/api/permissions/group", { name: "collection" }).then(
({ body }) => {
expect(body.id).to.eq(COLLECTION_GROUP); // 4
expect(body.id).to.eq(COLLECTION_GROUP); // 5
},
);
cy.request("POST", "/api/permissions/group", { name: "data" }).then(
({ body }) => {
expect(body.id).to.eq(DATA_GROUP); // 5
expect(body.id).to.eq(DATA_GROUP); // 6
},
);
cy.request("POST", "/api/permissions/group", { name: "readonly" }).then(
({ body }) => {
expect(body.id).to.eq(READONLY_GROUP); // 6
expect(body.id).to.eq(READONLY_GROUP); // 7
},
);
cy.request("POST", "/api/permissions/group", { name: "nosql" }).then(
({ body }) => {
expect(body.id).to.eq(NOSQL_GROUP); // 7
expect(body.id).to.eq(NOSQL_GROUP); // 8
},
);

View File

@@ -1,7 +1,7 @@
import "cypress-iframe";
// this is the only place we allow direct helper import
// eslint-disable-next-line no-direct-helper-import
// eslint-disable-next-line metabase/no-direct-helper-import
import { H } from "e2e/support";
import "./commands/ui/button";

View File

@@ -23,7 +23,7 @@ export const loginCache: Partial<
> = {};
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
// eslint-disable-next-line @typescript-eslint/no-require-imports -- dynamic require for optional file
const sampleDataFile = require("e2e/support/cypress_sample_instance_data.json");
const { loginCache: LOGIN_CACHE } = sampleDataFile;

View File

@@ -46,6 +46,67 @@ const assetsResolverPlugin = {
},
};
// Plugin to allow loading the ClojureScript bundle only when it exists
// on the filesystem. This is to avoid breaking the build when the bundle
// is not present.
//
// If the bundle is not present, tests relying on it will fail, but the Cypress
// process will be allowed to start.
//
// The error will look like this:
//
// Dynamic require of "cljs/metabase.types.core" is not supported
//
// This allows us to avoid building the ClojureScript bundle when we don't need to (like in certain CI tasks).
const virtualClojureScriptPlugin = {
name: "virtualClojureScriptBundle",
setup(build) {
let cljsDir = null;
async function isDirectory(path) {
try {
const stat = await fs.promises.stat(path);
return stat.isDirectory();
} catch (e) {
return false;
}
}
build.onStart(async () => {
const baseDir = path.join(__dirname, "../../target");
const devDir = path.join(baseDir, "cljs_dev");
const prodDir = path.join(baseDir, "cljs_prod");
const [devDirExists, prodDirExists] = await Promise.all([
isDirectory(devDir),
isDirectory(prodDir),
]);
if (prodDirExists) {
console.log("ClojureScript production bundle found");
cljsDir = prodDir;
} else if (devDirExists) {
console.log("ClojureScript development bundle found");
cljsDir = devDir;
} else {
console.log("No ClojureScript bundle found, not loading bundle");
cljsDir = null;
}
});
build.onResolve({ filter: /^cljs\// }, async (args) => {
if (cljsDir === null) {
return {
path: args.path,
external: true,
};
}
const unnested = args.path.split(path.sep).slice(1).join(path.sep);
return { path: path.join(cljsDir, unnested + ".js") };
});
},
};
const defaultConfig = {
// This is the functionality of the old cypress-plugins.js file
setupNodeEvents(cypressOn, config) {
@@ -75,7 +136,11 @@ const defaultConfig = {
loader: {
".svg": "text",
},
plugins: [NodeModulesPolyfillPlugin(), assetsResolverPlugin],
plugins: [
NodeModulesPolyfillPlugin(),
assetsResolverPlugin,
virtualClojureScriptPlugin,
],
sourcemap: "inline",
}),
);

View File

@@ -43,14 +43,15 @@ export const SAMPLE_DB_TABLES = {
export const USER_GROUPS = {
ALL_USERS_GROUP: 1,
ADMIN_GROUP: 2,
COLLECTION_GROUP: 4,
DATA_GROUP: 5,
READONLY_GROUP: 6,
NOSQL_GROUP: 7,
COLLECTION_GROUP: 5,
DATA_GROUP: 6,
READONLY_GROUP: 7,
NOSQL_GROUP: 8,
};
export const TENANT_USER_GROUPS = {
export const MAGIC_USER_GROUPS = {
EXTERNAL_USERS_GROUP: 3,
DATA_ANALYSTS_GROUP: 4,
};
const {

View File

@@ -156,6 +156,10 @@ export const ALL_EXTERNAL_USERS_GROUP_ID = _.findWhere(
},
)?.id;
export const DATA_ANALYSTS_GROUP_ID = _.findWhere(SAMPLE_INSTANCE_DATA.groups, {
magic_group_type: "data-analyst",
})?.id;
export const COLLECTION_GROUP_ID = _.findWhere(SAMPLE_INSTANCE_DATA.groups, {
name: "collection",
}).id;

View File

@@ -87,8 +87,6 @@ export async function resetWritableDb({ type = "postgres" }) {
export async function resetTable({ type = "postgres", table = "testTable1" }) {
const dbClient = getDbClient(WRITABLE_DB_CONFIG[type]);
// eslint-disable-next-line import/namespace
return testTables?.[table]?.(dbClient);
}

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node
const jwt = require("jsonwebtoken");
const process = require("process");
const jwt = require("jsonwebtoken");
const payload = process.argv[2];
const METABASE_SECRET_KEY = process.argv[3];

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line no-direct-helper-import
// eslint-disable-next-line metabase/no-direct-helper-import
import { H } from "e2e/support";
export const tryToCreateApiKeyViaModal = ({

View File

@@ -1,7 +1,6 @@
import {
entityPickerModal,
entityPickerModalLevel,
entityPickerModalTab,
getFullName,
navigationSidebar,
popover,
@@ -44,7 +43,7 @@ export function getPersonalCollectionName(
}
export function openCollectionItemMenu(item: string, index = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findByTestId("collection-table")
.findAllByText(item)
.eq(index)
@@ -79,11 +78,12 @@ export const openUnpinnedItemMenu = (name: string) => {
};
export const moveOpenedCollectionTo = (newParent: string) => {
cy.intercept("GET", "/api/collection/*/items**").as("getCollectionItems");
openCollectionMenu();
popover().within(() => cy.findByText("Move").click());
cy.wait(["@getCollectionItems", "@getCollectionItems"]);
entityPickerModal().within(() => {
cy.findByRole("tab", { name: /Collections/ }).click();
cy.findByTestId("nested-item-picker").findByText(newParent).click();
cy.button("Move").click();
});
@@ -94,16 +94,10 @@ export const moveOpenedCollectionTo = (newParent: string) => {
export function pickEntity({
path,
select,
tab,
}: {
path?: string[];
path?: (string | RegExp)[];
select?: boolean;
tab?: string;
}) {
if (tab) {
entityPickerModalTab(tab).click();
}
if (path) {
cy.findByTestId("nested-item-picker").within(() => {
for (const [index, name] of path.entries()) {

View File

@@ -36,12 +36,12 @@ export function getDashboardCards() {
}
export function getDashboardCard(index = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return getDashboardCards().eq(index);
}
export function ensureDashboardCardHasText(text: string, index = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByTestId("dashcard").eq(index).should("contain", text);
}
@@ -370,6 +370,10 @@ export const dashboardGrid = () => {
return cy.findByTestId("dashboard-grid");
};
export function dashboardCancelButton() {
return cy.findByTestId("edit-bar").findByRole("button", { name: "Cancel" });
}
export function dashboardSaveButton() {
return cy.findByTestId("edit-bar").findByRole("button", { name: "Save" });
}

View File

@@ -2,6 +2,7 @@ import type { MeasureId, SegmentId, TableId } from "metabase-types/api";
import { codeMirrorHelpers } from "./e2e-codemirror-helpers";
import { popover } from "./e2e-ui-elements-helpers";
const { H } = cy;
const libraryPage = () => cy.findByTestId("library-page");
const newSnippetPage = () => cy.findByTestId("new-snippet-page");
@@ -22,6 +23,11 @@ export const DataStudio = {
settingsTab: () => DataStudio.Transforms.header().findByText("Settings"),
dependenciesTab: () =>
DataStudio.Transforms.header().findByText("Dependencies"),
visit: () => {
cy.visit("/data-studio/transforms");
DataStudio.Transforms.list().should("be.visible");
},
pythonResults: () => cy.findByTestId("python-results"),
},
Jobs: {
header: () => cy.findByTestId("jobs-header"),
@@ -38,6 +44,7 @@ export const DataStudio = {
},
PythonLibrary: {
header: () => cy.findByTestId("python-library-header"),
editor: () => cy.findByTestId("python-editor"),
},
Snippets: {
newPage: newSnippetPage,
@@ -111,6 +118,10 @@ export const DataStudio = {
.findByTestId("table-description-section")
.findByPlaceholderText("No description"),
},
openFilterPopover: () => {
cy.findByRole("button", { name: "Filter" }).click();
H.popover();
},
},
Library: {
visit: () => {
@@ -131,26 +142,4 @@ export const DataStudio = {
collectionItem: (name: string) =>
libraryPage().findAllByTestId("collection-name").contains(name),
},
Tasks: {
visitBrokenEntities: () => cy.visit("/data-studio/tasks/broken"),
visitUnreferencedEntities: () =>
cy.visit("/data-studio/tasks/unreferenced"),
list: () => cy.findByTestId("dependency-list"),
searchInput: () => cy.findByTestId("dependency-list-search-input"),
filterButton: () => cy.findByTestId("dependency-list-filter-button"),
sidebar: () => cy.findByTestId("dependency-list-sidebar"),
Sidebar: {
get: () => cy.findByTestId("dependency-list-sidebar"),
header: () => cy.findByTestId("dependency-list-sidebar-header"),
locationInfo: () => cy.findByRole("region", { name: "Location" }),
transformInfo: () => cy.findByRole("region", { name: "Transform" }),
missingColumnsInfo: () =>
cy.findByRole("region", { name: "Missing columns" }),
creationInfo: () =>
cy.findByRole("region", { name: "Creator and last editor" }),
brokenDependentsInfo: () =>
cy.findByRole("region", { name: "Broken dependents" }),
},
},
};

View File

@@ -5,6 +5,8 @@ import type {
TableId,
} from "metabase-types/api";
import { popover } from "./e2e-ui-elements-helpers";
export const DataModel = {
visit,
visitDataStudio,
@@ -15,14 +17,19 @@ export const DataModel = {
get: getTablePicker,
getDatabase: getTablePickerDatabase,
getDatabaseToggle: getTablePickerDatabaseToggle,
getDatabaseCheckbox,
getDatabases: getTablePickerDatabases,
getSchemas: getTablePickerSchemas,
getSchema: getTablePickerSchema,
getSchemaToggle: getTablePickerSchemaToggle,
getSchemaCheckbox,
getTables: getTablePickerTables,
getTable: getTablePickerTable,
getSearchInput: getTablePickerSearchInput,
getFilterForm: getTablePickerFilter,
openFilterPopover,
selectFilterOption,
applyFilters,
},
TableSection: {
get: getTableSection,
@@ -664,3 +671,26 @@ function getMeasureEditorDependenciesTab() {
function getMeasureRevisionHistory() {
return cy.findByTestId("measure-revision-history-page");
}
export function openFilterPopover() {
cy.findByRole("button", { name: "Filter" }).click();
popover();
}
export function selectFilterOption(fieldLabel: string, optionLabel: string) {
cy.findByRole("textbox", { name: fieldLabel }).click();
popover().contains(optionLabel).click();
}
export function applyFilters() {
cy.findByRole("button", { name: "Apply" }).click();
cy.wait("@listTables");
}
export function getDatabaseCheckbox(databaseName: string) {
return getTablePickerDatabase(databaseName).find('input[type="checkbox"]');
}
export function getSchemaCheckbox(schemaName: string) {
return getTablePickerSchema(schemaName).find('input[type="checkbox"]');
}

View File

@@ -1,7 +0,0 @@
export const DependencyGraph = {
graph: () => cy.findByTestId("dependency-graph"),
entryButton: () => cy.findByTestId("graph-entry-button"),
entrySearchInput: () => cy.findByTestId("graph-entry-search-input"),
selectionButton: () => cy.findByTestId("graph-selection-button"),
dependencyPanel: () => cy.findByTestId("graph-dependency-panel"),
};

View File

@@ -0,0 +1,60 @@
import type {
DependencyNode,
ListBreakingGraphNodesResponse,
} from "metabase-types/api";
export const DependencyGraph = {
graph: () => cy.findByTestId("dependency-graph"),
entryButton: () => cy.findByTestId("graph-entry-button"),
entrySearchInput: () => cy.findByTestId("graph-entry-search-input"),
selectionButton: () => cy.findByTestId("graph-selection-button"),
dependencyPanel: () => cy.findByTestId("graph-dependency-panel"),
};
export const DependencyDiagnostics = {
visitBrokenDependencies: () =>
cy.visit("/data-studio/dependency-diagnostics/broken"),
visitUnreferencedEntities: () =>
cy.visit("/data-studio/dependency-diagnostics/unreferenced"),
list: () => cy.findByTestId("dependency-list"),
searchInput: () => cy.findByTestId("dependency-list-search-input"),
filterButton: () => cy.findByTestId("dependency-filter-button"),
sidebar: () => cy.findByTestId("dependency-list-sidebar"),
Sidebar: {
get: () => cy.findByTestId("dependency-list-sidebar"),
header: () => cy.findByTestId("dependency-list-sidebar-header"),
locationSection: () => cy.findByRole("region", { name: "Location" }),
infoSection: () => cy.findByRole("region", { name: "Info" }),
errorsSection: (name: string) => cy.findByRole("region", { name }),
missingColumnsSection: () =>
cy.findByRole("region", { name: "Missing columns" }),
fieldsSection: () => cy.findByRole("region", { name: "Fields" }),
brokenDependentsSection: () =>
cy.findByRole("region", { name: "Broken dependents" }),
},
};
const WAIT_TIMEOUT = 10000;
const WAIT_INTERVAL = 100;
export function waitForBreakingDependencies(
filter: (nodes: DependencyNode[]) => boolean,
timeout = WAIT_TIMEOUT,
): Cypress.Chainable {
return cy
.request<ListBreakingGraphNodesResponse>(
"GET",
"/api/ee/dependencies/graph/breaking",
)
.then((response) => {
if (filter(response.body.data)) {
return cy.wrap(response);
} else if (timeout > 0) {
cy.wait(WAIT_INTERVAL);
return waitForBreakingDependencies(filter, timeout - WAIT_INTERVAL);
} else {
throw new Error("Dependency analysis retry timeout");
}
});
}

View File

@@ -9,7 +9,7 @@ export function getDimensions(isSelected) {
}
export function getDimensionByName({ name, index = 0, isSelected }) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return getDimensions(isSelected).filter(`:contains("${name}")`).eq(index);
}

View File

@@ -1,8 +1,6 @@
import {
entityPickerModal,
modal,
popover,
} from "e2e/support/helpers/e2e-ui-elements-helpers";
import { modal, popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
import { pickEntity } from "./e2e-collection-helpers";
export function saveInteractiveQuestionAsNewQuestion(options: {
questionName: string;
@@ -13,7 +11,7 @@ export function saveInteractiveQuestionAsNewQuestion(options: {
cy.intercept("POST", "/api/card").as("createCard");
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByTestId("cell-data").last().click();
popover().findByText(`See these ${entityName}`).click();
cy.findByRole("button", { name: "Save" }).click();
@@ -29,15 +27,7 @@ export function saveInteractiveQuestionAsNewQuestion(options: {
if (collectionPickerPath) {
cy.findByTestId("dashboard-and-collection-picker-button").click();
entityPickerModal().within(() => {
cy.findByText("Browse").click();
collectionPickerPath.forEach((collectionName) =>
cy.findByText(collectionName).click(),
);
cy.findByRole("button", { name: "Select this collection" }).click();
});
pickEntity({ path: collectionPickerPath, select: true });
}
modal().within(() => {

View File

@@ -1,8 +1,6 @@
import {
entityPickerModal,
modal,
popover,
} from "e2e/support/helpers/e2e-ui-elements-helpers";
import { modal, popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
import { pickEntity } from "./e2e-collection-helpers";
export function setDropdownFilterType() {
cy.findByText("Dropdown list").click();
@@ -20,9 +18,7 @@ export function setFilterQuestionSource({ question, field }) {
cy.findByText("Pick a model or question…").click();
});
entityPickerModal().within(() => {
cy.findByText(question).click();
});
pickEntity({ path: [/Our analytics/, question], select: true });
modal().within(() => {
cy.findByText("Pick a column…").click();

View File

@@ -73,7 +73,7 @@ export function chatMessages() {
}
export function lastChatMessage() {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return chatMessages().last();
}

View File

@@ -359,7 +359,7 @@ export function checkSavedToCollectionQuestionToast(addToDashboard) {
export function saveQuestionToCollection(
name,
pickEntityOptions = { tab: "Browse", path: ["Our analytics"] },
pickEntityOptions = { path: ["Our analytics"] },
reqInfo,
) {
saveQuestion(name, reqInfo, pickEntityOptions);

View File

@@ -2,7 +2,6 @@ import type { CyHttpMessages } from "cypress/types/net-stubbing";
import {
entityPickerModal,
entityPickerModalTab,
miniPicker,
miniPickerBrowseAll,
popover,
@@ -84,7 +83,7 @@ export function addSummaryField({
stage?: number;
index?: number;
}) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getNotebookStep("summarize", { stage, index })
.findByTestId("aggregate-step")
.findAllByTestId("notebook-cell-item")
@@ -118,7 +117,7 @@ export function addSummaryGroupingField({
index?: number;
bucketSize?: string;
}) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getNotebookStep("summarize", { stage, index })
.findByTestId("breakout-step")
.findAllByTestId("notebook-cell-item")
@@ -142,7 +141,7 @@ export function addSummaryGroupingField({
});
if (bucketSize) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
popover().last().findByText(bucketSize).click();
}
}
@@ -206,7 +205,10 @@ export function selectSavedQuestionsToJoin(
) {
cy.intercept("GET", "/api/table/*/query_metadata").as("joinedTableMetadata");
miniPickerBrowseAll().click();
entityPickerModal().findByText(firstQuestionName).click();
entityPickerModal().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(firstQuestionName).click();
});
cy.wait("@joinedTableMetadata");
@@ -214,8 +216,10 @@ export function selectSavedQuestionsToJoin(
cy.icon("join_left_outer").click();
miniPickerBrowseAll().click();
entityPickerModalTab("Data").click();
entityPickerModal().findByText(secondQuestionName).click();
entityPickerModal().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(secondQuestionName).click();
});
}
export function selectFilterOperator(operatorName: string) {
@@ -339,7 +343,7 @@ function verifyNotebookExpressions(
);
for (let index = 0; index < expressions.length; ++index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getExpressionItems(stageIndex)
.eq(index)
.should("have.text", expressions[index]);
@@ -360,7 +364,7 @@ function verifyNotebookFilters(
);
for (let index = 0; index < filters.length; ++index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getFilterItems(stageIndex).eq(index).should("have.text", filters[index]);
}
} else {
@@ -381,7 +385,7 @@ function verifyNotebookAggregations(
);
for (let index = 0; index < aggregations.length; ++index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getSummarizeItems(stageIndex, "aggregate")
.eq(index)
.should("have.text", aggregations[index]);
@@ -410,7 +414,7 @@ function verifyNotebookBreakouts(
);
for (let index = 0; index < breakouts.length; ++index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getSummarizeItems(stageIndex, "breakout")
.eq(index)
.should("have.text", breakouts[index]);
@@ -444,9 +448,9 @@ function verifyNotebookSort(
for (let index = 0; index < sort.length; ++index) {
const { column, order } = sort[index];
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getSortItems(stageIndex).eq(index).should("have.text", column);
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getSortItems(stageIndex)
.eq(index)
.icon(order === "asc" ? "arrow_up" : "arrow_down")

View File

@@ -33,12 +33,14 @@ export function modifyPermission(
}
});
}
value && cy.findByText(value).click();
if (value) {
cy.findByText(value).click();
}
});
}
export function selectPermissionRow(item, permissionIndex) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getPermissionRowPermissions(item).eq(permissionIndex).click();
}
@@ -80,19 +82,20 @@ export function assertPermissionForItem(
permissionColumnIndex,
permissionValue,
) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
getPermissionRowPermissions(item)
.eq(permissionColumnIndex)
.should("have.text", permissionValue);
}
/**
* @param {string} index
* @param {string} row
* @param {number} index
* @param {string} permission
* @param {boolean} isDisabled
*/
export function isPermissionDisabled(row, index, permission, isDisabled) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return getPermissionRowPermissions(row)
.eq(index)
.should("have.attr", "aria-disabled", isDisabled.toString())

View File

@@ -45,15 +45,49 @@ export const commitToRepo = (
export function configureGit(
syncType: "read-write" | "read-only",
syncUrl = LOCAL_GIT_PATH + "/.git",
collections?: Record<number, boolean>,
) {
cy.request("PUT", "/api/ee/remote-sync/settings", {
"remote-sync-branch": "main",
"remote-sync-type": syncType,
"remote-sync-url": syncUrl,
"remote-sync-enabled": true,
...(collections && { collections }),
});
}
// Setup remote sync via the API and wait for/trigger the initial import
export function configureGitAndPullChanges(
syncType: "read-write" | "read-only",
syncUrl = LOCAL_GIT_PATH + "/.git",
) {
configureGit(syncType, syncUrl);
if (syncType === "read-only") {
// Read-only mode automatically triggers an import, just wait for it
pollForTask({ taskName: "import" });
} else {
// Read-write mode needs manual import trigger
cy.request("POST", "/api/ee/remote-sync/import", {});
pollForTask({ taskName: "import" });
}
}
// Setup remote sync with a new synced collection in one step
export function configureGitWithNewSyncedCollection(
syncType: "read-write" | "read-only",
collectionName = "Test Synced Collection",
syncUrl = LOCAL_GIT_PATH + "/.git",
) {
return cy
.request("POST", "/api/collection", { name: collectionName })
.then((response) => {
const collection = response.body;
configureGit(syncType, syncUrl, { [collection.id]: true });
return cy.wrap(collection);
});
}
// Prepare the local git repo and initializing with an empty commit
export function setupGitSync() {
cy.exec("rm -rf " + LOCAL_GIT_PATH);
@@ -136,7 +170,10 @@ export const updateRemoteQuestion = (
});
};
export const moveCollectionItemToSyncedCollection = (name: string) => {
export const moveCollectionItemToSyncedCollection = (
name: string,
targetCollection = "Synced Collection",
) => {
navigationSidebar()
.findByRole("treeitem", { name: /Our analytics/ })
.click();
@@ -145,22 +182,24 @@ export const moveCollectionItemToSyncedCollection = (name: string) => {
popover().findByText("Move").click();
entityPickerModal().within(() => {
cy.findAllByRole("tab", { name: /Browse|Collections/ }).click();
entityPickerModalItem(1, "Synced Collection").click();
entityPickerModalItem(1, targetCollection).click();
cy.button("Move").click();
});
getSyncStatusIndicators().should("have.length", 1);
navigationSidebar()
.findByRole("treeitem", { name: /Synced Collection/ })
.findByRole("treeitem", { name: new RegExp(targetCollection) })
.click();
collectionTable().findByText(name).should("exist");
};
export const goToSyncedCollection = (opts?: Partial<Cypress.ClickOptions>) =>
export const goToSyncedCollection = (
collectionName = "Synced Collection",
opts?: Partial<Cypress.ClickOptions>,
) =>
navigationSidebar()
.findByRole("treeitem", { name: /Synced Collection/ })
.findByRole("treeitem", { name: new RegExp(collectionName) })
.click(opts);
// Git sync controls are now in the app bar, not the sidebar
@@ -222,3 +261,47 @@ export const waitForTask = (
}
});
};
// Poll for task completion by actively querying the endpoint
// Use this when the app isn't loaded yet (e.g., in setup helpers before cy.visit)
export const pollForTask = (
{ taskName }: { taskName: "import" | "export" },
retries = 0,
) => {
if (retries > 30) {
throw Error(`Too many retries waiting for ${taskName}`);
}
return cy
.request("GET", "/api/ee/remote-sync/current-task")
.then((response) => {
const { body } = response;
// No task exists yet, keep waiting
if (!body) {
cy.wait(500);
return pollForTask({ taskName }, retries + 1);
}
// Wrong task type, keep waiting
if (body.sync_task_type !== taskName) {
cy.wait(500);
return pollForTask({ taskName }, retries + 1);
}
// Task hasn't completed successfully yet
if (body.status !== "successful") {
// Check if it errored
if (body.status === "errored") {
throw Error(
`Task ${taskName} failed: ${body.error_message || "Unknown error"}`,
);
}
cy.wait(500);
return pollForTask({ taskName }, retries + 1);
}
// Success!
return cy.wrap(body);
});
};

View File

@@ -74,30 +74,14 @@ export function entityPickerModalLevel(level) {
* @param {string | RegExp} name
*/
export function entityPickerModalItem(level, name) {
return entityPickerModalLevel(level).findByText(name).parents("a");
}
export function entityPickerModalTab(name) {
return cy.findAllByRole("tab").filter(`:contains(${name})`);
}
// displays at least these tabs:
export function shouldDisplayTabs(tabs) {
tabs.forEach((tab) => {
entityPickerModalTab(tab).should("exist");
});
}
export function tabsShouldBe(selected, tabs) {
cy.log(tabs);
cy.findAllByRole("tab").should("have.length", tabs.length);
tabs.forEach((tab) => {
if (tab === selected) {
entityPickerModalTab(tab).and("have.attr", "aria-selected", "true");
} else {
entityPickerModalTab(tab).should("exist");
}
});
return (
entityPickerModalLevel(level)
// in the recents and search results, the items look like: [collection name] [parent collection name]
// which makes matching difficult as you may inadvertently match the parent collection name
// so we ignore the parent collection name by ignoring data-testid="picker-item-location"
.findByText(name, { ignore: '[data-testid="picker-item-location"]' })
.parents("a")
);
}
export function collectionOnTheGoModal() {
@@ -195,12 +179,12 @@ export function filterWidget({ isEditing = false, name = null } = {}) {
}
export function clearFilterWidget(index = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return filterWidget().eq(index).icon("close").click();
}
export function resetFilterWidgetToDefault(index = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return filterWidget().eq(index).icon("revert").click();
}
@@ -321,6 +305,8 @@ export const moveDnDKitListElement = (
{ startIndex, dropIndex, onBeforeDragEnd = () => {} } = {},
) => {
const selector = new RegExp(dataTestId);
const getElement = () =>
cy.findAllByTestId(selector).should("have.length.gt", 1).eq(startIndex);
const getCenter = ($el) => {
const { x, y, width, height } = $el.getBoundingClientRect();
@@ -334,9 +320,7 @@ export const moveDnDKitListElement = (
const dragPoint = getCenter(dragEl);
const dropPoint = getCenter(dropEl);
cy.wrap(dragEl).as("dragElement");
moveDnDKitElementByAlias("@dragElement", {
moveDnDKitElementByGetter(getElement, {
vertical: dropPoint.clientY - dragPoint.clientY,
horizontal: dropPoint.clientX - dragPoint.clientX,
onBeforeDragEnd,
@@ -353,29 +337,45 @@ export const moveDnDKitListElement = (
* @param {number} [options.vertical=0] - Vertical distance to move in pixels
* @param {Function} [options.onBeforeDragEnd] - Optional callback executed before releasing the drag
*/
export const moveDnDKitElementByAlias = (
alias,
{ horizontal = 0, vertical = 0, onBeforeDragEnd = () => {} } = {},
) => {
export const moveDnDKitElementByAlias = (alias, options) => {
// This function queries alias before triggering every event to avoid running into "element was removed from the DOM"
// error caused by node remounting https://on.cypress.io/element-has-detached-from-dom
cy.get(alias)
const getElement = () => cy.get(alias);
return moveDnDKitElementByGetter(getElement, options);
};
/**
* Moves a dnd-kit draggable element by a specified offset using a function that returns the element.
*
* @param {() => Cypress.Chainable} getElement - A function that returns the element to drag
* @param {Object} options
* @param {number} [options.horizontal=0] - Horizontal distance to move in pixels
* @param {number} [options.vertical=0] - Vertical distance to move in pixels
* @param {Function} [options.onBeforeDragEnd] - Optional callback executed before releasing the drag
*/
const moveDnDKitElementByGetter = (
getElement,
{ horizontal = 0, vertical = 0, onBeforeDragEnd = () => {} } = {},
) => {
getElement()
.trigger("pointerdown", 0, 0, {
force: true,
isPrimary: true,
button: 0,
})
.wait(200);
// This initial move needs to be greater than the activation constraint
// of the pointer sensor
cy.get(alias)
getElement()
.trigger("pointermove", 20, 20, {
force: true,
isPrimary: true,
button: 0,
})
.wait(200);
cy.get(alias)
getElement()
.trigger("pointermove", horizontal, vertical, {
force: true,
isPrimary: true,
@@ -439,7 +439,7 @@ export function tableInteractiveFooter() {
}
export function resizeTableColumn(columnId, moveX, elementIndex = 0) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByTestId(`resize-handle-${columnId}`)
.eq(elementIndex)
.trigger("mousedown", {
@@ -483,7 +483,7 @@ export function assertTableRowsCount(value) {
}
export function lastTableRow() {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return tableInteractiveScrollContainer()
.scrollTo("bottomLeft")
.findAllByRole("row")
@@ -547,7 +547,7 @@ export function assertTableData({ columns, firstRows = [] }) {
.should("have.length", columns.length);
columns.forEach((column, index) => {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
tableInteractive()
.findAllByTestId("header-cell")
.eq(index)
@@ -556,7 +556,7 @@ export function assertTableData({ columns, firstRows = [] }) {
firstRows.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
tableInteractiveBody()
.findAllByTestId("cell-data")
.eq(columns.length * rowIndex + cellIndex)
@@ -596,12 +596,12 @@ export function fieldValuesTextbox() {
}
export function fieldValuesValue(index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return cy.findAllByTestId("token-field").eq(index);
}
export function removeFieldValuesValue(index) {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return cy
.findAllByTestId("token-field")
.findAllByLabelText("Remove")
@@ -610,7 +610,7 @@ export function removeFieldValuesValue(index) {
}
export function multiAutocompleteValue(index, filter = ":eq(0)") {
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
return cy
.findAllByRole("combobox")
.filter(filter)
@@ -667,3 +667,35 @@ export function ensureParameterColumnValue({ columnName, columnValue }) {
});
});
}
export function getProfileLink() {
return cy.findByTestId("app-switcher-target");
}
export const mainAppLinkText = "Main app";
export const dataStudioAppLinkText = "Data studio";
export const adminAppLinkText = "Admin";
export function goToMainApp() {
getProfileLink().click();
popover().findByText(mainAppLinkText).click();
}
export function goToAdmin() {
getProfileLink().click();
popover().findByText(adminAppLinkText).click();
}
export function goToDataStudio() {
getProfileLink().click();
popover().findByText(dataStudioAppLinkText).click();
}
export function goToProfile() {
getProfileLink().click();
popover().findByTestId("mode-switcher-profile-link").click();
}
export function getHelpSubmenu() {
return cy.findByTestId("help-submenu");
}

View File

@@ -78,7 +78,7 @@ export function uploadFile(
cy.wait(`@${uploadMode}CSV`);
// eslint-disable-next-line no-unsafe-element-filtering
// eslint-disable-next-line metabase/no-unsafe-element-filtering
cy.findAllByRole("status")
.last()
.findByText(`Data added to ${collectionName}`, {

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