Compare commits

...

407 Commits

Author SHA1 Message Date
Eric Normand
b584b35cd9 Better tests 2025-12-01 15:19:51 -06:00
Eric Normand
a36cda7ffd Merge branch 'master' into representations 2025-12-01 15:19:00 -06:00
Cam Saul
7f68894e80 New deps graph tooling to power Mage test watching (#66345) 2025-12-01 19:53:40 +00:00
Eric Normand
151f143233 Whitespace 2025-12-01 13:42:15 -06:00
Eric Normand
e440239b79 Move question test 2025-12-01 13:40:24 -06:00
Eric Normand
0f41ae3925 Remove stray whitespace 2025-12-01 13:35:25 -06:00
Eric Normand
d4344c308e Use representations build from clojars 2025-12-01 13:33:17 -06:00
Ryan Laurie
b933496d37 Don't use entities for Icons (#65633)
* dont need entities for collections methinks

* entities don't handle icons anymore

* revert url change (for now)

* fix tests

* fix verified model icon

* consistent handling of verified model icons
2025-12-01 12:32:53 -07:00
Jeff Bruemmer
32d69c1a4f remove env vars that now have defsettings (#66434) 2025-12-01 19:23:55 +00:00
Alex Yarosh
7096051905 Docs: DBs for editable tables (#66276)
* Add Metabase Sample Database to editable tables support

Updated the list of databases that support editable tables to include Metabase Sample Database.

* Update docs/data-modeling/editable-tables.md

* Update docs/data-modeling/editable-tables.md
2025-12-01 14:07:10 -05:00
Nemanja Glumac
e88075f7d4 Fix snapshots for backport compatibility tests (#66433) 2025-12-01 19:50:28 +01:00
github-automation-metabase
13db04d7a0 Update generated docs (master) (#66300)
Co-authored-by: github-actions <github-actions@github.com>
2025-12-01 13:43:39 -05:00
Jeff Bruemmer
3ae80a41da don't run on draft prs (#66426) 2025-12-01 12:13:37 -05:00
Eric Normand
6414a9d73b Closed connection with many fields (#66051)
Fix legacy path (for snowflake with large number of fields)
2025-12-01 15:53:12 +00:00
Gabriel Moreno
19a79d96c1 Add gantoreno to team.json and github-slack-map.json (#66334)
* feat: add myself (gantoreno) to `team.json` and `github-slack-map.json`

* fix: correct username alphabetical order
2025-12-01 11:01:41 -04:00
Nemanja Glumac
637fa143ff Provide minimal sane defaults to the Cypress runner (#66398)
- If you decide to build the backend, you should run it
- There are no more derived options - everything is explicit
- Running Docker containers is a non-negotiable
    - Tests need these supporting services to run
    - If you really insist on not running them, either comment out those lines temporarily or do not use this script to set up the e2e environment
    - Because of this, there are no more custom envs `QA_DB_ENABLED` and `CYPRESS_QA_DB_MONGO`
- Alternative location (directory) for the e2e copy of the sample-application.db is now set to be `e2e/tmp` since it's already gitignored
2025-12-01 15:44:48 +01:00
Riley Thompson
8393eb2820 Use backfill specific event to prevent card revisions (#66401)
* use backfill specific event

* fix kondo

* add test

* fix config modules

* add migration

* remove migrations
2025-12-01 14:20:52 +00:00
Brad Anderson
e67f25b401 UXW-2209 Update datamodel tracking triggered_from (#66259)
* UXW-2209 Updates datamodel tracking triggered_from to be either "data_studio" or "admin" depending on where the metadata is being edited

* Updates e2e tests
2025-12-01 09:13:11 -05:00
Brad Anderson
39dfac795e UXW-2352 Fix issue where CopyCardForm resets when the destination collection is changed (#66275)
* UXW-2352 Fix issue where CopyCardForm resets when the destination collection is changed

* Unskips now-passing e2e tests

* Fix issue where Duplicate modal doesn't close on success
2025-12-01 09:12:42 -05:00
Egor Grushin
9bef191295 small ui fixes GDGT-1276 (#66406)
* fix GDGT-1276

* fix tests
2025-12-01 12:07:18 +00:00
Nathan Voxland
520de22c5e Fixed semantic-search filtering by collection (#66286) 2025-11-30 20:28:21 -06:00
Oleksandr Yakushev
67a99bb92d [perf] More efficient validator-cache old key eviction (#65890) 2025-11-29 20:03:33 +02:00
Nick Fitzpatrick
cbe07addba should show library (#66343)
* should show library

* tests

* more tests

* types

* uxw-2344

* I guess I forgot this?
2025-11-28 14:33:53 -04:00
Nemanja Glumac
4efe26824e Explicitly use the backend JAR for local Cypress runs (#66349)
The previous version of the build script (`build-for-test`) had a weird quirk that could be both confusing and dangerous. It was building a backend-only JAR in the same location and under same name as the full-blown, all-in-one Metabase JAR. This means that, by running Cypress locally, one could easily override the previously built healthy JAR. It's not catastrophic but it was confusing.

This PR makes it perfectly clear what we're doing, and there should be no conflicts between the JARs any longer.
2025-11-28 12:01:27 +00:00
Nemanja Glumac
b7ff644e78 Have a dedicated runner for SDK host/sample apps (#66378) 2025-11-28 10:09:03 +01:00
Nick Fitzpatrick
ccbda1251f updating watermark svg color and opacity (#66339)
* updating watermark svg color and opacity

* static viz development mark has same color and opacity as dynamic viz
2025-11-27 12:40:49 -04:00
Stas Gavrylov
a07c3b654e Incremental transforms (#65842)
* library collection search

* Fix e2e tests from group-16

* Fix a couple tests from group 42

* Fix e2e tests from group-17

* Fix e2e tests from group-18

* tests: add bulk publishing scenario

fix wrong collection link in case of root collection

* UXW-2315, UXW-2314

* UXW-2294

* more UXW-2315, UXW-2314

* more UXW-2315, UXW-2314

* correct collection types

* Remove Browsing Memory Feature

* Revert "Revert "Don't return library collections by default from APIs""

This reverts commit 814c49473b.

* Revert "Revert "Fixed null type filtering""

This reverts commit 25e0d78408.

* fetch full tree to have library collections visible

* show library in permissions

* Fixing tests

* fix infinite re-render in tree

* rewrite unit tests and fix max depth problem

* fix: publish property not validated in the tree view after table publishing

* Move bulk editing apis to enterprise (#65968)

* Make sidebar sticky

* test: bulk attribute actions for tables, schemas and dbs

* fix more unit tests

* Copy preview section tests

* Copy error handling tests

* Copy undos tests

* Update test

* test: update new ee endpoints paths

* Make new bulk table APIs ee-only (fe)
- Closes GDGT-1251

* Fix global H.DataModel.visit overrides

* Remove redundant cy.reload

* Remove redundant cy.reload

* add select/deselect tests

* use helper

* data picker using proper library endpoint. Fixing types I broke. Endless Shame

* Fix Tree skeleton resizing multiple times during loading
- Closes GDGT-1253

* Improve icon

* Fill content

* don't call getCollectionItems on database collection

* add search tests and support shift click in search

* default table publish collection

* fox: reset bulk form values when selection changes

* Fix e2e tests from group-19

* fix:  fix missing dependencies in useEffect gdgt-1245

* test: refactor test setup action order

* Fix e2e tests from group-20

* Fix e2e tests from group-22

* Fix e2e tests from group-23

* test: restore default db as the first step

* Kondo the kondo config

* default collections for metrics and models outside of data studio

* Move edit test to ee

* Fix list filtering now default is bronze

* Move bulk-edit-test to ee

* Fix table metadata for test

* Revert "Added `collection` argument to search"

This reverts commits:
- 21999e01
- b0f71c03ea
- 4fa0c3b422

* Fixed OSS loading of tables

* Fixed OSS loading of tables

* add tests for filtering

* Fix e2e tests from group 41 / joins-repros

* more tests

* Fix e2e tests from group-20

* Fix e2e tests from group-23

* Fix e2e tests from group-24

* Fix e2e tests from group-26

* Fix e2e tests from group-27

* Fix e2e tests from group-28

* Fix e2e tests from group-30

* constuct initial paths from library collections

* segments editor, hasEnabledNestedQueries

* Fix a couple more (something between groups 40 and 43)

* Fixed module config

* keyboard navigation

* Added library/tree API

* Fix some e2e tests from groups 40 and 42

* Fixing tests

* disable scoped search on the 'database collection', models.cy.spec should be passing now

* Fix e2e tests from group-33

* Minor cleanup

* Fix e2e tests from group-34

* Fixing tests

* Fix e2e tests from group 37 (except one depending on Recents tab)

* Starts fixing e2e tests from oss group

* cleanup

* wait to focus

* hide our analytics in the data studio

* Added library/tree API

* merge library and data studio

* change backend library flag validation to data-studio

* tidy things up

* Make bulk edit inputs non-clearable
- This prevents sending "empty" requests when clearing values
- And clearing does not really make sense here

* fix: test: table rescan and discard mocks are wrong

* fix: test: wrong mock in oss sync table schema

* Remove ^:parallel from bulk edit tests

They can contend and cause deadlocks

* fix: test: failure due to repeated mocking of same route

* test: refactor: cypress table tests

* Update e2e test

* Update e2e test

* Update e2e test

* fix browse nav section test

* Add missing token

* add check icon for published tables

* Removed unused import

* Updates some e2e tests from oss group, removes some now-invalid tests

* test: use default external db, skip transform test

* Starts fixing e2e tests from mongo group

* remove background from not active table in search

* specs

* Fix e2e tests from group-15

* hack fix ci while i'm on meetings

* scoped search is back

* Fix some e2e tests from group-32

* table picker fixes

* Fixed merge

* Fix e2e test from group-42

* merge fixes

* re-enable the recents tab on the data picker modal

* Avoid race condition on list-table-filtering-test

* fix embedding types

* mini picker unit tests

* Fixing tests

* ci, fixes

* fix library collection bugs

* cleanup data studio plugin

* fix lots of unit tests

* fix import

* fix

* Fixed unused import

* Fixed accidental #p

* ci

* revert util

* specs

* specs

* specs

* fix bug / bad type in SchemaFormSelect (#65995)

* Use actual anti join in GET /table query

* Remove old comments

* Activate token after restore

* test: unskip table metadata transform test

* test: fix search tables e2e test

* fix: test: selection in table picker

* Fix Source input disabled when filtering by Metabase transform
- Closes GDGT-1259

* test: fix: filter unused table

* Fix e2e test

* fix: test: datamodel toasts and errors

* Fix e2e test

* Fix e2e tests from group-9

* Fix flaky tests

* GDGT-1244: Fix DB tree virtualization (#66125)

* GDGT-1244: Fix tree virtualization. Replace top with translateY to improve performance

* GDGT-1244: Fix scroll bug. Refactor border styling

* bring back modeling empty state

* test: adjust publishing test

* Fix e2e test from group-22

* Fix e2e test from group-25

* fix allowing cross DB joins

* properly disable invalid items in MiniPicker

* Fix e2e test from group-26

* Fix straggler from oss group

* Starts fixing e2e tests from group-32

* unit test adjustments

* Fix remaining issue with e2e group 37

* fix item filtering

* Update Loki Snapshots

* add recent views to mini picker

* test recents and backparsing

* additional filtering test

* fix minipicker shows items it should not show

* Fixed :here in library API

* Fixed :here in library API

* UXW-2358 publishing outside of the data studio

* Fixed :here in library API

* snapshot

* Fix most e2e tests from group-32

* Lint fixes

* Fixed failing test

* revert fix

* Reapply "Added `collection` argument to search"

This reverts commit f28d9d5af9.

* Quick fix in CollectionPicker/utils.ts

* Fix flaky Remote Sync test (#65862)

* Clean up backend-test matrix (#66158)

* Fix timeseries value detection (#66161)

* Prevent driver tests (#66156)

Co-authored-by: Bryan Maass <bryan.maass@gmail.com>

* Fetch uberjar using a proper action (#66165)

* fix embedding column picking

* Trying CI with search_index.verified

* Fixed failing test

* Translate label

* Fixed failing test

* fix: published info not updated in the tree picker after deleting model

* fix: refactor: publish collection link tests

* Fixed failing test

* Fix remaining e2e tests from group-32

* FileUploadStatus unit test, quesiton reproductions

* create data studio plugin file

* fix hidden items some more

* Fix issue where confirmation modal closes then reopens again then closes again when you archive a dashboard

* fix dark mode

* UXW-2367

* UXW-2378

* Fixed merge issues

* this element is not visible because you can't even open incompatible sample database db

* Fix e2e test from group-14

* fix embedding data picker clicks

* filtering fixes for transforms

* UXW-2372

* update mini picker specs after a fix

* fix flaky join test

* Fixing in-place support for collections

* fix: cannot scroll search results

* ui: tweak search results checkbox spacing to match tree results

* fix: scroll position jump when entering field sort mode gdgt-1269

* Revert "fix: scroll position jump when entering field sort mode gdgt-1269"

This reverts commit bfecdec661.

* UXW-2279 Changes Data Studio's Glossary page background color to background-secondary

* Fix linters

* Fix e2e tests

* fixes for sqlserver

* fix driver changelog

* fix

* more fix

* fix: support params in bigq

* fix: inline params for sqlserver

* fix test

* fix: we do want the unique key

* fix: missing nil in test

* fix clickhouse

---------

Co-authored-by: Ryan Laurie <iethree@gmail.com>
Co-authored-by: Brad Anderson <brad@metabase.com>
Co-authored-by: Anderson <anderson@metabase.com>
Co-authored-by: Jakub Chodorowicz <jakub@chodorowicz.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@gmail.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Uladzimir Havenchyk <uladzimir@metabase.com>
Co-authored-by: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
Co-authored-by: Dan Stone <wotbrew@gmail.com>
Co-authored-by: Sloan Sparger <sloansparger@users.noreply.github.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
Co-authored-by: dpsutton <dan@dpsutton.com>
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
Co-authored-by: Nicola Mometto <nicola.mometto@metabase.com>
2025-11-27 15:52:19 +00:00
Anderson
e80478b006 Prevent database sync status element to overlap pagination (#66225) 2025-11-27 11:16:01 -03:00
Anderson
c8d87bdf7b Do not allow setting up remote sync if git url is not set (#66281) 2025-11-27 11:14:41 -03:00
Anderson
e2ec2f6aff Fix broken PDF export layout due to CSP violation for inline styles (#66337) 2025-11-27 11:14:20 -03:00
Nick Fitzpatrick
ce9ac84ecc Update selector for loki test in Palette.stories (#66333) 2025-11-27 10:07:03 -04:00
Nemanja Glumac
66b5043940 Make file-path triggers more robust (#66311)
This pull request refactors and streamlines the configuration for CI workflows and file path groupings in .github/file-paths.yaml and updates related workflow logic in .github/workflows/run-tests.yml. The changes focus on consolidating shared configuration, improving maintainability, and removing redundant or unused path groups.
2025-11-27 14:01:33 +01:00
Ngoc Khuat
2b607f547a fix false positive logs (#66325) 2025-11-27 12:50:47 +00:00
Jakub Chodorowicz
0400e1bf07 fix: scroll position jump when entering field sort mode gdgt-1269 (#66318)
* fix: scroll position jump when entering field sort mode gdgt-1269

* test: fix failing unit test selector

* fix: e2e test selectors

* fix: test selectors for sync button

* test: adjust sync button selector

* test: fix semantic selector

* Update enterprise/frontend/src/metabase-enterprise/data-studio/data-model/components/TableSection/TableSection.tsx

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
2025-11-27 10:48:43 +01:00
appleby
df5abbd1b9 Add :database_type to metabot tools that return column metadata (#66302)
* Add :database_type to metabot tools that return column metadata

Add :database_type to metabot tools that return column metadata in order to give metabot information about the real
database type when generating SQL.

Examples of places the :database_type now appears:

- :used_tables in :user_is_viewing context
- {table,card,metric}-details
- get-query-details
- enhanced-database-tables

* Fix test after rebase onto #66271

Fix get-table-details-database-type-test after rebase (1) use new field-id formatting (2) get-table-details no longer returns implicitly-joinable fields
in the top-level `:fields`.
2025-11-26 23:32:44 +00:00
Egor Grushin
99fa786ec8 fix: flake test number 29943 (#66283)
* fix: flake test number 29943

* mark moveDnDKitElement as deprecated, fix singleused constant
2025-11-26 22:08:24 +00:00
Noah Moss
b561b5c8b9 Revised approach to related_tables and consistent field IDs for implicitly-joined fields (#66271)
* More flexible approach to resolving columns from metabot field_ids

* fix one field-stats-test

* fix field-stats-test

* support nano-ids in field ids (?)

* remove some dead code

* Exclude implicitly joinable fields when fetching cols for table

* try switching field format to use dash

* fix field IDs in a number of tests

* Kondo

* add some unit tests for new field parsing & column lookup helper fns

* Fix more tests

* Fix more tests

* fix regex

* New approach -- use wide field IDs in related_tables and add a related_by field for each table

* Fix some tests

* remove unneeded table

* cleanup, new test

* Revert to passing in & matching on expected prefix

* Address tyler's comments

* accept a regex for the prefix as well

* add back small malli group-by fix from the last PR - removed unintentionally during revision
2025-11-26 15:48:30 -05:00
Nathan Voxland
f2e8e294f8 Add warning for when mismatch in serdes version during loading (#66140) 2025-11-26 14:30:15 -06:00
Riley Thompson
8257c072a9 Revert "Bump com.clickhouse/clickhouse-jdbc from 0.8.4 to 0.9.2 (#64168)" (#66326)
This reverts commit 31f76f5856.
2025-11-26 17:59:28 +00:00
Denis Berezin
08770c8e8f UXW-1778 remove chart hover effects when exporting to png (#66200)
* UXW-1778 remove chart hover effects when exporting to png

* Fix typing and unit tests

* review fix
2025-11-26 19:02:51 +03:00
Nemanja Glumac
4ef178f794 Support building an uberjar with a custom filename (#66315)
* Make uberjar (artifact) file name configurable via env var

Add the ability to customize the output filename for the uberjar build
via the MB_JAR_FILENAME environment variable. This allows backend-only
test builds to use a separate filename (e.g., metabase-backend.jar)
to avoid overwriting the regular all-in-one metabase.jar.
2025-11-26 15:45:19 +00:00
Gabriel Moreno
89443cda94 UXW-2228 Properly mention available subscription & alert channels for non-admins (#66273)
* fix: add missing channels to copy & rendering logic

Mentioned webhooks and Slack where necessary to stay consistent with the
admin UI

* fix: standardize lowercase 'admin' & prefer 'email' over 'SMTP'

* fix: remove extra comma on wording

* test: adjust e2e test assertions

* test: add `DashboardSubscriptionMenuItem.unit.spec.tsx`

* test: extract single case from `describe` block

* test: fix subscriptions e2e case & text lookup on alert spec
2025-11-26 15:04:34 +00:00
Edward Paget
99fd1809cb Get user attributes from attributes map (#66274)
* Get user attributes from attributes map

* fixup! e2e test
2025-11-26 14:38:33 +00:00
Braden Shepherdson
868fb6da0a [MBQL lib] Allow string literals as arg to aggregations like :max (#66220)
Fixes #66199.

`metabase.legacy-mbql.schema`, now used for normalizing MBQL 4 in
place of the old bespoke normalization in earlier versions, regressed in
this weird but legitimate use case: Writing a custom aggregation like
`MAX("String Literal")`.

That's an odd thing to do, but it can be useful to provide a label in an
aggregated query. This is the MBQL equivalent of
`(SELECT ..., 'A' as label FROM ...) UNION ALL (SELECT ..., 'B' as label ...)`
2025-11-26 13:28:57 +00:00
Nicolò Pretto
8a56f6b3bb update file path to include tsx extension for component tests (#66254) 2025-11-26 11:00:31 +01:00
github-automation-metabase
384d24a75b Bump org.clojure/tools.cli from 1.1.230 to 1.2.245 (#64159)
Inspect dependency changes here: https://github.com/clojure/tools.cli/blob/v1.2.245/CHANGELOG.md

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
2025-11-26 00:04:43 +00:00
Cam Saul
e94dd16252 Split Redshift driver tests into two jobs (#66289) 2025-11-25 15:39:13 -08:00
Fraser
561d578043 Fix eslint cache (#66066) 2025-11-26 12:29:56 +13:00
github-automation-metabase
6f778c9269 Bump com.google.guava/guava from 33.4.8-jre to 33.5.0-jre (#63796)
Inspect dependency changes here:

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
Co-authored-by: bryan <bryan.maass@gmail.com>
2025-11-25 23:03:44 +00:00
bryan
9b93bfe279 Only real app-dbs should print setup instructions (#66255) 2025-11-25 23:03:06 +00:00
Nemanja Glumac
dc89b2bbeb Decouple the cleanup logic from running Cypress (#66236)
This pull request refactors the Cypress runner utility to simplify error handling and improve consistency in how test failures are reported. The main change is that runCypress now throws errors on test failures instead of relying on an external exit or cleanup function, which streamlines its usage across different scripts.

This ensures consistent error propagation and simplifies the control flow for callers.
2025-11-25 23:10:02 +01:00
github-automation-metabase
538ebb5f08 Bump cheshire/cheshire from 6.0.0 to 6.1.0 (#63217)
* Bump cheshire/cheshire from 6.0.0 to 6.1.0

Inspect dependency changes here: https://github.com/dakrone/cheshire/blob/6.1.0/ChangeLog.md

* update timestamps to match new cheshire parsing

---------

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
2025-11-25 22:02:13 +00:00
Nemanja Glumac
e4325d4e02 Always pass the input conditions to sdk jobs (#66269) 2025-11-25 22:56:17 +01:00
bryan
b453baf5ed Upgrades mage run-tests (#66108) 2025-11-25 21:42:28 +00:00
github-automation-metabase
773a588692 Bump org.apache.commons/commons-lang3 from 3.18.0 to 3.19.0 (#64150)
Inspect dependency changes here:

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
Co-authored-by: bryan <bryan.maass@gmail.com>
2025-11-25 21:38:24 +00:00
github-automation-metabase
31f76f5856 Bump com.clickhouse/clickhouse-jdbc from 0.8.4 to 0.9.2 (#64168)
Inspect dependency changes here:

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
Co-authored-by: bryan <bryan.maass@gmail.com>
2025-11-25 21:35:15 +00:00
github-automation-metabase
81f10bdb3c Update generated docs (master) (#66228)
Co-authored-by: github-actions <github-actions@github.com>
2025-11-25 21:34:41 +00:00
github-automation-metabase
3dcfbce301 Bump org.apache.calcite.avatica/avatica from 1.26.0 to 1.27.0 (#64402)
Inspect dependency changes here: https://github.com/apache/calcite-avatica/compare/avatica-1.26.0-rc0...avatica-1.27.0-rc0

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
2025-11-25 21:17:19 +00:00
Eric Normand
ae23f598f0 Remove special representations workflow 2025-11-25 15:01:42 -06:00
appleby
5c10e2d7a3 Add :database_engine to metabot table-details and card-details (#66227)
These tools already included :database_id in the results; we now include the :database_engine as well, to give metabot
a reminder about which SQL dialect it should generate.
2025-11-25 12:03:13 -08:00
github-automation-metabase
53f51fa289 Bump org.apache.arrow/arrow-memory-netty from 17.0.0 to 18.3.0 (#64401)
* Bump org.apache.arrow/arrow-memory-netty from 17.0.0 to 18.3.0

Inspect dependency changes here:

* Upgrade arrow-memory-core to version 18.3.0

Updated the version of arrow-memory-core from 17.0.0 to 18.3.0.

* Consolidate changes

* Fix formatting

---------

Co-authored-by: bryan <bryan.maass@gmail.com>
Co-authored-by: Luis Paolini <paoliniluis@gmail.com>
Co-authored-by: Cam Saul <github@camsaul.com>
Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
2025-11-25 19:41:46 +00:00
github-automation-metabase
4280e1af7a Bump org.jsoup/jsoup from 1.21.1 to 1.21.2 (#62652)
Inspect dependency changes here: https://github.com/jhy/jsoup/blob/jsoup-1.21.2/CHANGES.md

Co-authored-by: Cam Saul <1455846+camsaul@users.noreply.github.com>
Co-authored-by: bryan <bryan.maass@gmail.com>
2025-11-25 19:31:30 +00:00
Phoomparin Mano
0a2255c36f ci(sdk): only setup specific enterprise plugins in sdk unit tests as needed (#66238) 2025-11-26 02:03:22 +07:00
bryan
e1e8d53234 add deprecation support to defendpoint dox-gen (#66053) 2025-11-25 18:35:00 +00:00
Nemanja Glumac
cca02fcf31 Fix JAR health check condition logic (#66266)
Addresses this finding by the Claude code review:
https://github.com/metabase/metabase/pull/66237#discussion_r2560704357
2025-11-25 19:23:11 +01:00
Noah Moss
eb4a9f528e Move audit installation before search initialization again (#66222) 2025-11-25 12:02:01 -05:00
Nemanja Glumac
6e30ce468c Run JAR health check only on master and release branches (#66237) 2025-11-25 17:43:27 +01:00
Alexander Polyankin
96eac2d9da Data Studio (#65281)
* Fix some e2e tests from group-9

* test: add bulk sync action e2e test

* fix column selection

* Fixed module config

* Fix a few e2e tests from oss group

* Fix e2e tests from groups 46 and 47

* Fix e2e tests from groups 44 and 45

* Fix group-3 straggler, some of group-8

* Potentially disastrous change (increases item-picker-level of SchemaList and TableList) to fix test in group-8

* Fix group-6 straggler, some more of group-8

* Updates some more e2e tests from group-9 (some actual failures left)

* Fix e2e tests from group-14

* do enterprise code correctly

* library collection search

* Fix e2e tests from group-16

* Fix a couple tests from group 42

* Fix e2e tests from group-17

* Fix e2e tests from group-18

* tests: add bulk publishing scenario

fix wrong collection link in case of root collection

* UXW-2315, UXW-2314

* UXW-2294

* more UXW-2315, UXW-2314

* more UXW-2315, UXW-2314

* correct collection types

* Remove Browsing Memory Feature

* Revert "Revert "Don't return library collections by default from APIs""

This reverts commit 814c49473b.

* Revert "Revert "Fixed null type filtering""

This reverts commit 25e0d78408.

* fetch full tree to have library collections visible

* show library in permissions

* Fixing tests

* fix infinite re-render in tree

* rewrite unit tests and fix max depth problem

* fix: publish property not validated in the tree view after table publishing

* Move bulk editing apis to enterprise (#65968)

* Make sidebar sticky

* test: bulk attribute actions for tables, schemas and dbs

* fix more unit tests

* Copy preview section tests

* Copy error handling tests

* Copy undos tests

* Update test

* test: update new ee endpoints paths

* Make new bulk table APIs ee-only (fe)
- Closes GDGT-1251

* Fix global H.DataModel.visit overrides

* Remove redundant cy.reload

* Remove redundant cy.reload

* add select/deselect tests

* use helper

* data picker using proper library endpoint. Fixing types I broke. Endless Shame

* Fix Tree skeleton resizing multiple times during loading
- Closes GDGT-1253

* Improve icon

* Fill content

* don't call getCollectionItems on database collection

* add search tests and support shift click in search

* default table publish collection

* fox: reset bulk form values when selection changes

* Fix e2e tests from group-19

* fix:  fix missing dependencies in useEffect gdgt-1245

* test: refactor test setup action order

* Fix e2e tests from group-20

* Fix e2e tests from group-22

* Fix e2e tests from group-23

* test: restore default db as the first step

* Kondo the kondo config

* default collections for metrics and models outside of data studio

* Move edit test to ee

* Fix list filtering now default is bronze

* Move bulk-edit-test to ee

* Fix table metadata for test

* Revert "Added `collection` argument to search"

This reverts commits:
- 21999e01
- b0f71c03ea
- 4fa0c3b422

* Fixed OSS loading of tables

* Fixed OSS loading of tables

* add tests for filtering

* Fix e2e tests from group 41 / joins-repros

* more tests

* Fix e2e tests from group-20

* Fix e2e tests from group-23

* Fix e2e tests from group-24

* Fix e2e tests from group-26

* Fix e2e tests from group-27

* Fix e2e tests from group-28

* Fix e2e tests from group-30

* constuct initial paths from library collections

* segments editor, hasEnabledNestedQueries

* Fix a couple more (something between groups 40 and 43)

* Fixed module config

* keyboard navigation

* Added library/tree API

* Fix some e2e tests from groups 40 and 42

* Fixing tests

* disable scoped search on the 'database collection', models.cy.spec should be passing now

* Fix e2e tests from group-33

* Minor cleanup

* Fix e2e tests from group-34

* Fixing tests

* Fix e2e tests from group 37 (except one depending on Recents tab)

* Starts fixing e2e tests from oss group

* cleanup

* wait to focus

* hide our analytics in the data studio

* Added library/tree API

* merge library and data studio

* change backend library flag validation to data-studio

* tidy things up

* Make bulk edit inputs non-clearable
- This prevents sending "empty" requests when clearing values
- And clearing does not really make sense here

* fix: test: table rescan and discard mocks are wrong

* fix: test: wrong mock in oss sync table schema

* Remove ^:parallel from bulk edit tests

They can contend and cause deadlocks

* fix: test: failure due to repeated mocking of same route

* test: refactor: cypress table tests

* Update e2e test

* Update e2e test

* Update e2e test

* fix browse nav section test

* Add missing token

* add check icon for published tables

* Removed unused import

* Updates some e2e tests from oss group, removes some now-invalid tests

* test: use default external db, skip transform test

* Starts fixing e2e tests from mongo group

* remove background from not active table in search

* specs

* Fix e2e tests from group-15

* hack fix ci while i'm on meetings

* scoped search is back

* Fix some e2e tests from group-32

* table picker fixes

* Fixed merge

* Fix e2e test from group-42

* merge fixes

* re-enable the recents tab on the data picker modal

* Avoid race condition on list-table-filtering-test

* fix embedding types

* mini picker unit tests

* Fixing tests

* ci, fixes

* fix library collection bugs

* cleanup data studio plugin

* fix lots of unit tests

* fix import

* fix

* Fixed unused import

* Fixed accidental #p

* ci

* revert util

* specs

* specs

* specs

* fix bug / bad type in SchemaFormSelect (#65995)

* Use actual anti join in GET /table query

* Remove old comments

* Activate token after restore

* test: unskip table metadata transform test

* test: fix search tables e2e test

* fix: test: selection in table picker

* Fix Source input disabled when filtering by Metabase transform
- Closes GDGT-1259

* test: fix: filter unused table

* Fix e2e test

* fix: test: datamodel toasts and errors

* Fix e2e test

* Fix e2e tests from group-9

* Fix flaky tests

* GDGT-1244: Fix DB tree virtualization (#66125)

* GDGT-1244: Fix tree virtualization. Replace top with translateY to improve performance

* GDGT-1244: Fix scroll bug. Refactor border styling

* bring back modeling empty state

* test: adjust publishing test

* Fix e2e test from group-22

* Fix e2e test from group-25

* fix allowing cross DB joins

* properly disable invalid items in MiniPicker

* Fix e2e test from group-26

* Fix straggler from oss group

* Starts fixing e2e tests from group-32

* unit test adjustments

* Fix remaining issue with e2e group 37

* fix item filtering

* Update Loki Snapshots

* add recent views to mini picker

* test recents and backparsing

* additional filtering test

* fix minipicker shows items it should not show

* Fixed :here in library API

* Fixed :here in library API

* UXW-2358 publishing outside of the data studio

* Fixed :here in library API

* snapshot

* Fix most e2e tests from group-32

* Lint fixes

* Fixed failing test

* revert fix

* Reapply "Added `collection` argument to search"

This reverts commit f28d9d5af9.

* Quick fix in CollectionPicker/utils.ts

* Fix flaky Remote Sync test (#65862)

* Clean up backend-test matrix (#66158)

* Fix timeseries value detection (#66161)

* Prevent driver tests (#66156)

Co-authored-by: Bryan Maass <bryan.maass@gmail.com>

* Fetch uberjar using a proper action (#66165)

* fix embedding column picking

* Trying CI with search_index.verified

* Fixed failing test

* Translate label

* Fixed failing test

* fix: published info not updated in the tree picker after deleting model

* fix: refactor: publish collection link tests

* Fixed failing test

* Fix remaining e2e tests from group-32

* FileUploadStatus unit test, quesiton reproductions

* create data studio plugin file

* fix hidden items some more

* Fix issue where confirmation modal closes then reopens again then closes again when you archive a dashboard

* fix dark mode

* UXW-2367

* UXW-2378

* Fixed merge issues

* this element is not visible because you can't even open incompatible sample database db

* Fix e2e test from group-14

* fix embedding data picker clicks

* filtering fixes for transforms

* UXW-2372

* update mini picker specs after a fix

* fix flaky join test

* Fixing in-place support for collections

* fix: cannot scroll search results

* ui: tweak search results checkbox spacing to match tree results

* fix: scroll position jump when entering field sort mode gdgt-1269

* Revert "fix: scroll position jump when entering field sort mode gdgt-1269"

This reverts commit bfecdec661.

* UXW-2279 Changes Data Studio's Glossary page background color to background-secondary

---------

Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Brad Anderson <brad@metabase.com>
Co-authored-by: Jakub Chodorowicz <jakub@chodorowicz.com>
Co-authored-by: Ryan Laurie <iethree@gmail.com>
Co-authored-by: Anderson <anderson@metabase.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@gmail.com>
Co-authored-by: Uladzimir Havenchyk <uladzimir@metabase.com>
Co-authored-by: Uladzimir Havenchyk <125459446+uladzimirdev@users.noreply.github.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
Co-authored-by: Dan Stone <wotbrew@gmail.com>
Co-authored-by: Sloan Sparger <sloansparger@users.noreply.github.com>
Co-authored-by: Stas Gavrylov <stas@metabase.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
Co-authored-by: dpsutton <dan@dpsutton.com>
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
2025-11-25 08:09:59 -08:00
Eric Normand
2d6624319a Test collection tree import/export 2025-11-25 09:43:28 -06:00
Gabriel Moreno
15bb72ee66 fix: anchor table info popover to bottom (#66226)
Since `position` was set to `top`, the popover had little space to
expand upwards, and the box was cut-off before reaching the anchor
point.
2025-11-25 14:15:12 +00:00
Stas Gavrylov
ce0232662d GDGT-1252: Respect show mini bar chart setting in list view (#66012)
* GDGT-1252: Make list view consider mini-bar-chart setting

* GDGT-1252: Fix incorrect model settings update when changing columns settings

* GDGT-1252: e2e test

* GDGT-1252: Fix unit test for ListViewItem

* GDGT-1252: Address PR comments. Minor refactoring

* GDGT-1252: Refactor DatasetEditor to get rid of modelSettings hack

* GDGT-1252: Update e2e test to cover settings persistence

* Update Loki Snapshots

* Revert Loki update

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2025-11-25 12:48:09 +02:00
Nemanja Glumac
076267ed7c chore: Upgrade nextjs to resolve dependabot alert (#66203) 2025-11-25 10:08:00 +01:00
Dennis Schridde
a7f3165348 Fix purchase page for paid Metabase AI add-on for non-store users (#66086)
### Description

Missing billing information is treated as an error for purposes of
displaying the purchase page with prices.  Non-store users cannot
retrieve billing information.  Thus, previously, they would always see
an error page: "Error fetching information about available add-ons."
With this change, they see the "Please ask a Metabase Store Admin" page
as intended.

We achieve this by splitting the page content for store users from that
of non-store users.  Only the former needs to load data (billing and
add-on information), so only that page needs `LoadingAndErrorWrapper`.
For store users we show no dynamic content that could fail to load
(apart from a store user's email address, which we gracefully omit if
it is missing).

### How to verify

1. Ensure you have a token with the `offer-metabase-ai-tiered`
   feature, but without the `metabot-v3` and `offer-metabase-ai`
   features.
2. As a Metabase user with an email address that *does not* match any
   of the token's organization's store user email addresses, navigate
   to `/admin/metabot` and see "Please ask a Metabase Store Admin".
3. As a Metabase user with an email address that *does* match any
   of the token's organization's store user email addresses, navigate
   to `/admin/metabot` and see the purchase page with tier selector.

### Checklist

- [x] Tests have been added/updated to cover changes in this PR

Fixes: 2b76c38e1f
Closes: https://metaboat.slack.com/archives/C07RMUQ0GE5/p1763644357893079
2025-11-25 02:37:44 +01:00
Nemanja Glumac
1881164422 Improve how skipped SDK tests are handled (#66211) 2025-11-24 23:29:37 +01:00
Nemanja Glumac
81ba716093 Check for project requirements (#66192)
This PR adds a non-destructive dev script whose only job is to detect and report on your local development environment setup.

## What it does

**`./bin/dev-requirements`** detects:
  - Version managers:
      -  nvm, fnm, n (Node.js version managers)
      - volta, asdf, mise (multi-language support)
      - jenv, sdkman (Java version managers)
  - Installed development tools and their versions
  - Whether your setup meets Metabase's requirements

It cross-references detected versions against project configuration files:
  - Node.js version from `.nvmrc`
  - Clojure version from `deps.edn`
  - Java 21 Temurin distribution requirement

## What it doesn't do

  - No installations
  - No modifications
  - No assumptions about your preferences

The script is purely informational - it shows you what you have, what's required, and recommends mise for version management. Perfect for new contributors or when troubleshooting setup issues.
2025-11-24 22:09:43 +01:00
Eric Normand
f51192d3db Collection tests! 2025-11-24 14:33:43 -06:00
Nemanja Glumac
f90fbb06ce Delete now unused ci-test-config.json (#66189) 2025-11-24 21:23:35 +01:00
Eric Normand
23754f9e95 Tests pass, kondo passes, cljfmt done 2025-11-24 14:18:16 -06:00
Brad Anderson
76cf73dde5 Fix #65501 - Resets search back to first page when filters change (#66104) 2025-11-24 15:07:39 -05:00
Noah Moss
74b8adabe6 Two quality-of-life improvements for @-mentions (#66131)
* QoL improvements for @-mentions

* Add a unit test for new tab click behavior
2025-11-24 19:30:31 +00:00
Riley Thompson
7d7595996c Add snowflake collate expression (#65944)
* get collate working

* add collate test

* add changelog

* oragnize collate code

* fix driver changelog typo
2025-11-24 14:09:59 -05:00
Riley Thompson
8f23a5b725 Cancel jobs in bigquery (#66054)
* cancel bigquery jobs

* move .cancel to promise handling

* wrap .cancel in try catch finally

* use getqueryresults instead of .query
2025-11-24 19:07:31 +00:00
Edward Paget
05b294f47a feat: use boolean flag for remote-sync status (#66048)
* feat: use boolean flag for remote-sync status

It turns out type is too overloaded to have a 'remote-synced' collection
type. This is behavior that we might want to share around other typed
collections, so this migrates us to use a boolean column for this value instead.

* fixup! e2e test find synced collection

* fixup! e2e collection fixture has correct values

* fixup! make sure flag is set on insert/update
2025-11-24 12:16:43 -06:00
Nemanja Glumac
cb37ce8108 chore: Upgrade zx to resolve dependabot alert (#66207)
Should resolve
https://github.com/metabase/metabase/security/dependabot/275
2025-11-24 18:40:46 +01:00
Eric Normand
ae65686d30 Fix tests 2025-11-21 15:49:57 -06:00
Eric Normand
f26f6a18cd core test pass 2025-11-21 15:25:43 -06:00
Eric Normand
4b33138799 Better database tests 2025-11-21 15:22:03 -06:00
Eric Normand
d3bad99de1 Whitespace 2025-11-21 15:21:51 -06:00
Eric Normand
a576039ae0 Remove persist! 2025-11-21 14:48:18 -06:00
Eric Normand
619e2fa17e Remove non-existent imports 2025-11-21 14:39:47 -06:00
Eric Normand
9116c09afe Get rid of frontend changes 2025-11-21 14:38:58 -06:00
Eric Normand
47b0340836 Merge branch 'master' into representations 2025-11-21 14:23:32 -06:00
Eric Normand
eda624973b Remove old stuff and simplify core 2025-11-21 14:21:48 -06:00
Eric Normand
0c0634dbc1 Module deps for kondo 2025-11-21 13:54:13 -06:00
Eric Normand
8a1139118f Remove transform-specific import 2025-11-21 13:53:57 -06:00
Eric Normand
8bb448b8fc People keep moving namespaces 2025-11-21 13:53:46 -06:00
Eric Normand
99e16ef825 Fixups after merge 2025-11-21 13:53:25 -06:00
Eric Normand
8b44711794 Don't throw if not found 2025-11-21 13:52:57 -06:00
Eric Normand
c35be5ea97 Remove unused transforms export 2025-11-21 13:52:43 -06:00
Eric Normand
04f1e58c3a Delete some unused files. 2025-11-21 13:50:55 -06:00
Eric Normand
20657a3b0b Remove dependencies "integration" (unused) 2025-11-19 11:34:10 -06:00
Eric Normand
174a118a6c Claude fixed the kondo warnings 2025-11-19 11:23:53 -06:00
Eric Normand
4d6693adae Typo 2025-11-19 10:27:24 -06:00
Eric Normand
91d2191c93 Merge branch 'master' into representations 2025-11-19 10:17:57 -06:00
Riley Thompson
86dcae0d73 clean up pulse ui 2025-11-13 19:07:01 -05:00
Riley Thompson
ea48c27d72 use refs with pulses 2025-11-13 19:02:23 -05:00
Riley Thompson
13cb16df11 initial pulse impl 2025-11-13 16:57:35 -05:00
Riley Thompson
89e585ac23 finish timeline tests 2025-11-13 15:30:05 -05:00
Riley Thompson
7c8078bd09 Merge branch 'master' into representations 2025-11-10 18:13:21 -05:00
Riley Thompson
8e37784337 update timelines 2025-11-10 17:52:06 -05:00
Riley Thompson
b0443a4805 timeline impl 2025-11-10 17:13:20 -05:00
Eric Normand
ddcc13edd1 Simplify portable query visitor 2025-11-07 16:53:26 -06:00
Eric Normand
38a5d7b604 Simplify portable query visitor protocol 2025-11-07 16:49:36 -06:00
Eric Normand
938600d341 Fix mbql tests 2025-11-07 11:17:18 -06:00
Eric Normand
1469864d71 Question tests pass again 2025-11-07 11:07:20 -06:00
Eric Normand
777980d11e Forgot the new file 2025-11-07 10:40:52 -06:00
Eric Normand
0a63009b9e lib.portable and representations integration 2025-11-07 10:37:56 -06:00
Eric Normand
609a58ad6d Tiny cleanup 2025-11-04 15:48:39 -06:00
Eric Normand
e202541ac8 Don't double ref: 2025-11-04 15:27:12 -06:00
Eric Normand
7e54e25766 Better 2025-11-04 15:09:27 -06:00
Eric Normand
9b903edd5e document 2025-11-04 14:33:50 -06:00
Eric Normand
ea1cdf4917 Checkpoint 2025-11-04 10:35:49 -06:00
Eric Normand
c58e26c8e2 Add resolve to export 2025-11-03 17:28:03 -06:00
Timothy Dean
99d1bd8cdf Use lookup/lookup-database-id in more places 2025-11-03 14:42:52 -07:00
Timothy Dean
57999716ce Playing with model metadata 2025-11-03 14:05:31 -07:00
Eric Normand
57f64aded5 Hook rename refs into api 2025-11-03 13:34:16 -06:00
Eric Normand
8d204c712e Whitespace 2025-11-03 13:28:25 -06:00
Eric Normand
2cbece56c3 Nice set of name tests with a munge-name implementation 2025-11-03 13:01:15 -06:00
Timothy Dean
c8c515462a Fix bad var reference 2025-11-03 10:53:40 -07:00
Timothy Dean
4ed9e32676 Delete unused and stale yml files 2025-10-31 16:48:46 -06:00
Timothy Dean
3a02474632 Delete bin/* scripts 2025-10-31 16:44:03 -06:00
Timothy Dean
20875923d7 Delete example-manifest.yml 2025-10-31 16:36:36 -06:00
Timothy Dean
2b2cd76b2f Delete ./representation babashka stuff 2025-10-31 16:35:28 -06:00
Eric Normand
6a78c448cf Oops bad merge 2025-10-31 16:24:46 -05:00
Eric Normand
81fa467312 Export ref renaming 2025-10-31 16:18:40 -05:00
Eric Normand
7b604076bc move more stuff for order-reprsentations 2025-10-31 16:18:00 -05:00
Timothy Dean
8f8b2b762a Bugfixes in insert! and update! 2025-10-31 14:24:33 -06:00
Timothy Dean
db2d21ef20 Fix v0.common-test errors 2025-10-31 14:19:49 -06:00
Timothy Dean
c2a163701b Step 3, multimethods gone and circular dependencies resolved. 2025-10-31 14:16:53 -06:00
Timothy Dean
2a9df86ff0 Step 2 of cleanup, removing import dependency from v0.* representation nses 2025-10-31 13:45:31 -06:00
Timothy Dean
b2502af083 Step 1 of a series of big cleanup commits 2025-10-31 13:35:06 -06:00
Eric Normand
4cd66a1b6e move order-representations into v0-common 2025-10-31 11:37:48 -05:00
Eric Normand
eb9a0acb6f Oops 2025-10-31 10:22:09 -05:00
Timothy Dean
bc6f659963 m. Remove ref: initial from workflows importing metabase/representations 2025-10-30 16:09:09 -06:00
Timothy Dean
93fe76a5c6 More temporary stuff, getting verify-i18n-files running 2025-10-30 16:06:35 -06:00
Timothy Dean
993672c878 Merge remote-tracking branch 'origin/master' into representations 2025-10-30 15:58:00 -06:00
Timothy Dean
2421a24f31 More temporary boilerplate 2025-10-30 15:57:41 -06:00
Timothy Dean
2688cf4cfb Build artifact with metabase/representations 2025-10-30 15:53:20 -06:00
Timothy Dean
4ac312e62c Missed a file 2025-10-30 15:50:16 -06:00
Timothy Dean
32a67d3633 Experimental. Delete copy of metabase/representations, use PAT to check out private repo 2025-10-30 15:46:02 -06:00
Eric Normand
e343d5ebbe Default db. Fix insert/update. Default to name if display_name is not avilable. 2025-10-30 14:49:36 -05:00
Eric Normand
49020716b5 Multify insert! and update! 2025-10-30 13:38:03 -05:00
Timothy Dean
e406c0c35c Merge remote-tracking branch 'origin/master' into representations 2025-10-29 17:10:05 -06:00
Timothy Dean
3cd8c2f2cd Fix core-test 2025-10-29 17:02:35 -06:00
Timothy Dean
59f53c16db The big switchover to metabase/representations for schemas 2025-10-29 16:57:55 -06:00
Timothy Dean
cac18c4db2 Copy in metabase/representations 2025-10-29 16:56:34 -06:00
Timothy Dean
dde1cd6b95 Copy from metabase/representations 2025-10-29 15:19:08 -06:00
Timothy Dean
e4b0d120e5 Copy 2b8f5d6 from metabase/representations 2025-10-29 14:31:00 -06:00
Eric Normand
56fac7fa11 Transforms order and better export/import 2025-10-29 15:23:27 -05:00
Eric Normand
6826b70279 Import test 2025-10-29 14:01:13 -05:00
Eric Normand
dae088f934 Databases maintain key order 2025-10-29 12:37:59 -05:00
Eric Normand
983bf8b002 Maintain model key order 2025-10-29 12:35:22 -05:00
Eric Normand
709a494ad4 :ref->:name in export 2025-10-29 12:32:20 -05:00
Eric Normand
29a5c1d804 Metric maintains order of keys 2025-10-29 12:32:05 -05:00
Eric Normand
d2a4f43bfa Maintain order of question keys 2025-10-29 12:31:54 -05:00
Eric Normand
9f04d4525c Docstring 2025-10-29 12:17:00 -05:00
Timothy Dean
6a98142d10 name -> display_name, ref -> name 2025-10-29 11:16:04 -06:00
Eric Normand
6e075b3fee MBQL5 fields appear to work 2025-10-29 12:04:58 -05:00
Eric Normand
f4f2fae807 Also use database: in referred to databases 2025-10-29 11:57:25 -05:00
Eric Normand
4b1783d724 ALways use query key 2025-10-29 11:57:02 -05:00
Eric Normand
9bb863e36f Reduce tables on export 2025-10-29 11:00:19 -05:00
Eric Normand
18b558507c Reduce database tables on export 2025-10-29 10:52:54 -05:00
Eric Normand
60af228ce8 Don't convert to markdown for now 2025-10-29 10:52:38 -05:00
Eric Normand
4ebe289c9e Represetnations button on database admin screen 2025-10-29 10:27:37 -05:00
Eric Normand
b96378dd1d Try deps free version 2025-10-29 09:57:35 -05:00
Eric Normand
80a1ee7544 Change to tiptap 2025-10-29 09:51:35 -05:00
Eric Normand
190b660861 Refs for documents 2025-10-28 16:39:25 -05:00
Eric Normand
69a0182908 Can you handle the whitespace? 2025-10-28 16:13:20 -05:00
Eric Normand
096c943179 Better document export 2025-10-28 15:53:53 -05:00
Eric Normand
3250dbbe3b Document prosemirror to markdown 2025-10-28 15:53:53 -05:00
Timothy Dean
eb27ad00a2 Copy changes from metabase/representations 2025-10-28 12:03:41 -06:00
Timothy Dean
e5d50e6e41 Add :local/root to deps.edn for copy of metabase/representations 2025-10-28 11:33:39 -06:00
Timothy Dean
e876cc59ff Copy in metabase/representations 2025-10-28 11:19:52 -06:00
Timothy Dean
04f22bce1c Revert "Attempt to bring in metabase/representations as submodule"
This reverts commit 38edca362b.
2025-10-28 11:12:19 -06:00
Timothy Dean
fe38c6dbfb Revert "Maybe github CI can use submodules: true"
This reverts commit ab8d8e1a85.
2025-10-28 11:12:05 -06:00
Timothy Dean
ab8d8e1a85 Maybe github CI can use submodules: true 2025-10-28 11:06:40 -06:00
Timothy Dean
38edca362b Attempt to bring in metabase/representations as submodule 2025-10-28 11:00:11 -06:00
Eric Normand
481d5c7dd7 Fix card question 2025-10-28 11:47:55 -05:00
Eric Normand
bd407a7a23 Use native-only-query? 2025-10-28 11:17:34 -05:00
Eric Normand
81d235e1f9 Docstring 2025-10-28 10:35:48 -05:00
Eric Normand
a6e8e815e1 Correct env var problem 2025-10-28 10:34:21 -05:00
Eric Normand
20bc080cbe Source cards and native queris 2025-10-28 10:34:21 -05:00
Timothy Dean
9489190f9b Merge remote-tracking branch 'origin/master' into representations 2025-10-28 09:21:47 -06:00
Timothy Dean
481b92c289 Remove whitespace 2025-10-28 09:21:03 -06:00
Timothy Dean
adcc8e3e5c Merge remote-tracking branch 'origin/master' into representations 2025-10-28 09:17:36 -06:00
Eric Normand
d9fff0a2e7 Documents and collections representations 2025-10-27 18:38:41 -05:00
Eric Normand
ebe192cc51 Yes, transforms too. 2025-10-27 18:10:05 -05:00
Eric Normand
fbe0f1a83b Better UI, more cards 2025-10-27 18:10:05 -05:00
Timothy Dean
5f100521aa More use of lookup-database-id 2025-10-27 15:59:37 -06:00
Timothy Dean
dcc2e5b777 disable test, add comments 2025-10-27 14:58:26 -06:00
Timothy Dean
3152844182 Transforms code is stale and broken 2025-10-27 14:52:32 -06:00
Timothy Dean
6cd4d53aaa Fix typo in snippet-test and add uses of with-toucan-defaults 2025-10-27 14:42:53 -06:00
Timothy Dean
de157d98f1 Use v0-common/map-entity-index in more places 2025-10-27 14:37:16 -06:00
Timothy Dean
64a914bb6c Getting tests up to date 2025-10-27 14:22:59 -06:00
Timothy Dean
b392c931ef Delete stale test 2025-10-27 14:13:27 -06:00
Timothy Dean
57dce27409 Fix bug in new lookup-id impl for maps, update common-test 2025-10-27 14:10:05 -06:00
Timothy Dean
7bbf5f082e Update stale reference to old with-toucan-defaults 2025-10-27 14:00:25 -06:00
Timothy Dean
6a90502eaa Fix CI issue: create lookup/lookup-database-id 2025-10-27 13:51:02 -06:00
Timothy Dean
be07b8fe0f Remove metabase/representations dependency so CI can run 2025-10-27 13:41:19 -06:00
Timothy Dean
802765a6b4 Fix with-toucan-defaults, add resolve-database-id, get more tests passing 2025-10-27 13:40:28 -06:00
Eric Normand
8e4d85d37b Get a modal. It works. Could be better. 2025-10-27 14:12:20 -05:00
Timothy Dean
ae17a6f937 Fix compilation error in representations.export 2025-10-27 11:41:01 -06:00
Timothy Dean
cafc5540c0 Add metabase/representations dependency 2025-10-27 11:41:01 -06:00
Eric Normand
ccc6c717d2 Missing yamls 2025-10-27 12:26:17 -05:00
Timothy Dean
944cefd0b5 Merge remote-tracking branch 'origin/master' into representations 2025-10-27 11:05:48 -06:00
Eric Normand
8bd2e87737 WIP document 2025-10-24 14:25:59 -05:00
Eric Normand
9927a4abcb Add with-toucan-defaults 2025-10-23 14:37:20 -05:00
Eric Normand
b087de7b13 Guard against infinite loops in order-representations 2025-10-23 12:23:37 -05:00
Eric Normand
3760109621 Robust tests against faulty cycle detection 2025-10-22 16:59:44 -05:00
Eric Normand
c44514f19e Cleanup and test 2025-10-22 16:50:44 -05:00
Eric Normand
d197607055 Add a first implementation of a RefIndex 2025-10-22 15:32:37 -05:00
Eric Normand
5b4b835a45 Move representation-type to common 2025-10-22 15:17:09 -05:00
Eric Normand
9cb6173c3f Cycle detection in order-representations 2025-10-22 14:44:28 -05:00
Eric Normand
04fb2f80b8 Cleanup 2025-10-22 12:47:34 -05:00
Eric Normand
434e4c77b4 Whitespace 2025-10-22 12:35:46 -05:00
Eric Normand
fa86a2f729 Convert type->model to multimethod 2025-10-22 12:35:22 -05:00
Eric Normand
3e8cc7d241 Add docstrings in lookup 2025-10-22 12:01:12 -05:00
Eric Normand
9672377eff Add some todos for migration in comments 2025-10-22 12:00:52 -05:00
Eric Normand
2d25ff6801 More testing and polishing 2025-10-20 15:10:03 -05:00
Eric Normand
23c32a9679 More lookup functions 2025-10-20 13:37:11 -05:00
Eric Normand
192a3f8cab Good tests for exporting based on types 2025-10-20 13:21:01 -05:00
Eric Normand
d48e2e167f Test lookup and type conversion 2025-10-20 13:07:07 -05:00
Eric Normand
0fc449bb3f First pass at better import (WIP) 2025-10-14 13:55:33 +02:00
Eric Normand
0ccfcf9368 At leasts the tests pass 2025-10-10 23:06:49 -05:00
Eric Normand
f920a0ad6b Work with lib in transforms??? 2025-10-10 22:59:52 -05:00
Eric Normand
ad03eb5e71 Fix transform tests 2025-10-10 22:54:28 -05:00
Eric Normand
968ab66658 Get lib questions working (with tests) 2025-10-10 22:41:56 -05:00
Timothy Dean
531c0a2ec0 Tweaking mbql.clj 2025-10-10 13:46:38 -06:00
Timothy Dean
361b089ae6 Fix nesting bug in mbql.clj, re-enable a models test 2025-10-10 13:13:50 -06:00
Timothy Dean
f868deee6b Models and their metadata working much better 2025-10-10 12:55:18 -06:00
Timothy Dean
e24dd51f77 Land bugfixes in flow we're not using any longer, but it's related to the collections.yml ticket 2025-10-10 12:54:41 -06:00
Eric Normand
a8c3bf86bf Missed a reference 2025-10-10 12:31:11 -05:00
Eric Normand
f77ee2c518 Oops cyclical dep 2025-10-10 12:27:42 -05:00
Eric Normand
126768e0b4 Cleanup mbql stuff 2025-10-10 12:27:08 -05:00
Riley Thompson
a0088a70e2 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-10 13:24:43 -04:00
Riley Thompson
64860019bc add snippet import tests 2025-10-10 13:24:33 -04:00
Eric Normand
b899d8cd97 Update test 2025-10-10 11:23:54 -05:00
Eric Normand
fc0f79e339 Export only schemas and tables that are referred to 2025-10-10 11:22:55 -05:00
Riley Thompson
60050841a2 snippet export test 2025-10-09 18:35:32 -04:00
Eric Normand
bc8d0d0423 Metrics import/export tests 2025-10-09 15:02:19 -05:00
Riley Thompson
75de8e0572 remove static serve code from 3312f91 and 89bf939 2025-10-09 15:21:57 -04:00
Riley Thompson
9b1198ce7b remove frontend code 2025-10-09 14:50:58 -04:00
Riley Thompson
f1cb9c64fd use refs directory for db representations 2025-10-09 11:43:48 -04:00
Riley Thompson
80af50a637 replace content with sql in snippet representation 2025-10-09 11:29:22 -04:00
Riley Thompson
0ac69e8f89 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-08 16:33:14 -04:00
Riley Thompson
2b3a76cf5b use lib native to build template tags 2025-10-08 16:33:09 -04:00
Eric Normand
a4acab9eff Dependency check API and beginnings of import 2025-10-08 13:32:22 -05:00
Riley Thompson
8dd09eaf20 Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-08 14:06:53 -04:00
Riley Thompson
085b060c4c add template tags to sql snippetes yaml->toucan 2025-10-08 14:06:43 -04:00
Timothy Dean
fb34fc1654 babashka import: manually fix a few issues, but the code organization is pretty bad 2025-10-08 11:00:46 -06:00
Riley Thompson
ca9f37c05f replace versioned :type key with separate :version and :type keys 2025-10-08 12:16:47 -04:00
Timothy Dean
2ebdcb6139 Init commit for representation.bb 2025-10-07 19:00:23 -06:00
Riley Thompson
1300e4fe2d Merge branch 'representations' of https://github.com/metabase/metabase into representations 2025-10-07 18:30:55 -04:00
Riley Thompson
ee5c71754a sql snippet representations 2025-10-07 18:30:49 -04:00
Eric Normand
e0790dda10 Structural change for better recursion 2025-10-07 16:21:41 -05:00
Eric Normand
ca30b7d361 Export entire collection (API) 2025-10-07 16:18:08 -05:00
Eric Normand
6697c05014 Can check question dependencies 2025-10-07 15:39:54 -05:00
Eric Normand
56e70074d0 Merge branch 'representations' of github.com:metabase/metabase into representations 2025-10-07 12:15:01 -05:00
Eric Normand
7e0b5e85cd Don't use ref/ namespace 2025-10-07 12:14:34 -05:00
Timothy Dean
e7154969aa Delete a ton of code and experimental feature work. 2025-10-07 10:46:25 -06:00
Timothy Dean
471e76be64 Whitespace 2025-10-06 13:01:42 -06:00
Timothy Dean
25908d5f96 Fix core-test 2025-10-06 12:53:41 -06:00
Timothy Dean
42f5d6cc13 Merge remote-tracking branch 'origin/master' into representations 2025-10-06 12:44:46 -06:00
Timothy Dean
e3a9edcb7a Fix question-test 2025-10-06 12:41:56 -06:00
Timothy Dean
cf1c863b2b Fix YAML dumper-options, filenames, misc. 2025-10-06 12:19:26 -06:00
Timothy Dean
f7db036c73 Add latest export examples 2025-10-06 12:18:09 -06:00
Timothy Dean
49cb9edfa7 Fix topological sort for base64-encoded mbql 2025-10-06 11:10:36 -06:00
Timothy Dean
0fd583ee05 Non-cleaned-up, but new tests and metadata overrides for models. Mix of hand and vibe code, cleanup needed 2025-10-04 00:57:43 -06:00
Timothy Dean
a33fee2435 MBQL separated as base64-encoded blob 2025-10-03 23:33:53 -06:00
Timothy Dean
de107e5639 Add forgotten file. 2025-10-03 17:20:59 -06:00
Timothy Dean
339ce9b24c export-import-with-refs-test 2025-10-03 17:12:09 -06:00
Timothy Dean
07b6dcd9ea export-with-refs and export-with-ids 2025-10-03 16:56:57 -06:00
Timothy Dean
560a3b67f0 Cleanup, about to refactor to allow bimodal exports 2025-10-03 15:39:08 -06:00
Eric Normand
d8ae013339 Ooops! 2025-10-03 15:58:36 -05:00
Eric Normand
ad06c09272 Transforms work! 2025-10-03 15:35:38 -05:00
Timothy Dean
b0d38931d0 Some fixes, mostly around models 2025-10-03 12:15:50 -06:00
Eric Normand
30f997054b Make question tests pass 2025-10-01 16:51:44 -05:00
Eric Normand
bc73878e2a Fix kondo and remove bad docstring 2025-10-01 14:07:41 -05:00
Eric Normand
2bdaa1ca68 Make export-set take a collection of reps 2025-10-01 14:07:23 -05:00
Eric Normand
949633567d Whitespace in cond 2025-10-01 14:06:57 -05:00
Eric Normand
ee68edecae Fix question test 2025-10-01 14:06:45 -05:00
Eric Normand
6b055d5133 Use u/remove-nils instead of custom one 2025-10-01 13:36:55 -05:00
Timothy Dean
c39e42692c Fix circular dependencies 2025-09-30 23:55:11 -06:00
Timothy Dean
34dc6533b2 Cleaning up a little, and resolving 104 kondo warnings 2025-09-30 22:52:08 -06:00
Timothy Dean
46bf926ee3 Get import/export buttons working more nicely (for database.yml and question.yml only) 2025-09-30 16:13:12 -06:00
Riley Thompson
54389ff03a fix collection script 2025-09-30 17:10:01 -04:00
Riley Thompson
13a76e983e Merge branch 'master' into representations 2025-09-30 17:04:15 -04:00
Riley Thompson
34b84f3151 download and upload transform represenations 2025-09-30 17:02:04 -04:00
Timothy Dean
d65e9c8ec4 Merge duplicated work, get batch-imports working for at least questions and databases.
Eric and I duplicated some work. This merges our efforts. This also builds in ref patching on import and export and provides an `import-dir!` method that #worksonmymachine.

Co-authored-by: Eric Normand <ericwnormand@gmail.com>
2025-09-29 14:23:59 -06:00
Timothy Dean
29263dad18 Fix circular dependency, clean up some code 2025-09-26 15:15:13 -06:00
Eric Normand
b70431b8f6 Match existing database by name or do nothing. 2025-09-26 12:47:46 -05:00
Eric Normand
57c7b6e15c Database api and better yaml format 2025-09-26 11:45:03 -05:00
Eric Normand
9b0a44d385 Update database export
Env vars for passwords and other secure details
2025-09-26 11:41:56 -05:00
Eric Normand
c0bec3f32e Remove question 2025-09-26 11:41:38 -05:00
Eric Normand
2510bae3c2 Tests 2025-09-26 11:09:46 -05:00
Eric Normand
1dad0e5c1e Import ordering 2025-09-25 15:25:27 -05:00
Eric Normand
a9be96bbd2 Merge branch 'representations' of github.com:metabase/metabase into representations 2025-09-25 13:23:18 -05:00
Eric Normand
1625842d80 Whitespace 2025-09-25 13:21:02 -05:00
Eric Normand
9af2a858cb Export databases. 2025-09-25 13:19:18 -05:00
Eric Normand
942fcbd2d0 Remove prints 2025-09-25 13:17:56 -05:00
Timothy Dean
67566c475b m. 2025-09-24 17:58:23 -06:00
Timothy Dean
e1bf8c6582 Basic database export functionality 2025-09-24 17:56:06 -06:00
Timothy Dean
069d773c94 Make collection-8 test_resources dir mirroring /collection/8-serialization-v0 2025-09-24 15:47:25 -06:00
Eric Normand
82029ecfbb We're not ready for import-export 2025-09-24 14:26:24 -05:00
Riley Thompson
7070fac7a6 add frontend 2025-09-24 14:15:42 -04:00
Timothy Dean
2653b9d303 Merge remote-tracking branch 'origin/master' into representations 2025-09-24 11:39:14 -06:00
Riley Thompson
addf6fda01 export and import collection representation endpoints 2025-09-24 13:00:51 -04:00
Timothy Dean
4a2651851b representations.core: remove unnecessary decoder code 2025-09-24 10:34:09 -06:00
Timothy Dean
2f80bab68f Fix transform schema 2025-09-24 10:21:03 -06:00
Timothy Dean
a136a66af7 Just delete run-trigger 2025-09-23 17:02:05 -06:00
Timothy Dean
aab38f089a Transform improvements 2025-09-23 16:51:09 -06:00
Eric Normand
8dd1b8c4cc import/export tests for questions 2025-09-23 16:02:37 -05:00
Eric Normand
d8e3b30a83 Add transforms export 2025-09-23 14:35:42 -05:00
Eric Normand
6b9a0c0277 Add roundtrip tests 2025-09-23 14:31:12 -05:00
Timothy Dean
774cc91a4e Transforms schema tweaks, ingestion. Persist method not well tested 2025-09-23 10:22:30 -06:00
Eric Normand
0d185aa777 Export metrics 2025-09-23 10:10:22 -05:00
Eric Normand
4434205bc0 Add model test 2025-09-23 09:34:19 -05:00
Timothy Dean
9fc8a2724d Add validate-representation script 2025-09-22 14:05:04 -06:00
Timothy Dean
94a5098f74 Add load-representation script 2025-09-22 13:44:14 -06:00
Timothy Dean
5c788efbe1 Questions and models from serialization-v0 collection 2025-09-22 12:47:58 -06:00
Eric Normand
dc9bbc95d5 More question tests 2025-09-22 11:39:47 -05:00
Eric Normand
29e408bf72 Merge branch 'master' into representations 2025-09-22 10:06:21 -05:00
Timothy Dean
9398a2f092 Push experimental model yml file 2025-09-20 01:45:06 -05:00
Timothy Dean
bed66ab743 Fix yarn dev 2025-09-20 01:39:08 -05:00
Timothy Dean
ed931e2404 Playing with model metadata 2025-09-20 01:38:13 -05:00
Timothy Dean
afa3d6d70c Fix the /load endpoint 2025-09-20 01:37:15 -05:00
dan sutton
d616d269f9 fixup tests
need db to run db tests. and validation checks against schema
2025-09-19 17:36:31 -05:00
Eric Normand
ea1ef9c283 Comment 2025-09-19 16:57:54 -05:00
Eric Normand
639fcf2a3e Add question tests 2025-09-19 16:54:31 -05:00
dan sutton
89bf939860 referencing questions in static assets
```
❯ tree /tmp/behold
/tmp/behold
├── c-1-card196164.card.yml
├── c-2-card196324.card.yml
├── sub-collection
│   └── c-3-card12345.card.yml
└── with-references
    ├── question-122.yml
    └── question-125.yml

3 directories, 5 files
```

the with-references directory is as follows:

```yaml
name: kondo_lints_question
type: question
ref: question-122
mbql_query:
  type: query
  database: pg
  query:
    source-table:
    - pg
    - public
    - kondo_lints
    fields:
    - - field
      - - pg
        - public
        - kondo_lints
        - id
      - base-type: type/Integer
    - - field
      - - pg
        - public
        - kondo_lints
        - filename
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - line
      - base-type: type/Integer
    - - field
      - - pg
        - public
        - kondo_lints
        - status
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - error
      - base-type: type/Text
    - - field
      - - pg
        - public
        - kondo_lints
        - category
      - base-type: type/Text
database: pg
```

and 125:

```yaml
name: kondo_lints_question, Filtered by Status is error
type: question
ref: question-125
mbql_query:
  database: pg
  type: query
  query:
    source-table:
      ref: question-122
    filter:
    - =
    - - field
      - status
      - base-type: type/Text
    - error
database: pg
```

It _references_ another static, non-app-db question

```clojure
core=> (:dataset_query (fetch :question -5))
{:database 3,
 :type "query",
 :query {:source-table "card__-4", ;; <----- card -4
         :filter ["="
                  ["field" "status" {:base-type "type/Text"}]
                  "error"]}}
core=> (->> (qp/process-query (:dataset_query (fetch :question -5)))
            :data :rows (take 2))
([12933
  "shared/src/metabase/mbql/normalize.cljc"
  594
  "error"
  "Recur can only be used in tail position."
  "shared/src/metabase/mbql/normalize.cljc"]
 [12934
  "shared/src/metabase/mbql/normalize.cljc"
  594
  "error"
  "Unresolved symbol"
  "shared/src/metabase/mbql/normalize.cljc"])
```
2025-09-19 16:38:05 -05:00
Eric Normand
c635152b92 Fixes 2025-09-19 15:50:30 -05:00
Eric Normand
7e8d562358 Move export functions 2025-09-19 14:25:21 -05:00
Eric Normand
320aab30bc whitespace 2025-09-19 14:25:16 -05:00
Eric Normand
968cf8c336 Add a tiny test 2025-09-19 13:37:15 -05:00
Eric Normand
f4f22cb733 Revert all disablings 2025-09-19 13:33:30 -05:00
Timothy Dean
48239abdd3 Add an MBQL model.yml file 2025-09-19 12:24:33 -06:00
Eric Normand
2c40e5f217 kick ci 2025-09-19 13:22:56 -05:00
Eric Normand
38550465eb disable frontend tests? 2025-09-19 13:20:19 -05:00
Eric Normand
8f73866c87 Disable frontend tests? 2025-09-19 13:19:20 -05:00
Eric Normand
d1625d7384 Skip frontend? 2025-09-19 13:18:11 -05:00
Eric Normand
e22c93ed4c try skip 2025-09-19 13:11:23 -05:00
Eric Normand
a229d08410 remove stuff from run-tests.yml 2025-09-19 13:07:49 -05:00
Eric Normand
35aec6c7cb Revert "Delete e2e tests files"
This reverts commit 58c4abdd10.
2025-09-19 13:05:51 -05:00
Eric Normand
58c4abdd10 Delete e2e tests files 2025-09-19 13:04:02 -05:00
Eric Normand
08e9272440 Get representation tests to run???? 2025-09-19 12:51:38 -05:00
Eric Normand
5c9303e6e3 Attempt to disable tests 2025-09-19 12:49:47 -05:00
Eric Normand
554c6388b3 Remove more tests 2025-09-19 12:41:13 -05:00
Eric Normand
f21d0de52e Disable backend test runs 2025-09-19 12:11:19 -05:00
Eric Normand
8dc61e4eb0 Disable more tests 2025-09-19 12:08:10 -05:00
Timothy Dean
270fcf4299 Reorganize code again based on huddle discussion 2025-09-19 11:06:43 -06:00
Eric Normand
77cfe40954 kick ci 2025-09-19 11:58:19 -05:00
Eric Normand
6c4f743f38 Attempt: Disable driver tests and add representation tests runner 2025-09-19 11:56:39 -05:00
Eric Normand
65a5a67624 wip 2025-09-19 11:06:15 -05:00
Timothy Dean
abaac8c3f7 m. Rename 2025-09-19 09:20:07 -06:00
Timothy Dean
e1406025a9 Provide a wrapper for yaml->toucan operations 2025-09-19 09:18:41 -06:00
dan sutton
3312f915ee Static serving
given

```
❯ tree /tmp/swaperoo
/tmp/swaperoo
├── c-1-card196164.card.yml
├── c-2-card196324.card.yml
└── sub-collection
    └── c-3-card12345.card.yml

2 directories, 3 files
```

the running

```
clj -M:dev:ee:ee-dev:drivers:drivers-dev:socket -m metabase.core.bootstrap static --directory /tmp/swaperoo
```

Will serve the assets from /tmp/swaperoo.
It does this by loading them in MEMORY and assigning them sequential
negative ids. These are not loaded into the app-db.

```
❯ tree /tmp/swaperoo
/tmp/swaperoo  (collection -1: swaperoo)
├── c-1-card196164.card.yml (card -1)
├── c-2-card196324.card.yml (card -2)
└── sub-collection (collection -2: sub-collection)
    └── c-3-card12345.card.yml (card -3)

```
2025-09-19 09:32:02 -05:00
Timothy Dean
3809c909f0 Create an invalid example yml file 2025-09-18 16:44:09 -06:00
Timothy Dean
388f208bb2 Tweak model.yml 2025-09-18 16:28:12 -06:00
Eric Normand
55ef1e8392 card->question 2025-09-18 17:14:56 -05:00
Eric Normand
576535eaf7 dataset->model 2025-09-18 17:09:20 -05:00
Timothy Dean
78e3b5d282 Wire up model ingestion 2025-09-18 15:50:51 -06:00
Timothy Dean
ff96dabfcf Slightly improved /validate endpoint 2025-09-18 15:35:02 -06:00
Timothy Dean
3dece4143a WIP model ingestion, not wired up yet 2025-09-18 15:28:52 -06:00
Eric Normand
092674a6b8 string instead of keyword 2025-09-18 16:22:21 -05:00
dan sutton
4d46a3beb4 clean up representation -> card in memory representation 2025-09-18 16:21:33 -05:00
Eric Normand
edccda93bf Add bb script to export a collection 2025-09-18 16:18:05 -05:00
Timothy Dean
2d10da8267 Stop hardcoding :native on question ingestion 2025-09-18 15:11:16 -06:00
Timothy Dean
64d22caf90 Move creator-id and entity-id determination into representation->question-data 2025-09-18 15:00:24 -06:00
Eric Normand
c09d983f2c Add collection endpoint 2025-09-18 15:50:49 -05:00
Eric Normand
503c35172d Merge branch 'master' into representations 2025-09-18 15:02:12 -05:00
Eric Normand
57906d9ddc kick ci 2025-09-18 14:59:28 -05:00
Eric Normand
0e2fdcdfef Merge branch 'representations' of github.com:metabase/metabase into representations 2025-09-18 13:55:55 -05:00
Eric Normand
83f6814775 kick ci 2025-09-18 13:55:37 -05:00
Timothy Dean
1bb21b1ad7 Add /validate endpoint to API 2025-09-18 11:46:51 -06:00
Timothy Dean
fb62d65095 m. 2025-09-18 11:37:10 -06:00
Timothy Dean
ad1423767a Better example model.yml 2025-09-18 11:36:14 -06:00
Timothy Dean
ef661cc1d1 Add example model yml with some schema 2025-09-18 11:29:51 -06:00
Timothy Dean
be445608ce Create representations.ingestion.common 2025-09-18 11:23:48 -06:00
Eric Normand
075ee44a6c Forgot this file 2025-09-18 12:05:25 -05:00
Eric Normand
407e6dd824 Export models
Note: I copied the export for questions, including the logic and schema. I don't know if there's something else in
models (metadata?) that I am not capturing.
2025-09-18 12:04:35 -05:00
Eric Normand
5b86da8b8b Export mbql questions 2025-09-18 11:01:06 -05:00
Eric Normand
39d9982ae4 API for exporting a native query question.
GET /api/ee/representation/question/92
2025-09-18 10:38:10 -05:00
Timothy Dean
530b985bc4 tweak commented out dev utils 2025-09-17 17:35:24 -06:00
Timothy Dean
ab8afe34b1 Delete 'card' as a named entity, reorganize namespaces as cleanup 2025-09-17 17:31:45 -06:00
Eric Normand
26b4d85352 API route
curl -X POST -H "Content-Type: text/yaml" -H "X-API-Key: mb_5Qj3t1s5NYcXNq1jB3wIebP3rAhLcmmgkSBp8YTvXOU=" --data-binary @test_resources/representations/v0/monthly-revenue.question.yml localhost:3002/api/ee/representation/load/5
2025-09-17 17:30:44 -05:00
Timothy Dean
558a2f91b2 On card ingestion, update existing card if ref matches 2025-09-17 16:24:56 -06:00
Timothy Dean
6b9381b859 Tweak collection schema 2025-09-17 16:15:48 -06:00
Timothy Dean
0cfa725615 Hook up card ingestion 2025-09-17 15:50:36 -06:00
Timothy Dean
7d74867058 Ingest one example question.yml file and insert it into the app-db 2025-09-17 15:42:20 -06:00
Timothy Dean
bf3a3e9e00 WIP: ingest & validate yml against schemas 2025-09-17 12:30:48 -06:00
dan sutton
bcca5c2329 first pass at writing representations
Interesting things:
- using serdes mbql export. this puts nano-ids, so we walk over each card, get mapping of nano-id to id, then replace
with refs. So the ingestion will need to do the opposite.

  ;; do we like entity type in filename?
  ;; walk over all, identity which depend on other refs. so first, get all refs
  ;; then walk over each dataset_query, looking for any refs that are reffed
  ;; then do topological sort, inserting cards without deps and keeping a map of ref to inserted id
  ;; update existing refs to ids (note source-tables are "card__<id>"
  ;; insert and continue

```clojure
core=> (write-em "/tmp" 8)
.
..nil
```

Very dump to write dots but this is exploratory, baby.

example:

(note, rows in yaml are actually commented out but git would remove them)
```yaml
❯ tree custom
custom
├── c-115-card1150883.card.yml
├── c-116-card1151398.card.yml
└── c-117-card1150868.card.yml

1 directory, 3 files

/tmp on ☁️  metabase-query
❯ cat custom/*
name: Orders and weird column
version: 1-card
type: card
ref: c-115-card1150883
dataset_query:
  database: Sample Database
  type: query
  query:
    source-table:
    - Sample Database
    - PUBLIC
    - ORDERS
    expressions:
      sum-of-weird:
      - +
      - - field
        - - Sample Database
          - PUBLIC
          - ORDERS
          - TAX
        - base-type: type/Float
      - - field
        - - Sample Database
          - PUBLIC
          - ORDERS
          - TOTAL
        - base-type: type/Float

 [1 1 14 37.648145389078365 2.07 39.718145389078366 nil "2025-02-11T21:40:27.892Z" 2 41.788145389078366 "Hudson Borer" "Awesome Concrete Shoes"]
 [2 1 123 110.93145648834248 6.1 117.03145648834247 nil "2024-05-15T08:04:04.58Z" 3 123.13145648834247 "Hudson Borer" "Mediocre Wooden Bench"]
 [3 1 105 52.723521442619514 2.9 49.206842233769756 6.416679208849759 "2025-12-06T22:22:48.544Z" 2 52.106842233769754 "Hudson Borer" "Fantastic Wool Shirt"]
 [4 1 94 109.21864156655383 6.01 115.22864156655383 nil "2025-08-22T16:30:42.392Z" 6 121.23864156655384 "Hudson Borer" "Awesome Bronze Plate"]
 [5 1 132 127.88197029833711 7.03 134.9119702983371 nil "2024-10-10T03:34:47.309Z" 5 141.9419702983371 "Hudson Borer" "Sleek Steel Table"]
 [6 1 60 29.80214751859149 1.64 31.44214751859149 nil "2025-11-06T16:38:50.134Z" 3 33.08214751859149 "Hudson Borer" "Rustic Paper Car"]
 [7 1 55 95.77128575934437 5.27 101.04128575934436 nil "2024-09-11T11:22:26.521Z" 5 106.31128575934436 "Hudson Borer" "Mediocre Rubber Shoes"]
 [8 1 65 68.22769726470014 3.75 63.32374433449318 8.653952930206964 "2025-06-17T02:37:41.693Z" 7 67.07374433449317 "Hudson Borer" "Ergonomic Silk Table"]
 [9 1 184 77.3982748679465 4.26 78.06353271268733 3.594742155259162 "2023-05-03T16:00:54.923Z" 3 82.32353271268734 "Hudson Borer" "Practical Granite Plate"]
 [10 1 6 97.43621265344382 5.36 102.79621265344382 nil "2026-01-17T01:44:37.233Z" 2 108.15621265344382 "Hudson Borer" "Small Marble Hat"]
name: Orders and weird column, Filtered by sum-of-weird is greater than 10
version: 1-card
type: card
ref: c-116-card1151398
dataset_query:
  database: Sample Database
  type: query
  query:
    source-table: c-115-card1150883
    filter:
    - '>'
    - - field
      - sum-of-weird
      - base-type: type/Float
    - 10

 [1 1 14 37.648145389078365 2.07 39.718145389078366 nil "2025-02-11T21:40:27.892Z" 2 41.788145389078366 "Hudson Borer" "Awesome Concrete Shoes"]
 [2 1 123 110.93145648834248 6.1 117.03145648834247 nil "2024-05-15T08:04:04.58Z" 3 123.13145648834247 "Hudson Borer" "Mediocre Wooden Bench"]
 [3 1 105 52.723521442619514 2.9 49.206842233769756 6.416679208849759 "2025-12-06T22:22:48.544Z" 2 52.106842233769754 "Hudson Borer" "Fantastic Wool Shirt"]
 [4 1 94 109.21864156655383 6.01 115.22864156655383 nil "2025-08-22T16:30:42.392Z" 6 121.23864156655384 "Hudson Borer" "Awesome Bronze Plate"]
 [5 1 132 127.88197029833711 7.03 134.9119702983371 nil "2024-10-10T03:34:47.309Z" 5 141.9419702983371 "Hudson Borer" "Sleek Steel Table"]
 [6 1 60 29.80214751859149 1.64 31.44214751859149 nil "2025-11-06T16:38:50.134Z" 3 33.08214751859149 "Hudson Borer" "Rustic Paper Car"]
 [7 1 55 95.77128575934437 5.27 101.04128575934436 nil "2024-09-11T11:22:26.521Z" 5 106.31128575934436 "Hudson Borer" "Mediocre Rubber Shoes"]
 [8 1 65 68.22769726470014 3.75 63.32374433449318 8.653952930206964 "2025-06-17T02:37:41.693Z" 7 67.07374433449317 "Hudson Borer" "Ergonomic Silk Table"]
 [9 1 184 77.3982748679465 4.26 78.06353271268733 3.594742155259162 "2023-05-03T16:00:54.923Z" 3 82.32353271268734 "Hudson Borer" "Practical Granite Plate"]
 [10 1 6 97.43621265344382 5.36 102.79621265344382 nil "2026-01-17T01:44:37.233Z" 2 108.15621265344382 "Hudson Borer" "Small Marble Hat"]
name: 5 products
version: 1-card
type: card
ref: c-117-card1150868
dataset_query:
  database: Sample Database
  type: native
  native:
    template-tags:
      category_filter:
        widget-type: string/=
        display-name: Category Filter
        type: dimension
        name: category_filter
        id: b5cca8e2-d397-4195-9957-1f69fda66d95
        dimension:
        - field
        - - Sample Database
          - PUBLIC
          - PRODUCTS
          - CATEGORY
        - null
    query: select * from products where {{category_filter}} limit 5

 [1 "1018947080336" "Rustic Paper Wallet" "Gizmo" "Swaniawski, Casper and Hilll" 29.463261130679875 4.6 "2023-07-19T19:44:56.582Z"]
 [2 "7663515285824" "Small Marble Shoes" "Doohickey" "Balistreri-Ankunding" 70.07989613071763 0.0 "2025-04-11T08:49:35.932Z"]
 [3 "4966277046676" "Synergistic Granite Chair" "Doohickey" "Murray, Watsica and Wunsch" 35.388744881539054 4.0 "2024-09-08T22:03:20.239Z"]
 [4 "4134502155718" "Enormous Aluminum Shirt" "Doohickey" "Regan Bradtke and Sons" 73.99178100854834 3.0 "2024-03-06T02:53:09.937Z"]
 [5 "5499736705597" "Enormous Marble Wallet" "Gadget" "Price, Schultz and Daniel" 82.7450976850356 4.0 "2022-10-03T01:47:39.147Z"]
```
2025-09-16 16:49:54 -05:00
Timothy Dean
25c142a7aa v0 scaffolding collection.yml 2025-09-16 15:43:43 -06:00
Timothy Dean
2d53706de7 v0 database scaffolding 2025-09-16 15:36:14 -06:00
Timothy Dean
ae6f82e0ee Scaffolding: clj schemas 2025-09-16 15:16:19 -06:00
Timothy Dean
219685e26f Scaffolding 2025-09-16 15:01:02 -06:00
Timothy Dean
1f892946c9 Scaffolding 2025-09-16 14:53:43 -06:00
Timothy Dean
cdacf75b02 Init commit 2025-09-16 13:59:53 -06:00
1334 changed files with 45386 additions and 9742 deletions

View File

@@ -14,6 +14,12 @@
- **Run a test:** `./bin/mage run-tests namespace/test-name`
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
- **Run all tests for a module:** `./bin/mage run-tests test/metabase/notification` Because the module lives in that directory.
Note: the `./bin/mage run-tests` command accepts multiple args, so you can pass
`./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test`
to run 3 tests, or
`./bin/mage run-tests test/metabase/module1 test/metabase/module2` to run 2 modules.
## Code Readability

View File

@@ -752,7 +752,10 @@
;; `metabase-enterprise.sandbox.query-processor*-test`
;;
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
:name general-qp-and-driver-test-namespaces}]
:name general-qp-and-driver-test-namespaces}
{:pattern "(?:^mage.*)"
:name mage-namespaces}]
:config-in-ns
{test-namespaces
@@ -947,4 +950,11 @@
toucan2.tools.with-temp/with-temp {:message "Use lib.tu/mock-metadata-provider in QP tests. Exceptions: tests for code that doesn't use MPs or models not supported by MPs"}}
:metabase/disallow-hardcoded-driver-names-in-tests
{:level :warning}}}}}
{:level :warning}}}
mage-namespaces
{:linters
{:discouraged-java-method
{java.lang.System {exit
{:level :error
:message "Don't use System/exit directly. Instead throw an ex-info with {:babashka/exit <exit-code>} (and optionally :mage/error) let the mage runner handle exiting the process"}}}}}}}

View File

@@ -237,6 +237,7 @@
:api #{metabase.bookmarks.api}
:uses #{api
app-db
collections
config
premium-features
queries
@@ -679,6 +680,7 @@
metabase.lib.join.util
metabase.lib.metadata
metabase.lib.metadata.cached-provider
metabase.lib.metadata.column
metabase.lib.metadata.invocation-tracker
metabase.lib.metadata.protocols
metabase.lib.metadata.result-metadata
@@ -1530,6 +1532,7 @@
api
app-db
audit-app
config
driver
lib
lib-be
@@ -1554,6 +1557,7 @@
lib
models
parameters
premium-features
query-processor
request
sync
@@ -1613,7 +1617,6 @@
warehouses
enterprise/advanced-permissions}}
xrays
{:team "Querying"
:api #{metabase.xrays.api
@@ -1840,6 +1843,22 @@
:api #{}
:uses #{premium-features}}
enterprise/data-studio
{:team "Gadget"
:api #{metabase-enterprise.data-studio.api}
:uses #{api
collections
database-routing
driver
events
lib
lib-be
queries
request
sync
util
warehouse-schema}}
enterprise/database-replication
{:team "Cloud"
:api #{metabase-enterprise.database-replication.api
@@ -1876,7 +1895,6 @@
:uses #{analyze
api
collections
config
driver
events
graph
@@ -1975,6 +1993,13 @@
:uses #{premium-features
settings}}
enterprise/library
{:team "UX West"
:api #{metabase-enterprise.library.api}
:uses #{api
collections
premium-features}}
enterprise/llm
{:team "Metabot"
:api #{metabase-enterprise.llm.api
@@ -2073,6 +2098,21 @@
util
enterprise/serialization}}
enterprise/representations
{:team "Querying"
:api #{metabase-enterprise.representations.core
metabase-enterprise.representations.api
metabase-enterprise.representations.init}
:uses #{api
collections
collections-rest
config
driver
lib
lib-be
models
util}}
enterprise/sandbox
{:team "UX West"
:api #{metabase-enterprise.sandbox.api.routes
@@ -2150,6 +2190,7 @@
app-db
appearance
collections
config
logger
models
plugins
@@ -2250,6 +2291,8 @@
app-db
config
driver
lib
lib-be
query-processor
settings
util

View File

@@ -241,6 +241,7 @@ module.exports = {
plugins: ["@typescript-eslint"],
rules: {
"prefer-rest-params": "off",
"react/prop-types": "off", // TypeScript handles prop validation
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",

View File

@@ -11,9 +11,18 @@ shared_specs: &shared_specs
- "test/**/*.cljc"
- "test/**/*.cljs"
frontend_ci: &frontend_ci
shared_ci: &shared_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/run-tests.yml"
shared_e2e: &shared_e2e
- "e2e/runner/**"
- "e2e/snapshot-creators/**"
- "e2e/support/**"
frontend_ci: &frontend_ci
- *shared_ci
- ".github/workflows/frontend.yml"
frontend_configs: &frontend_configs
@@ -36,14 +45,12 @@ frontend_sources: &frontend_sources
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/conventional-commit-pr-title-reminder.yml
frontend_embedding_sources:
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
frontend_embedding_sdk_sources:
- *frontend_configs
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/embedding-sdk-labels-reminder.yml
frontend_embedding_sdk_package_sources:
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
@@ -71,8 +78,7 @@ frontend_loki_ci: &frontend_loki_ci
- ".storybook/**"
backend_ci: &backend_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- *shared_ci
- ".github/actions/test-driver/**"
- ".github/workflows/backend.yml"
- ".github/workflows/drivers.yml"
@@ -112,9 +118,9 @@ sources: &sources
- *backend_sources
e2e_ci: &e2e_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- *shared_ci
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/e2e-prepare-containers/**"
- ".github/actions/prepare-cypress/**"
- ".github/workflows/e2e-*"
@@ -123,22 +129,26 @@ e2e_specs: &e2e_specs
- e2e/!(test-host-app)**/*.cy.*.{js,ts}
embedding_sdk_ci: &embedding_sdk_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- *shared_ci
- ".github/actions/prepare-uberjar-artifact/**"
- ".github/actions/fetch-artifact/action.yml"
- ".github/actions/prepare-cypress/**"
- ".github/workflows/embedding-sdk*.yml"
embedding_sdk_components:
- *default
- *embedding_sdk_ci
- *sources
- "e2e/component/**/*.cy.*.{js,ts}"
- *shared_e2e
- "e2e/test-component/**/*.cy.*.{js,jsx,ts,tsx}"
embedding_sdk_host_sample_apps:
- *default
- *embedding_sdk_ci
- *frontend_sources
- *backend_sources
- *shared_e2e
- "e2e/embedding-sdk-host-apps/**"
- "e2e/runner/embedding-sdk/**"
- "e2e/test-host-app/**/*.cy.*.{js,ts}"
snowplow: &snowplow

5
.github/team.json vendored
View File

@@ -50,7 +50,7 @@
"nemanjaglumac"
]
},
{
{
"name": "Metabot",
"label": ".Team/Metabot",
"members": [
@@ -92,7 +92,8 @@
"deniskaber",
"alxnddr",
"bpander",
"andsalves"
"andsalves",
"gantoreno"
]
},
{

View File

@@ -51,14 +51,19 @@ jobs:
eslint-cache-key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
generate-m2-cache:
generate-caches:
runs-on: ubuntu-22.04
timeout-minutes: 15
timeout-minutes: 30
needs: get-cache-keys
env:
M2_CACHE_KEY: ${{ needs.get-cache-keys.outputs.m2-cache-key }}
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
steps:
- uses: actions/checkout@v4
# Backend preparation and cache
- name: Prepare backend
uses: ./.github/actions/prepare-backend
@@ -75,16 +80,7 @@ jobs:
~/.gitlibs
key: ${{ env.M2_CACHE_KEY }}
generate-frontend-caches:
runs-on: ubuntu-22.04
timeout-minutes: 15
needs: get-cache-keys
env:
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
steps:
- uses: actions/checkout@v4
# Frontend preparation and caches
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
# It then (3) runs `yarn install --frozen-lockfile --prefer-offline` which ensures the local cache is fully populated.
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
@@ -111,6 +107,11 @@ jobs:
yarn cypress verify
shell: bash
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
# so it gets saved as part of the node_modules cache below
- name: Run Prettier to populate cache
run: yarn lint-prettier-pure
- name: Update node_modules cache
uses: ./.github/actions/update-cache
with:
@@ -119,14 +120,21 @@ jobs:
node_modules
key: ${{ env.NODE_CACHE_KEY }}
- name: Update eslint cache
uses: ./.github/actions/update-cache
with:
path: ${{ github.workspace}}/.eslintcache
key: ${{ env.ESLINT_CACHE_KEY }}
- name: Update Cypress cache
uses: ./.github/actions/update-cache
with:
path: ~/.cache/Cypress
key: ${{ env.CYPRESS_CACHE_KEY }}
# ESLint cache (requires both backend and frontend)
- name: Compile CLJS
run: yarn build-pure:cljs
- name: Run ESLint to populate cache
run: yarn lint-eslint-pure
- name: Update eslint cache
uses: ./.github/actions/update-cache
with:
path: ${{ github.workspace}}/.eslintcache
key: ${{ env.ESLINT_CACHE_KEY }}

View File

@@ -23,6 +23,7 @@ concurrency:
jobs:
review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
permissions:
contents: read
pull-requests: write

View File

@@ -972,12 +972,22 @@ jobs:
test-args: >-
:only-tags [:mb/upload-tests]
:fail-fast? true
- name: Redshift (Excluding Upload Tests)
- name: Redshift (Excluding Upload Tests) Part 1
skip: false
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 0
- name: Redshift (Excluding Upload Tests) Part 2
skip: false
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
:fail-fast? true
:partition/total 2
:partition/index 1
- name: Redshift Transforms Python Tests
skip: false
test-args: >-

View File

@@ -22,14 +22,10 @@ on:
required: false
default: "ee"
qa_db:
description: "Select the type of Database you want to run your tests with"
type: choice
description: "Whether to set up and include external QA databases"
type: boolean
required: true
default: "none"
options:
- none
- sql
- mongo
default: false
enable_network_throttling:
description: "Enable network throttling simulation (4g slow like in chrome dev tools)"
type: boolean
@@ -81,8 +77,6 @@ jobs:
name: Stress test E2E flake fix
env:
DISPLAY: ""
QA_DB_ENABLED: ${{ contains(fromJSON('["sql", "mongo"]'), inputs.qa_db) }}
CYPRESS_QA_DB_MONGO: ${{ inputs.qa_db == 'mongo' }}
MB_RUN_MODE: e2e
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
@@ -109,9 +103,9 @@ jobs:
maildev: true
openldap: true
snowplow: true
postgres: ${{ inputs.qa_db == 'sql' }}
mysql: ${{ inputs.qa_db == 'sql' }}
mongo: ${{ inputs.qa_db == 'mongo' }}
postgres: ${{ inputs.qa_db }}
mysql: ${{ inputs.qa_db }}
mongo: ${{ inputs.qa_db }}
- name: Download Metabase ${{ matrix.edition }} uberjar
uses: ./.github/actions/e2e-download-uberjar
@@ -140,8 +134,11 @@ jobs:
uses: ./.github/actions/prepare-cypress
- name: Run Metabase
run: node e2e/runner/run_cypress_ci.js start
- name: Generate database snapshots
run: node e2e/runner/run_cypress_ci.js snapshot
run: |
node e2e/runner/run_cypress_ci.js snapshot \
--env grepTags="${{inputs.qa_db && '' || '-@external'}}"
# For all options and fine-grained control, take a look at the documentation
# https://github.com/cypress-io/cypress/tree/develop/npm/grep

View File

@@ -37,7 +37,6 @@ jobs:
env:
MB_EDITION: ${{ inputs.edition }}
DISPLAY: ""
QA_DB_ENABLED: true
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
@@ -58,7 +57,6 @@ jobs:
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
TZ: US/Pacific # to make node match the instance tz
CYPRESS_CI: true
CYPRESS_QA_DB_MONGO: ${{ inputs.name == 'mongo' }}
steps:
- uses: actions/checkout@v4
with:
@@ -80,9 +78,9 @@ jobs:
openldap: true
webhook: true
snowplow: true
postgres: ${{ inputs.name != 'mongo' }}
mysql: ${{ inputs.name != 'mongo' }}
mongo: ${{ inputs.name == 'mongo' }}
postgres: true
mysql: true
mongo: true
- name: Retrieve uberjar artifact for ${{ inputs.edition }}
uses: ./.github/actions/fetch-artifact

View File

@@ -62,7 +62,10 @@ jobs:
- name: Make app db snapshot
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js snapshot
# Component tests only need the default snapshot
run: |
node e2e/runner/run_cypress_ci.js snapshot \
--spec "e2e/snapshot-creators/default.cy.snap.js"
# Has to be run after the app db snapshot is made, as it is not compatible with React 19
- name: Change React version to ${{ matrix.react-version }}
@@ -201,7 +204,10 @@ jobs:
- name: Make app db snapshot
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js snapshot
# Component tests only need the default snapshot
run: |
node e2e/runner/run_cypress_ci.js snapshot \
--spec "e2e/snapshot-creators/default.cy.snap.js"
- name: Run component tests for Embedding SDK
env:

View File

@@ -278,7 +278,9 @@ jobs:
run: node e2e/runner/run_cypress_ci.js start
- name: Generate database snapshots
run: node e2e/runner/run_cypress_ci.js snapshot
run: |
node e2e/runner/run_cypress_ci.js snapshot \
--spec "e2e/snapshot-creators/default.cy.snap.js"
- name: Prepare and launch Host App
run: npx tsx ./e2e/runner/embedding-sdk/host-apps/start-ci.ts ${{ matrix.test_suite }}

View File

@@ -23,6 +23,10 @@ on:
description: "Whether to run host and sample apps compatibility tests."
required: true
type: boolean
skip:
description: Whether to skip running tests due to one of the artifacts failing to build.
required: true
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
@@ -30,7 +34,7 @@ concurrency:
jobs:
embedding-sdk-cli-snippets-type-check:
if: inputs.cli-snippets-type-check
if: inputs.cli-snippets-type-check && !inputs.skip
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -46,7 +50,7 @@ jobs:
run: yarn run embedding-sdk:cli-snippets:type-check
embedding-sdk-docs-snippets-type-check:
if: inputs.docs-snippets-type-check
if: inputs.docs-snippets-type-check && !inputs.skip
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -62,7 +66,7 @@ jobs:
run: yarn run embedding-sdk:docs-snippets:type-check
embedding-sdk-api-documentation-generation-validation:
if: inputs.documentation-validation
if: inputs.documentation-validation && !inputs.skip
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -110,13 +114,13 @@ jobs:
# See `Component e2e tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
embedding-sdk-component-tests:
name: "SDK component tests"
if: inputs.component-tests
if: inputs.component-tests && !inputs.skip
uses: ./.github/workflows/embedding-sdk-component-tests.yml
secrets: inherit
not-required-embedding-sdk-apps-compatibility-tests:
name: "SDK e2e tests for compatibility with (host/sample) apps"
if: inputs.host-sample-apps-tests
if: inputs.host-sample-apps-tests && !inputs.skip
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
secrets: inherit

View File

@@ -25,7 +25,6 @@ jobs:
frontend_all: ${{ steps.changes.outputs.frontend_all }}
frontend_sources: ${{ steps.changes.outputs.frontend_sources }}
documentation: ${{ steps.changes.outputs.documentation }}
frontend_embedding_sdk_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_sources }}
embedding_documentation: ${{ steps.changes.outputs.embedding_documentation }}
embedding_sdk_components: ${{ steps.changes.outputs.embedding_sdk_components }}
embedding_sdk_host_sample_apps: ${{ steps.changes.outputs.embedding_sdk_host_sample_apps }}
@@ -102,7 +101,7 @@ jobs:
if: |
!cancelled() &&
(needs.files-changed.outputs.embedding_sdk_components == 'true' ||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' ||
needs.files-changed.outputs.frontend_sources == 'true' ||
needs.files-changed.outputs.embedding_documentation == 'true' ||
needs.files-changed.outputs.embedding_sdk_ci == 'true')
uses: ./.github/workflows/embedding-sdk-package-build.yml
@@ -110,17 +109,16 @@ jobs:
sdk-tests:
needs: [uberjar, embedding-sdk-package, files-changed]
if: |
needs.embedding-sdk-package.result == 'success' &&
needs.uberjar.result == 'success'
if: always()
uses: ./.github/workflows/embedding-sdk.yml
secrets: inherit
with:
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}
docs-snippets-type-check: ${{ needs.files-changed.outputs.embedding_documentation == 'true' }}
documentation-validation: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
documentation-validation: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}
component-tests: ${{ needs.files-changed.outputs.embedding_sdk_components == 'true' }}
host-sample-apps-tests: ${{ needs.files-changed.outputs.embedding_sdk_host_sample_apps == 'true' }}
skip: ${{ needs.embedding-sdk-package.result != 'success' || needs.uberjar.result != 'success' }}
pr-env:
needs: [uberjar]

View File

@@ -146,7 +146,13 @@ jobs:
runs-on: ubuntu-22.04
name: Is ${{ matrix.edition }} (java ${{ matrix.java-version }}) healthy?
needs: [build, check-existing-artifacts]
if: always() && !cancelled() && (needs.build.result == 'success' || (needs.check-existing-artifacts.outputs.should_build == 'false' && needs.check-existing-artifacts.outputs.ee_exists == 'true' && needs.check-existing-artifacts.outputs.oss_exists == 'true'))
# We only care about the explicit JAR health check on push to `master` and the `release-` branches.
# JARs created in random branches on PRs are 99% of the time treated as ephemeral, and used only in e2e tests.
# E2E tests already have a step where they wait for the instance health check.
if: |
always() && !cancelled() &&
(github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) &&
(needs.build.result == 'success' || (needs.check-existing-artifacts.outputs.should_build == 'false' && needs.check-existing-artifacts.outputs.ee_exists == 'true' && needs.check-existing-artifacts.outputs.oss_exists == 'true'))
timeout-minutes: 10
strategy:
matrix:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 56 KiB

56
bb.edn
View File

@@ -3,10 +3,11 @@
;; so the namespaces are mage.cli, mage.format, etc.
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
:deps {dev.weavejester/cljfmt {:mvn/version "0.15.3"}
org.babashka/json {:mvn/version "0.1.6"}
io.github.paintparty/bling {:mvn/version "0.8.8"}
metosin/malli {:mvn/version "0.17.0"}
table/table {:mvn/version "0.5.0"}
mvxcvi/puget {:mvn/version "1.3.4"}}
mvxcvi/puget {:mvn/version "1.3.4"}
org.babashka/json {:mvn/version "0.1.6"}
table/table {:mvn/version "0.5.0"}}
:tasks
{:requires [[mage.cli :as cli]
[mage.color :as c]]
@@ -186,24 +187,21 @@
(nrepl-eval "metabase-enterprise.comments.core" code)))}
run-tests
{:doc "Run a tests (or a single test) against nrepl server"
:requires [[clojure.string :as str]
[mage.be-dev :refer [nrepl-eval]]]
:arg-schema [:tuple [:string {:name "NS/TEST" :desc "or just NS"}]]
:options [["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
:task (let [{:keys [options]
[test] :arguments} (cli/parse! (current-task))
test (if (or (str/starts-with? test "test/")
(str/starts-with? test "enterprise/"))
;; paths should come in as strings
(format "\"%s\"" test)
;; ns or var should be a symbol
(str "'" test))
code (format
"(do ((requiring-resolve 'dev.reload/reload!)) (find-and-run-tests-repl {:only %s}))"
test)]
(nrepl-eval "mb.hawk.core" (str code) (:port options)))}
{:doc "Quickly run tests locally. Pass dirs/files as arguments to run them."
: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"]]
;; 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 {:description "Files or directories (recursive) to test"}]]
:requires [[mage.quick-test-runner :as qtr]]
:task (task! (qtr/go parsed))}
affected-modules
{:doc "Prints information about which backend modules have changes compared to a Git ref e.g. `master`"
:examples [["./bin/mage affected-modules master"]
@@ -218,10 +216,11 @@
:requires [[mage.modules]]
:task (task! (mage.modules/cli-can-skip-driver-tests (:arguments parsed)))}
ls {:doc "List all mage public and private tasks"
:examples [["./bin/mage ls" "List all tasks"]]
:requires [[mage.util :as u]]
:task (task! (u/print-tasks parsed))}
ls
{:doc "List all mage public and private tasks"
:examples [["./bin/mage ls" "List all tasks"]]
:requires [[mage.util :as u]]
:task (task! (u/print-tasks parsed))}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Private Tasks:
@@ -276,8 +275,7 @@
(apply readability/check arguments))}
;; Uncomment this if you want to play with the example calculator task:
#_#_
-example-calculator
#_#_-example-calculator
{;; `:doc` is a short docstring for the task, will be listed in `./bin/mage -h` and other help info.
;; Keep these short, they need to fit on one line in the output from `./bin/mage -h`.
:doc "The example calculator's task docstring"
@@ -334,6 +332,6 @@
-escherize-fix-whitespace
{:doc "Removes all lines like '^<space>+$' from files in glob pattern."
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
:examples [["mage -escherize-fix-whitespace '**/*.clj'" "Removes all lines like ' ' from all clj files"]]
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
:arg-schema [:sequential [:string {:description "Glob pattern to match files"}]]
:task (task! (fix-whitespace/run-clean parsed))}}}
:task (task! (fix-whitespace/run-clean parsed))}}}

60
bin/build-backend-for-test Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Build a backend-only uberjar for e2e testing.
#
# This script builds a minimal backend uberjar (metabase-backend.jar) that contains
# only the Clojure backend code. It is NOT the same as the all-in-one metabase.jar
# which includes backend, frontend, static assets, and other resources.
#
# The backend-only jar is used for e2e tests where the frontend is served separately
# and we only need the backend API server.
#
# Features:
# - Outputs to target/uberjar/metabase-backend.jar
# - Caches builds based on source file hashes to avoid unnecessary rebuilds
# - Embeds a source hash in version.properties to detect when rebuilds are needed
set -eu
readonly VERSION_PROPERTY_NAME="src_hash"
readonly JAR_FILENAME="metabase-backend.jar"
readonly JAR_PATH="target/uberjar/$JAR_FILENAME"
backend-source-hash() {
# hash all the files that might change a backend-only uberjar build
(
find src deps.edn resources/sample-database.db.mv.db -type f -print0 | xargs -0 shasum ;
find resources -type f \( -iname \*.clj -o -iname \*.edn -o -iname \*.yaml -o -iname \*.properties -o -iname \*.html \) -not -name "version.properties" -print0 | xargs -0 shasum ;
) | shasum | awk '{ print $1 }'
}
backend-jar-hash() {
# pulling the version.properties directly from the jar is much faster than starting the JVM
unzip -c "$JAR_PATH" version.properties | grep "$VERSION_PROPERTY_NAME" | cut -f2 -d=
}
check-backend-jar-hash() {
local expected_hash actual_hash
expected_hash=$(backend-source-hash)
actual_hash=$(backend-jar-hash)
if [ "$expected_hash" == "$actual_hash" ]; then
return 0
else
return 1
fi
}
build-backend-uberjar() {
local source_hash
source_hash=$(backend-source-hash)
./bin/build.sh :steps [:version]
echo -e "\n$VERSION_PROPERTY_NAME=$source_hash" >> resources/version.properties
MB_JAR_FILENAME="$JAR_FILENAME" ./bin/build.sh :steps [:uberjar]
}
if [ ! -f "$JAR_PATH" ] || ! check-backend-jar-hash; then
echo "Building backend uberjar for e2e testing ($JAR_FILENAME)"
build-backend-uberjar
else
echo "Backend uberjar already up to date for e2e testing ($JAR_FILENAME)"
fi

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env bash
set -eu
VERSION_PROPERTY_NAME="src_hash"
source-hash() {
# hash all the files that might change a backend-only uberjar build (for integration tests)
(
find src deps.edn resources/sample-database.db.mv.db -type f -print0 | xargs -0 shasum ;
find resources -type f \( -iname \*.clj -o -iname \*.edn -o -iname \*.yaml -o -iname \*.properties -o -iname \*.html \) -not -name "version.properties" -print0 | xargs -0 shasum ;
) | shasum | awk '{ print $1 }'
}
uberjar-hash() {
# java --add-opens java.base/java.nio=ALL-UNNAMED -jar target/uberjar/metabase.jar version | grep -oE 'source_hash [a-f0-9]+' | awk '{ print $2 }'
# pulling the version.properties directly from the jar is much faster
unzip -c target/uberjar/metabase.jar version.properties | grep "$VERSION_PROPERTY_NAME" | cut -f2 -d=
}
check-uberjar-hash() {
expected_hash=$(source-hash)
actual_hash=$(uberjar-hash)
if [ "$expected_hash" == "$actual_hash" ]; then
return 0
else
return 1
fi
}
build-uberjar-for-test() {
./bin/build.sh :steps [:version]
echo -e "\n$VERSION_PROPERTY_NAME=$(source-hash)" >> resources/version.properties
./bin/build.sh :steps [:uberjar]
}
if [ ! -f "target/uberjar/metabase.jar" ] || ! check-uberjar-hash; then
echo "Building uberjar for testing"
build-uberjar-for-test
else
echo "Uberjar already up to date for testing"
fi

View File

@@ -18,9 +18,18 @@
(def ^:private class-dir
(u/filename u/project-root-directory "target" "classes"))
(def ^:private uberjar-dir
"Canonical directory for all uberjar builds."
(u/filename u/project-root-directory "target" "uberjar"))
(def ^:private artifact-name
"The uberjar artifact name, customizable via the MB_JAR_FILENAME env var."
(or (System/getenv "MB_JAR_FILENAME")
"metabase.jar"))
(def uberjar-filename
"Target filename for the Metabase uberjar."
(u/filename u/project-root-directory "target" "uberjar" "metabase.jar"))
"Full path to the Metabase uberjar, including the artifact name."
(u/filename uberjar-dir artifact-name))
(defn- do-with-duration-ms [thunk f]
(let [timer (u/start-timer)
@@ -164,7 +173,7 @@
to do it by hand for the time being."
[]
(u/step "Update META-INF/MANIFEST.MF"
(u/with-open-jar-file-system [fs "target/uberjar/metabase.jar"]
(u/with-open-jar-file-system [fs uberjar-filename]
(let [manifest-path (u/get-path-in-filesystem fs "META-INF" "MANIFEST.MF")]
(with-open [os (Files/newOutputStream manifest-path (into-array OpenOption [StandardOpenOption/WRITE
StandardOpenOption/TRUNCATE_EXISTING]))]
@@ -184,5 +193,4 @@
(copy-resources! basis)
(create-uberjar! basis)
(update-manifest!))
(u/announce "Built target/uberjar/metabase.jar in %.1f seconds."
(/ duration-ms 1000.0)))))
(u/announce "Built %s in %.1f seconds." uberjar-filename (/ duration-ms 1000.0)))))

389
bin/dev-requirements Executable file
View File

@@ -0,0 +1,389 @@
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Print functions
print_header() {
echo -e "\n${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${BLUE} $1${NC}"
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
}
print_section() {
echo -e "\n${BOLD}$1${NC}"
echo -e "${BOLD}$(printf '─%.0s' {1..63})${NC}"
}
print_found() {
echo -e " ${GREEN}✓${NC} $1"
}
print_not_found() {
echo -e " ${RED}✗${NC} $1"
}
print_warning() {
echo -e " ${YELLOW}⚠️${NC} $1"
}
print_info() {
echo -e " ${BLUE}ℹ${NC} $1"
}
# Detection functions
detect_version_managers() {
local managers=()
# nvm - Node Version Manager
if [ -d "$HOME/.nvm" ] || [ -n "${NVM_DIR:-}" ] || command -v nvm &> /dev/null; then
managers+=("nvm")
fi
# fnm - Fast Node Manager
if command -v fnm &> /dev/null || [ -d "$HOME/.fnm" ]; then
managers+=("fnm")
fi
# n - Node version management (no subcommands)
if command -v n &> /dev/null || [ -d "/usr/local/n" ]; then
managers+=("n")
fi
# volta - JavaScript Tool Manager
if command -v volta &> /dev/null || [ -d "$HOME/.volta" ]; then
managers+=("volta")
fi
# asdf - Extendable version manager (multi-language)
if [ -d "$HOME/.asdf" ] || command -v asdf &> /dev/null; then
managers+=("asdf")
fi
# mise - Dev tools version manager (formerly rtx)
if command -v mise &> /dev/null || [ -d "$HOME/.local/share/mise" ] || [ -d "$HOME/.mise" ]; then
managers+=("mise")
fi
# jenv - Java version manager
if [ -d "$HOME/.jenv" ] || command -v jenv &> /dev/null; then
managers+=("jenv")
fi
# SDKMAN! - Software Development Kit Manager (Java, Gradle, Maven, etc.)
if [ -d "$HOME/.sdkman" ] || [ -n "${SDKMAN_DIR:-}" ]; then
managers+=("sdkman")
fi
echo "${managers[@]}"
}
get_node_version() {
if command -v node &> /dev/null; then
node --version
else
echo ""
fi
}
get_yarn_version() {
if command -v yarn &> /dev/null; then
yarn --version
else
echo ""
fi
}
get_java_version() {
if command -v java &> /dev/null; then
java -version 2>&1 | head -n 1 | awk -F '"' '{print $2}'
else
echo ""
fi
}
get_java_distribution() {
if command -v java &> /dev/null; then
local full_version=$(java -version 2>&1)
# Check for various distributions
if echo "$full_version" | grep -qi "Temurin"; then
echo "Eclipse Temurin"
elif echo "$full_version" | grep -qi "Adoptium"; then
echo "Eclipse Adoptium"
elif echo "$full_version" | grep -qi "Oracle"; then
echo "Oracle"
elif echo "$full_version" | grep -qi "OpenJDK"; then
echo "OpenJDK"
elif echo "$full_version" | grep -qi "Amazon Corretto"; then
echo "Amazon Corretto"
elif echo "$full_version" | grep -qi "Azul Zulu"; then
echo "Azul Zulu"
elif echo "$full_version" | grep -qi "GraalVM"; then
echo "GraalVM"
else
echo "Unknown"
fi
else
echo ""
fi
}
check_java_requirements() {
local version=$1
local distribution=$2
local major_version=""
if [ -n "$version" ]; then
# Extract major version (handles both "21.0.1" and "1.8.0" formats)
if [[ $version =~ ^1\. ]]; then
major_version=$(echo "$version" | cut -d. -f2)
else
major_version=$(echo "$version" | cut -d. -f1)
fi
# Check version and distribution
if [ "$major_version" = "21" ] && [[ "$distribution" =~ "Temurin" ]]; then
echo "correct"
elif [ "$major_version" = "21" ]; then
echo "wrong_distribution"
else
echo "wrong_version"
fi
else
echo "not_installed"
fi
}
get_clojure_version() {
if command -v clojure &> /dev/null; then
clojure --version 2>&1 | head -n 1
else
echo ""
fi
}
get_required_clojure_version() {
if [ -f "deps.edn" ]; then
# Extract org.clojure/clojure version from deps.edn
grep "org.clojure/clojure" deps.edn | grep ":mvn/version" | sed -E 's/.*"([0-9.]+)".*/\1/' | head -n 1
else
echo ""
fi
}
get_babashka_version() {
if command -v bb &> /dev/null; then
bb --version
else
echo ""
fi
}
check_yarn_classic() {
local version=$1
if [ -n "$version" ]; then
local major_version=$(echo "$version" | cut -d. -f1)
if [ "$major_version" = "1" ]; then
return 0
fi
fi
return 1
}
main() {
print_header "Metabase Development Environment Detection"
echo "This script detects your current development tool setup and provides"
echo "guidance on how to prepare your environment for Metabase development."
echo ""
echo "Note: This script only detects and reports. It will not modify your system."
print_section "Version Managers Detected"
managers=($(detect_version_managers))
if [ ${#managers[@]} -eq 0 ]; then
print_info "No version managers detected"
echo " You may be using system-installed tools or manual installations."
else
for manager in "${managers[@]}"; do
print_found "$manager"
done
# Check for conflicts
if [ ${#managers[@]} -gt 1 ]; then
echo ""
print_warning "Multiple version managers detected!"
echo " Having multiple version managers can cause conflicts where different"
echo " tools compete to manage the same runtime versions. This may lead to:"
echo " • Unexpected version switches"
echo " • PATH conflicts"
echo " • Shell initialization slowdowns"
echo ""
echo -e " 💡 ${BOLD}Recommendation:${NC} Use only one version manager (we recommend mise)."
fi
fi
print_section "Installed Development Tools"
requirements_met=true
# Node.js
node_version=$(get_node_version)
if [ -n "$node_version" ]; then
node_major=$(echo "$node_version" | sed 's/v//' | cut -d. -f1)
if [ "$node_major" -ge 22 ]; then
print_found "Node.js $node_version (meets requirement: ≥22)"
else
print_warning "Node.js $node_version (required: ≥22)"
requirements_met=false
fi
else
print_not_found "Node.js (required: ≥22)"
requirements_met=false
fi
# Yarn
yarn_version=$(get_yarn_version)
if [ -n "$yarn_version" ]; then
if check_yarn_classic "$yarn_version"; then
print_found "Yarn $yarn_version (Yarn Classic - correct!)"
else
print_warning "Yarn $yarn_version (required: Yarn Classic 1.x)"
requirements_met=false
fi
else
print_not_found "Yarn (required: Yarn Classic 1.x)"
requirements_met=false
fi
# Java
java_version=$(get_java_version)
java_distribution=$(get_java_distribution)
java_status=$(check_java_requirements "$java_version" "$java_distribution")
case "$java_status" in
"correct")
print_found "Java $java_version ($java_distribution) - correct!"
;;
"wrong_distribution")
print_warning "Java $java_version ($java_distribution)"
echo " Required: Java 21 (Eclipse Temurin)"
requirements_met=false
;;
"wrong_version")
print_warning "Java $java_version ($java_distribution)"
echo " Required: Java 21 (Eclipse Temurin)"
requirements_met=false
;;
"not_installed")
print_not_found "Java (required: Java 21 Eclipse Temurin)"
requirements_met=false
;;
esac
# Clojure CLI
clojure_version=$(get_clojure_version)
required_clojure_version=$(get_required_clojure_version)
if [ -n "$clojure_version" ]; then
if [ -n "$required_clojure_version" ]; then
print_found "Clojure CLI ($clojure_version) - requires Clojure $required_clojure_version"
else
print_found "Clojure CLI ($clojure_version)"
fi
else
if [ -n "$required_clojure_version" ]; then
print_not_found "Clojure CLI (required for Clojure $required_clojure_version)"
else
print_not_found "Clojure CLI"
fi
requirements_met=false
fi
# Babashka
bb_version=$(get_babashka_version)
if [ -n "$bb_version" ]; then
print_found "Babashka $bb_version"
else
print_not_found "Babashka (required for mage task automation)"
requirements_met=false
fi
# Show warning if requirements are not met
if [ "$requirements_met" = false ]; then
echo ""
echo -e " ${BOLD}${RED}⚠ REQUIREMENTS NOT MET ⚠${NC}"
echo ""
echo -e " ${BOLD}Your current setup does not meet Metabase's requirements.${NC}"
echo " The project will not work until the issues above are resolved."
echo ""
echo " Please install or update the missing/incorrect tools listed above."
fi
print_section "Metabase Project Requirements"
required_clojure_version=$(get_required_clojure_version)
echo -e " ${BOLD}Required Tools:${NC}"
echo " • Node.js ≥22 (currently specified in .nvmrc)"
echo " • Yarn Classic 1.x (NOT Yarn 2+, NO Corepack)"
echo " • Java 21 with Eclipse Temurin distribution (for Clojure backend)"
if [ -n "$required_clojure_version" ]; then
echo " • Clojure CLI (for Clojure $required_clojure_version specified in deps.edn)"
else
echo " • Clojure CLI"
fi
echo " • Babashka (for mage task automation)"
echo ""
echo -e " ${BOLD}Optional Tools (quality of life):${NC}"
if command -v bat &> /dev/null; then
echo -e " ${GREEN}✓${NC} bat - A cat clone with syntax highlighting (https://github.com/sharkdp/bat)"
else
echo -e " ${RED}✗${NC} bat - A cat clone with syntax highlighting (https://github.com/sharkdp/bat)"
fi
if command -v fzf &> /dev/null; then
echo -e " ${GREEN}✓${NC} fzf - A fuzzy finder for the terminal (https://github.com/junegunn/fzf)"
else
echo -e " ${RED}✗${NC} fzf - A fuzzy finder for the terminal (https://github.com/junegunn/fzf)"
fi
if command -v fd &> /dev/null; then
echo -e " ${GREEN}✓${NC} fd - A fast alternative to find (https://github.com/sharkdp/fd)"
else
echo -e " ${RED}✗${NC} fd - A fast alternative to find (https://github.com/sharkdp/fd)"
fi
if command -v rg &> /dev/null; then
echo -e " ${GREEN}✓${NC} ripgrep - A fast alternative to grep (https://github.com/BurntSushi/ripgrep)"
else
echo -e " ${RED}✗${NC} ripgrep - A fast alternative to grep (https://github.com/BurntSushi/ripgrep)"
fi
print_section "Recommended Setup"
echo -e " ${BOLD}${GREEN}We recommend using 'mise'${NC} for version management because:"
echo " • Manages Node.js, Java, and other tools in one place"
echo " • Automatically switches versions per-project"
echo " • Cross-platform and actively maintained"
echo ""
echo " Learn more: https://mise.jdx.dev/getting-started.html"
# Footer
print_header "Detection Complete"
echo "For detailed development environment setup, see:"
echo " docs/developers-guide/devenv.md"
echo ""
echo "To re-run this detection script:"
echo " ./bin/dev-requirements"
echo ""
}
main "$@"

View File

@@ -9,6 +9,6 @@
"express": "^5.0.1",
"express-session": "^1.18.1",
"jsonwebtoken": "^9.0.2",
"next": "^15.2.3"
"next": "^15.4.7"
}
}

View File

@@ -2,185 +2,204 @@
# yarn lockfile v1
"@emnapi/runtime@^1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d"
integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==
"@emnapi/runtime@^1.7.0":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791"
integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==
dependencies:
tslib "^2.4.0"
"@img/sharp-darwin-arm64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz#65049ef7c6be7857da742cd028f97602ce209635"
integrity sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==
"@img/colour@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311"
integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==
"@img/sharp-darwin-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86"
integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.1.0"
"@img/sharp-libvips-darwin-arm64" "1.2.4"
"@img/sharp-darwin-x64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz#d37ff7c75c46d5a68a3756e3f1924ef7ca7b285e"
integrity sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==
"@img/sharp-darwin-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b"
integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.1.0"
"@img/sharp-libvips-darwin-x64" "1.2.4"
"@img/sharp-libvips-darwin-arm64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz#843f7c09c7245dc0d3cfec2b3c83bb08799a704f"
integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==
"@img/sharp-libvips-darwin-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
"@img/sharp-libvips-darwin-x64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz#1239c24426c06a8e833815562f78047a3bfbaaf8"
integrity sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==
"@img/sharp-libvips-darwin-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
"@img/sharp-libvips-linux-arm64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz#20d276cefd903ee483f0441ba35961679c286315"
integrity sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==
"@img/sharp-libvips-linux-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
"@img/sharp-libvips-linux-arm@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz#067c0b566eae8063738cf1b1db8f8a8573b5465c"
integrity sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==
"@img/sharp-libvips-linux-arm@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
"@img/sharp-libvips-linux-ppc64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802"
integrity sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==
"@img/sharp-libvips-linux-ppc64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
"@img/sharp-libvips-linux-s390x@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz#82fcd68444b3666384235279c145c2b28d8ee302"
integrity sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==
"@img/sharp-libvips-linux-riscv64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
"@img/sharp-libvips-linux-x64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz#65b2b908bf47156b0724fde9095676c83a18cf5a"
integrity sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==
"@img/sharp-libvips-linux-s390x@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
"@img/sharp-libvips-linuxmusl-arm64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz#72accf924e80b081c8db83b900b444a67c203f01"
integrity sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==
"@img/sharp-libvips-linux-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
"@img/sharp-libvips-linuxmusl-x64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz#1fa052737e203f46bf44192acd01f9faf11522d7"
integrity sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==
"@img/sharp-libvips-linuxmusl-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
"@img/sharp-linux-arm64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz#c9690fac5f3137eaab3f7ad6065390d10f66f1fa"
integrity sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==
"@img/sharp-libvips-linuxmusl-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
"@img/sharp-linux-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc"
integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.1.0"
"@img/sharp-libvips-linux-arm64" "1.2.4"
"@img/sharp-linux-arm@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz#771dd2ec645f85f98441359bfc118afaf38cbd8b"
integrity sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==
"@img/sharp-linux-arm@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d"
integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.1.0"
"@img/sharp-libvips-linux-arm" "1.2.4"
"@img/sharp-linux-s390x@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz#82132d158abff57bd90b53574f2865f72f94e6c8"
integrity sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==
"@img/sharp-linux-ppc64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813"
integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.1.0"
"@img/sharp-libvips-linux-ppc64" "1.2.4"
"@img/sharp-linux-x64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz#d815fb87899d462b28b62a9252ad127f02fe0740"
integrity sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==
"@img/sharp-linux-riscv64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60"
integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.1.0"
"@img/sharp-libvips-linux-riscv64" "1.2.4"
"@img/sharp-linuxmusl-arm64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz#cfac45b2abbc04628f676e123bfe3aeb300266c7"
integrity sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==
"@img/sharp-linux-s390x@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7"
integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.1.0"
"@img/sharp-libvips-linux-s390x" "1.2.4"
"@img/sharp-linuxmusl-x64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz#b876c23ff51d0fb6d9f3b0a07e2f4d1436c203ad"
integrity sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==
"@img/sharp-linux-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8"
integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.1.0"
"@img/sharp-libvips-linux-x64" "1.2.4"
"@img/sharp-wasm32@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz#b1dd0bab547dccf517586eb1fa5852160bba3b82"
integrity sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==
"@img/sharp-linuxmusl-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086"
integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
"@img/sharp-linuxmusl-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f"
integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
"@img/sharp-wasm32@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0"
integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==
dependencies:
"@emnapi/runtime" "^1.4.3"
"@emnapi/runtime" "^1.7.0"
"@img/sharp-win32-arm64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz#f37bee0f60c167f825a09d2b8de6849b823e8b30"
integrity sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==
"@img/sharp-win32-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
"@img/sharp-win32-ia32@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz#8fc30b6655bc6ff8910344a2020d334aa6361672"
integrity sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==
"@img/sharp-win32-ia32@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
"@img/sharp-win32-x64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz#ecf19250f8fe35de684aa2b0ec6f773b3447247b"
integrity sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==
"@img/sharp-win32-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
"@next/env@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.2.tgz#7143eafa9b11cfdf3d3c7318b0facb9dfdb2948f"
integrity sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==
"@next/env@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.6.tgz#7009d88d419a36a4ba9e110c151604444744a74d"
integrity sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==
"@next/swc-darwin-arm64@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz#1a7b36bf3c439f899065c878a580bc57a3630ec7"
integrity sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==
"@next/swc-darwin-arm64@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz#f80d3fe536f29f3217ca07d031f7b43862234059"
integrity sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==
"@next/swc-darwin-x64@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz#3742026344f49128cf1b0f43814c67e880db7361"
integrity sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==
"@next/swc-darwin-x64@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz#289334478617318a0d8d9f1f6661a15952f4e4ab"
integrity sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==
"@next/swc-linux-arm64-gnu@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz#fb29d45c034e3d2eef89b0e2801d62eb86155823"
integrity sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==
"@next/swc-linux-arm64-gnu@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz#efdd993cd9ad88b82c948c8e518e045566dd2f98"
integrity sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==
"@next/swc-linux-arm64-musl@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz#396784ef312666600ab1ae481e34cb1f6e3ae730"
integrity sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==
"@next/swc-linux-arm64-musl@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz#a45185126faf69eb65a017bd2c015ad7e86f5c84"
integrity sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==
"@next/swc-linux-x64-gnu@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz#ac01fda376878e02bc6b57d1e88ab8ceae9f868e"
integrity sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==
"@next/swc-linux-x64-gnu@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz#8174dc8e03a1f7df292bead360f83c53f8dd8b73"
integrity sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==
"@next/swc-linux-x64-musl@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz#327a5023003bcb3ca436efc08733f091bba2b1e8"
integrity sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==
"@next/swc-linux-x64-musl@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz#6d0776d81c5bd6a1780e6c39f32d7ef172900635"
integrity sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==
"@next/swc-win32-arm64-msvc@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz#ce3a6588bd9c020960704011ab20bd0440026965"
integrity sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==
"@next/swc-win32-arm64-msvc@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz#4b63d511b5c41278a48168fccb89cf00912411af"
integrity sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==
"@next/swc-win32-x64-msvc@15.3.2":
version "15.3.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz#43cc36097ac27639e9024a5ceaa6e7727fa968c8"
integrity sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==
"@swc/counter@0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@next/swc-win32-x64-msvc@15.5.6":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz#9ed5e84bd85009625dd35c444668d13061452a50"
integrity sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==
"@swc/helpers@0.5.15":
version "0.5.15"
@@ -310,13 +329,6 @@ buffer-equal-constant-time@1.0.1:
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
busboy@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
bytes@3.1.2, bytes@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -348,34 +360,6 @@ client-only@0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
dependencies:
color-convert "^2.0.1"
color-string "^1.9.0"
content-disposition@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2"
@@ -447,10 +431,10 @@ destroy@^1.2.0:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
detect-libc@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
detect-libc@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
dunder-proto@^1.0.1:
version "1.0.1"
@@ -665,11 +649,6 @@ ipaddr.js@1.9.1:
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-promise@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
@@ -812,28 +791,26 @@ negotiator@^1.0.0:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
next@^15.2.3:
version "15.3.2"
resolved "https://registry.yarnpkg.com/next/-/next-15.3.2.tgz#97510629e38a058dd154782a5c2ec9c9ab94d0d8"
integrity sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==
next@^15.4.7:
version "15.5.6"
resolved "https://registry.yarnpkg.com/next/-/next-15.5.6.tgz#16d9d1e9ba2e8caf82ba15e060a12286cd25db30"
integrity sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==
dependencies:
"@next/env" "15.3.2"
"@swc/counter" "0.1.3"
"@next/env" "15.5.6"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.3.2"
"@next/swc-darwin-x64" "15.3.2"
"@next/swc-linux-arm64-gnu" "15.3.2"
"@next/swc-linux-arm64-musl" "15.3.2"
"@next/swc-linux-x64-gnu" "15.3.2"
"@next/swc-linux-x64-musl" "15.3.2"
"@next/swc-win32-arm64-msvc" "15.3.2"
"@next/swc-win32-x64-msvc" "15.3.2"
sharp "^0.34.1"
"@next/swc-darwin-arm64" "15.5.6"
"@next/swc-darwin-x64" "15.5.6"
"@next/swc-linux-arm64-gnu" "15.5.6"
"@next/swc-linux-arm64-musl" "15.5.6"
"@next/swc-linux-x64-gnu" "15.5.6"
"@next/swc-linux-x64-musl" "15.5.6"
"@next/swc-win32-arm64-msvc" "15.5.6"
"@next/swc-win32-x64-msvc" "15.5.6"
sharp "^0.34.3"
object-assign@^4:
version "4.1.1"
@@ -954,10 +931,10 @@ semver@^7.5.4:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@^7.7.2:
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
semver@^7.7.3:
version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
send@^1.0.0, send@^1.1.0:
version "1.1.0"
@@ -992,36 +969,39 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
sharp@^0.34.1:
version "0.34.2"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.2.tgz#648bd639854dbe48047b0b420213c186d036b32d"
integrity sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==
sharp@^0.34.3:
version "0.34.5"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0"
integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==
dependencies:
color "^4.2.3"
detect-libc "^2.0.4"
semver "^7.7.2"
"@img/colour" "^1.0.0"
detect-libc "^2.1.2"
semver "^7.7.3"
optionalDependencies:
"@img/sharp-darwin-arm64" "0.34.2"
"@img/sharp-darwin-x64" "0.34.2"
"@img/sharp-libvips-darwin-arm64" "1.1.0"
"@img/sharp-libvips-darwin-x64" "1.1.0"
"@img/sharp-libvips-linux-arm" "1.1.0"
"@img/sharp-libvips-linux-arm64" "1.1.0"
"@img/sharp-libvips-linux-ppc64" "1.1.0"
"@img/sharp-libvips-linux-s390x" "1.1.0"
"@img/sharp-libvips-linux-x64" "1.1.0"
"@img/sharp-libvips-linuxmusl-arm64" "1.1.0"
"@img/sharp-libvips-linuxmusl-x64" "1.1.0"
"@img/sharp-linux-arm" "0.34.2"
"@img/sharp-linux-arm64" "0.34.2"
"@img/sharp-linux-s390x" "0.34.2"
"@img/sharp-linux-x64" "0.34.2"
"@img/sharp-linuxmusl-arm64" "0.34.2"
"@img/sharp-linuxmusl-x64" "0.34.2"
"@img/sharp-wasm32" "0.34.2"
"@img/sharp-win32-arm64" "0.34.2"
"@img/sharp-win32-ia32" "0.34.2"
"@img/sharp-win32-x64" "0.34.2"
"@img/sharp-darwin-arm64" "0.34.5"
"@img/sharp-darwin-x64" "0.34.5"
"@img/sharp-libvips-darwin-arm64" "1.2.4"
"@img/sharp-libvips-darwin-x64" "1.2.4"
"@img/sharp-libvips-linux-arm" "1.2.4"
"@img/sharp-libvips-linux-arm64" "1.2.4"
"@img/sharp-libvips-linux-ppc64" "1.2.4"
"@img/sharp-libvips-linux-riscv64" "1.2.4"
"@img/sharp-libvips-linux-s390x" "1.2.4"
"@img/sharp-libvips-linux-x64" "1.2.4"
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
"@img/sharp-linux-arm" "0.34.5"
"@img/sharp-linux-arm64" "0.34.5"
"@img/sharp-linux-ppc64" "0.34.5"
"@img/sharp-linux-riscv64" "0.34.5"
"@img/sharp-linux-s390x" "0.34.5"
"@img/sharp-linux-x64" "0.34.5"
"@img/sharp-linuxmusl-arm64" "0.34.5"
"@img/sharp-linuxmusl-x64" "0.34.5"
"@img/sharp-wasm32" "0.34.5"
"@img/sharp-win32-arm64" "0.34.5"
"@img/sharp-win32-ia32" "0.34.5"
"@img/sharp-win32-x64" "0.34.5"
side-channel-list@^1.0.0:
version "1.0.0"
@@ -1063,13 +1043,6 @@ side-channel@^1.0.6, side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
dependencies:
is-arrayish "^0.3.1"
source-map-js@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@@ -1080,11 +1053,6 @@ statuses@2.0.1, statuses@^2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
styled-jsx@5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499"

View File

@@ -62,6 +62,10 @@
(.getCause e)
(assoc :cause (summarize-exception (.getCause e)))))
(def ^:private ex-message-label (c/red (c/reverse-color "ex-message : ")))
(def ^:private ex-data-label (c/yellow (c/reverse-color "ex-data : ")))
(def ^:private mage-error-label (c/blue (c/reverse-color "mage/error : ")))
(defn -main [& _]
(cond
;; help
@@ -73,7 +77,9 @@
;; errors
(invalid-task?)
(do (print-help) (System/exit 1))
(do (print-help)
#_{:clj-kondo/ignore [:discouraged-java-method]}
(System/exit 1))
:else
;; at this point, we always have a valid task, and we are running in bb, so
@@ -101,13 +107,13 @@
(cond-> e
(not (u/env "MAGE_DEBUG" (constantly nil)))
summarize-exception)))
(when (and message (not (str/blank? message)))
(println (c/red (c/reverse-color "ex-message : ")) message))
(when data
(println (c/yellow (c/reverse-color "ex-data : ")) (pr-str
(dissoc data :mage/error))))
(when (not (str/blank? message))
(println ex-message-label message))
(when (not-empty (dissoc data :mage/error))
(println ex-data-label (u/pp (dissoc data :mage/error))))
(when (:mage/error data)
(println (c/blue (c/reverse-color "mage/error : ")) (:mage/error data)))
(println mage-error-label (:mage/error data)))
#_{:clj-kondo/ignore [:discouraged-java-method]}
(System/exit (:babashka/exit data 1))))))))))
(when (= *file* (System/getProperty "babashka.file"))

View File

@@ -1,17 +0,0 @@
{
"version": "1.0.0",
"ignored": {
"vars": [
"metabase.session.models.session-test/send-email-on-first-login-from-new-device-test",
"metabase-enterprise.impersonation.driver-test/nested-do-with-connection-with-options-test",
"metabase.warehouse-schema.api.table-test/update-table-sync-test",
"metabase.warehouses.api-test/can-rescan-fieldvalues-for-a-db",
"metabase-enterprise.transforms-python.transforms-api-test/python-transform-schema-change-integration-test"
]
},
"metadata": {
"last_updated": "2025-08-02",
"updated_by": "dpsutton",
"reason": "Two flakes: send-email-on-first-login-from-new-device-test has schema error if emails from another thread are present; nested-do-with-connection-with-options-test is causing deadlocks i believe."
}
}

View File

@@ -10,5 +10,4 @@
"MB_PRO_CLOUD_TOKEN": "get it from the password manager",
"MB_PRO_SELF_HOSTED_TOKEN": "get it from the password manager",
"MB_EDITION": "ee",
"QA_DB_ENABLED": "true"
}

View File

@@ -19,7 +19,7 @@
org.bouncycastle/bcprov-jdk18on]}
buddy/buddy-sign {:mvn/version "3.6.1-359"} ; JSON Web Tokens; High-Level message signing library
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} ; util functions for converting between camel, snake, and kebob case
cheshire/cheshire {:mvn/version "6.0.0"} ; fast JSON encoding (used by Ring JSON middleware)
cheshire/cheshire {:mvn/version "6.1.0"} ; fast JSON encoding (used by Ring JSON middleware)
clj-bom/clj-bom {:mvn/version "0.1.2"} ; handle BOMs in imported CSVs
clj-commons/clj-yaml {:mvn/version "1.0.29"} ; Clojure wrapper for YAML library SnakeYAML
clj-commons/iapetos {:mvn/version "0.1.14"} ; prometheus metrics
@@ -53,7 +53,7 @@
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"} ; Talk to JDBC DBs
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.8"} ; Telemetry library
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.5"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
com.google.guava/guava {:mvn/version "33.4.8-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
com.google.guava/guava {:mvn/version "33.5.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
{:mvn/version "2.1.214"} ; embedded SQL database
com.ibm.icu/icu4j {:mvn/version "78.1"} ; Used to transliterate Unicode characters into Latin characters
@@ -89,6 +89,7 @@
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/representations {:mvn/version "0.1.13"}
;; TODO: replace back to normal artifact
io.github.metabase/macaw {:mvn/version "0.2.29"} ; Parse native SQL queries
io.github.resilience4j/resilience4j-retry ^:antq/exclude ; 2.x do not support Java 8
@@ -120,7 +121,7 @@
junit/junit]}
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
org.apache.commons/commons-lang3 {:mvn/version "3.18.0"} ; helper methods for working with java.lang stuff
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.2"} ; apache logging framework
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.2"} ; add compatibility with log4j 1.2
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.2"} ; apache logging framework
@@ -160,7 +161,7 @@
org.clojure/java.jmx {:mvn/version "1.1.0"} ; JMX bean library, for exporting diagnostic info
org.clojure/math.combinatorics {:mvn/version "0.3.0"} ; combinatorics functions
org.clojure/math.numeric-tower {:mvn/version "0.1.0"} ; math functions like `ceil`
org.clojure/tools.cli {:mvn/version "1.1.230"} ; command-line argument parsing
org.clojure/tools.cli {:mvn/version "1.2.245"} ; command-line argument parsing
org.clojure/tools.logging {:mvn/version "1.3.0"} ; logging framework
org.clojure/tools.macro {:mvn/version "0.2.1"} ; local macros
org.clojure/tools.namespace {:mvn/version "1.5.0"}
@@ -174,7 +175,7 @@
org.flatland/ordered {:mvn/version "1.15.12"} ; ordered maps & sets
org.graalvm.polyglot/js-community {:mvn/version "25.0.1" :extension "pom"} ; JavaScript engine
org.graalvm.polyglot/polyglot {:mvn/version "25.0.1"} ; GraalVM platform, required by JS engine
org.jsoup/jsoup {:mvn/version "1.21.1"} ; required by hickory
org.jsoup/jsoup {:mvn/version "1.21.2"} ; required by hickory
org.liquibase/liquibase-core {:mvn/version "4.33.0" ; migration management (Java lib)
:exclusions [ch.qos.logback/logback-classic]}
org.mariadb.jdbc/mariadb-java-client ^:antq/exclude ; 3.x only support jdbc:mariadb, so using 2.x for now
@@ -860,6 +861,13 @@
{:exec-args {:only ["test/metabase/notification"
"test/metabase/channel"]}}
;; test the transforms feature
;; clj -X:dev:ee:ee-dev:test:test/transforms
:test/transforms
{:exec-args {:only ["enterprise/backend/test/metabase_enterprise/transforms"
"enterprise/backend/test/metabase_enterprise/transforms_python"]}}
;; test all MBQL related stuff: MLv2, the legacy shared `metabase.legacy-mbql` code, and the QP
;;
;; clj -X:dev:ee:ee-dev:test:test/mbql

View File

@@ -204,6 +204,21 @@
;; has 2 defenterprise-schemas, both to the same ns:
(find-defenterprise-schemas "src/metabase/sso/ldap/default_implementation.clj"))
(def ^:private ^String project-root (System/getProperty "user.dir"))
(defn- file->path-relative-to-project-root
"Get the path of a file relative to the project (repo) root directory e.g.
(file->path-relative-to-project-root \"/home/cam/metabase/deps.edn\")
;; =>
\"deps.edn\""
[file]
(let [file (io/file file)
path (.getAbsolutePath file)]
(if (str/starts-with? path project-root)
(subs path (inc (count project-root))) ; project root won't include trailing `/`
path)))
(def ^:private ignored-dependencies
"Technically `config` 'uses' `enterprise/core` and `test` since it tries to load them to see if they exist so we know
if EE/test code is available; however we can ignore them since they're not 'real' usages. So add them here so we
@@ -212,13 +227,16 @@
(mu/defn- file-dependencies :- [:map
[:namespace simple-symbol?]
[:filename string?] ; filename is relative to [[project-root]]
[:module symbol?]
[:deps [:sequential
[:map
[:namespace simple-symbol?]
[:module symbol?]
[:dynamic {:optional true} :keyword]]]]]
[file]
[file :- [:or
string?
[:fn {:error/message "Instance of a java.io.File"} #(instance? java.io.File %)]]]
(try
(let [decl (ns.file/read-file-ns-decl file)
ns-symb (ns.parse/name-from-ns-decl decl)
@@ -239,6 +257,7 @@
#_defenterprise-deps
#_defenterprise-schema-deps])]
{:namespace ns-symb
:filename (file->path-relative-to-project-root file)
:module (module ns-symb)
:deps (sort-by pr-str
(keep (fn [required-ns]
@@ -262,7 +281,10 @@
(file-dependencies "src/metabase/query_processor/middleware/permissions.clj"))
(defn dependencies []
(defn dependencies
"Calculate information about all the modules dependencies for all *SOURCE* files in the Metabase project by parsing
the files."
[]
(pmap file-dependencies (find-source-files)))
(defn external-usages
@@ -518,14 +540,15 @@
(defn leaf-modules
"Modules that are leaf nodes in the module dependency tree -- nothing else depends on them."
[]
(let [deps (dependencies)]
(into (sorted-set)
(comp (map :module)
(keep (fn [module]
(when (zero? (count (external-usages deps module)))
module))))
deps)))
([]
(leaf-modules (dependencies)))
([deps]
(into (sorted-set)
(comp (map :module)
(keep (fn [module]
(when (zero? (count (external-usages deps module)))
module))))
deps)))
(defn non-dependencies
"Modules that `module` does not depend on, either directly or indirectly -- changes to any of these modules should not
@@ -534,6 +557,7 @@
(let [deps (dependencies)
all-modules (into (sorted-set) (map :module) deps)
module-deps (set (keys (all-module-deps-paths deps module)))]
#_{:clj-kondo/ignore [:discouraged-var]}
(printf "Module %s depends on %d/%d (%.1f%%) other modules.\n"
module
(count module-deps)
@@ -575,3 +599,129 @@
new-deps (simulate-rename deps old-namespace->new-namespace)
new-module-deps (into (sorted-set) (keys (all-module-deps-paths new-deps module)))]
(set/difference old-module-deps new-module-deps)))
(mu/defn- module->source-files :- [:set :string]
"Return the set of all *source* filenames (relative to the [[project-root]] directory) for a `module`."
[deps module]
(into
(sorted-set)
(comp (filter #(= (:module %) module))
(map :filename))
deps))
(defn- file->namespace
"Infer the Clojure namespace from a core project filename.
(file->namespace \"/home/cam/metabase/enterprise/backend/src/metabase_enterprise/advanced_permissions/common.clj\")
;; =>
metabase-enterprise.advanced-permissions.common"
[file]
(-> file
file->path-relative-to-project-root
(str/replace #"^enterprise/backend/" "")
(str/replace #"^(?:(?:src)|(?:test))/" "")
(str/replace #"\.clj[cs]?$" "")
(str/replace #"/" ".")
(str/replace #"_" "-")
symbol))
(defn- module->all-deps [deps module]
(keys (all-module-deps-paths deps module)))
(defn test-filenames->relevant-source-filenames
"Given a collection of `test-filenames`, return the set of source filenames (relative to the project root directory)
that when changed should trigger these tests."
([test-filenames]
(test-filenames->relevant-source-filenames (dependencies) test-filenames))
([deps test-filenames]
(into
(sorted-set)
(comp (map file->namespace)
(map module)
(distinct)
(mapcat (fn [module]
(into #{module} (module->all-deps deps module))))
(distinct)
(mapcat #(module->source-files deps %)))
test-filenames)))
(comment
;; should include source files for `settings-rest` itself as well as all modules used either directly or indirectly
;; by `settings-rest` (currently this set is huge, 955 files as of 2025-11-26)
(test-filenames->relevant-source-filenames ["test/metabase/settings_rest/api_test.clj"]))
(defn- direct-dependents
"Set of modules that directly depend on `module`."
[deps module]
(into (sorted-set)
(keep (fn [ns-info]
(when (some (fn [ns-deps]
(= (:module ns-deps) module))
(:deps ns-info))
(:module ns-info))))
deps))
(comment
(direct-dependents (dependencies) 'driver)
(direct-dependents (dependencies) 'settings-rest))
(defn- indirect-dependents
"Set of modules that either directly or indirectly depend on `module`."
([deps module]
(indirect-dependents deps module (sorted-set)))
([deps module acc]
(let [module-deps (direct-dependents deps module)
new-deps (set/difference module-deps acc)
acc (into acc new-deps)]
(reduce
(fn [acc new-dep]
(indirect-dependents deps new-dep acc))
acc
new-deps))))
(comment
(indirect-dependents (dependencies) 'settings-rest))
(defn- module->dependents [deps module]
(into #{module}
(indirect-dependents deps module)))
(comment
(module->dependents (dependencies) 'settings-rest))
(defn- module->test-directory [module]
(let [parent-dir (case (namespace module)
nil "test/metabase/"
"enterprise" "enterprise/backend/test/metabase_enterprise/")
module-dir (str/replace (name module) #"-" "_")]
(str parent-dir module-dir)))
(mu/defn- module->test-files :- [:set :string]
"Return the set of test filenames associated with a `module`."
[module :- :symbol]
(let [test-dir (module->test-directory module)
test-filenames (ns.find/find-sources-in-dir (io/file test-dir))]
(into
(sorted-set)
(map file->path-relative-to-project-root)
test-filenames)))
(defn source-filenames->relevant-test-filenames
"Given a collection of `source-filenames`, return the set of test filenames (relative to the project root directory)
that we should re-run when any of `source-filenames` change."
([source-filenames]
(source-filenames->relevant-test-filenames (dependencies) source-filenames))
([deps source-filenames]
(into
(sorted-set)
(comp (map file->namespace)
(map module)
(distinct)
(mapcat #(module->dependents deps %))
(mapcat module->test-files))
source-filenames)))
(comment
;; should only include tests for `settings-rest`, `api-routes`, `core`, and the handful of random modules that use
;; `settings-rest` (21 files as of 2025-11-26)
(source-filenames->relevant-test-filenames ["src/metabase/settings_rest/api.clj"]))

View File

@@ -245,6 +245,18 @@
} ]
},
"mbql.clause.coalesce" : { },
"mbql.clause.collate" : {
"type" : "array",
"prefixItems" : [ {
"const" : "collate"
}, {
"$ref" : "#/components/schemas/metabase.lib.schema.common.options"
}, {
"$ref" : "#/components/schemas/metabase.lib.schema.expression.string"
}, {
"type" : "string"
} ]
},
"mbql.clause.concat" : { },
"mbql.clause.contains" : { },
"mbql.clause.convert-timezone" : { },
@@ -1125,6 +1137,45 @@
"$ref" : "#/components/schemas/metabase-enterprise.action-v2.api.api-action-expression"
} ]
},
"metabase-enterprise.data-studio.api.table.data-authorities" : {
"type" : "string",
"enum" : [ "computed", "authoritative", "unconfigured", "ingested" ]
},
"metabase-enterprise.data-studio.api.table.data-layers" : {
"type" : "string",
"enum" : [ "copper", "bronze", "gold", "silver" ]
},
"metabase-enterprise.data-studio.api.table.data-sources" : {
"type" : "string",
"enum" : [ "source-data", "transform", "unknown", "ingested", "metabase-transform", "upload" ]
},
"metabase-enterprise.data-studio.api.table.table-selectors" : {
"type" : "object",
"properties" : {
"database_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
},
"schema_ids" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"table_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
}
}
},
"metabase-enterprise.dependencies.api.card-body" : {
"description" : "Schema for an instance of a `:model/Card` (everything is optional to support updates).",
"type" : "object",
@@ -3094,6 +3145,9 @@
"metabase-enterprise.metabot-v3.tools.api.table-result" : {
"type" : "object",
"properties" : {
"database_engine" : {
"type" : "string"
},
"id" : {
"type" : "integer"
},
@@ -3132,7 +3186,7 @@
"type" : "string"
}
},
"required" : [ "id", "type", "name", "display_name", "database_id", "fields" ]
"required" : [ "id", "type", "name", "display_name", "database_id", "database_engine", "fields" ]
},
"metabase-enterprise.metabot-v3.tools.api.temporal-extraction-filter" : {
"allOf" : [ {
@@ -3259,6 +3313,114 @@
"description" : "value must be a valid attribute remappings map (attribute name -> remapped name)",
"optional" : true
},
"metabase-enterprise.support-access-grants.schema.create-grant-request" : {
"description" : "Schema for POST /api/ee/support-access-grants request body.",
"type" : "object",
"properties" : {
"grant_duration_minutes" : {
"type" : "integer",
"minimum" : 1,
"maximum" : 10080
},
"notes" : {
"type" : "string",
"minLength" : 1,
"maxLength" : 255
},
"ticket_number" : {
"type" : "string",
"minLength" : 1,
"maxLength" : 100
}
},
"required" : [ "grant_duration_minutes" ]
},
"metabase-enterprise.support-access-grants.schema.current-grant-response" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response",
"description" : "Schema for GET /api/ee/support-access-grants/current response.",
"optional" : true
},
"metabase-enterprise.support-access-grants.schema.grant-response" : {
"description" : "Schema for a support access grant object in API responses.",
"type" : "object",
"properties" : {
"user_id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"token" : {
"type" : "string"
},
"user_name" : {
"type" : "string"
},
"user_email" : {
"type" : "string"
},
"grant_end_timestamp" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
},
"revoked_at" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
},
"revoked_by_user_id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"ticket_number" : {
"type" : "string"
},
"updated_at" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
},
"created_at" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
},
"notes" : {
"type" : "string"
},
"grant_start_timestamp" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
}
},
"required" : [ "id", "user_id", "grant_start_timestamp", "grant_end_timestamp", "created_at", "updated_at" ]
},
"metabase-enterprise.support-access-grants.schema.list-grants-response" : {
"description" : "Schema for GET /api/ee/support-access-grants response (paginated list).",
"type" : "object",
"properties" : {
"data" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
}
},
"limit" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"offset" : {
"type" : "integer",
"minimum" : 0
},
"total" : {
"type" : "integer",
"minimum" : 0
}
},
"required" : [ "data", "total", "limit", "offset" ]
},
"metabase-enterprise.support-access-grants.schema.timestamp" : {
"anyOf" : [ { }, { } ]
},
"metabase-enterprise.transforms.api.run-trigger" : {
"type" : "string",
"enum" : [ "none", "global-schedule" ]
@@ -5183,6 +5345,8 @@
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.text"
}, {
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.split-part"
}, {
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.collate"
} ]
},
"metabase.legacy-mbql.schema.StringExpressionArg" : {
@@ -5397,6 +5561,10 @@
"description" : "schema for a valid MBQL 4 :coalesce clause",
"allOf" : [ ]
},
"metabase.legacy-mbql.schema.collate" : {
"description" : "schema for a valid MBQL 4 :collate clause",
"allOf" : [ ]
},
"metabase.legacy-mbql.schema.concat" : {
"description" : "schema for a valid MBQL 4 :concat clause",
"allOf" : [ ]
@@ -6830,6 +6998,8 @@
"$ref" : "#/components/schemas/mbql.clause.ceil"
}, {
"$ref" : "#/components/schemas/mbql.clause.coalesce"
}, {
"$ref" : "#/components/schemas/mbql.clause.collate"
}, {
"$ref" : "#/components/schemas/mbql.clause.concat"
}, {
@@ -9272,6 +9442,14 @@
"type" : "string",
"enum" : [ "computed", "authoritative", "unconfigured", "ingested" ]
},
"metabase.warehouse-schema-rest.api.table.data-layers" : {
"type" : "string",
"enum" : [ "copper", "bronze", "gold", "silver" ]
},
"metabase.warehouse-schema-rest.api.table.data-sources" : {
"type" : "string",
"enum" : [ "source-data", "transform", "unknown", "ingested", "metabase-transform", "upload" ]
},
"metabase.warehouses-rest.api.available-settings" : {
"type" : "object",
"additionalProperties" : {
@@ -9704,6 +9882,7 @@
"description" : "Server error response"
}
},
"deprecated" : true,
"tags" : [ "/api/activity" ]
}
},
@@ -12394,10 +12573,6 @@
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"type" : {
"type" : "string",
"enum" : [ "remote-synced" ]
}
},
"required" : [ "name" ]
@@ -12548,7 +12723,7 @@
"/api/collection/root/items" : {
"get" : {
"summary" : "GET /api/collection/root/items",
"description" : "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection\n doesn't actually exist as a row in the application DB: it's simply a virtual Collection where things with no\n `collection_id` exist. It does, however, have its own set of Permissions.\n\n This endpoint will actually show objects with no `collection_id` for Users that have Root Collection\n permissions, but for people without Root Collection perms, we'll just show the objects that have an effective\n location of `/`.\n\n This endpoint is intended to power a 'Root Folder View' for the Current User, so regardless you'll see all the\n top-level objects you're allowed to access.\n\n By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as\n `snippets`, you can pass the `?namespace=` parameter.\n\n Note that this endpoint should return results in a similar shape to `/api/dashboard/:id/items`, so if this is\n changed, that should too.",
"description" : "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection\n doesn't actually exist as a row in the application DB: it's simply a virtual Collection where things with no\n `collection_id` exist. It does, however, have its own set of Permissions.\n\n This endpoint will actually show objects with no `collection_id` for Users that have Root Collection\n permissions, but for people without Root Collection perms, we'll just show the objects that have an effective\n location of `/`.\n\n This endpoint is intended to power a 'Root Folder View' for the Current User, so regardless you'll see all the\n top-level objects you're allowed to access.\n\n By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as\n `snippets`, you can pass the `?namespace=` parameter.\n\n By default, library collections are excluded from the results; to include them, pass `?include_library=true`.\n\n Note that this endpoint should return results in a similar shape to `/api/dashboard/:id/items`, so if this is\n changed, that should too.",
"parameters" : [ {
"in" : "query",
"name" : "models",
@@ -12592,6 +12767,14 @@
"type" : "string",
"minLength" : 1
}
}, {
"in" : "query",
"name" : "include_library",
"required" : false,
"schema" : {
"type" : "boolean",
"default" : false
}
}, {
"in" : "query",
"name" : "pinned_state",
@@ -12731,6 +12914,14 @@
"type" : "boolean",
"default" : false
}
}, {
"in" : "query",
"name" : "include-library",
"required" : false,
"schema" : {
"type" : "boolean",
"default" : false
}
}, {
"in" : "query",
"name" : "namespace",
@@ -13109,6 +13300,7 @@
"description" : "Server error response"
}
},
"deprecated" : true,
"tags" : [ "/api/dashboard" ]
},
"post" : {
@@ -16598,6 +16790,222 @@
"tags" : [ "/api/ee/content-translation" ]
}
},
"/api/ee/data-studio/table/discard-values" : {
"post" : {
"summary" : "POST /api/ee/data-studio/table/discard-values",
"description" : "Batch version of /table/:id/discard_values. Takes an abstract table selection as /table/edit does.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
}
}
}
},
"tags" : [ "/api/ee/data-studio/table" ]
}
},
"/api/ee/data-studio/table/edit" : {
"post" : {
"summary" : "POST /api/ee/data-studio/table/edit",
"description" : "Bulk updating tables.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : {
"data_authority" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-authorities"
},
"data_layer" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-layers"
},
"table_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
},
"owner_email" : {
"type" : "string"
},
"owner_user_id" : {
"type" : "integer"
},
"data_source" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-sources"
},
"schema_ids" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"entity_type" : {
"type" : "string"
},
"database_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
}
},
"additionalProperties" : false
}
}
}
},
"tags" : [ "/api/ee/data-studio/table" ]
}
},
"/api/ee/data-studio/table/publish-model" : {
"post" : {
"summary" : "POST /api/ee/data-studio/table/publish-model",
"description" : "Create a model for each of selected tables",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"properties" : {
"database_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
},
"schema_ids" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"table_ids" : {
"type" : "array",
"items" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
},
"target_collection_id" : {
"anyOf" : [ {
"type" : "integer",
"minimum" : 1
}, {
"const" : "library"
} ]
}
},
"required" : [ ]
}
}
}
},
"tags" : [ "/api/ee/data-studio/table" ]
}
},
"/api/ee/data-studio/table/rescan-values" : {
"post" : {
"summary" : "POST /api/ee/data-studio/table/rescan-values",
"description" : "Batch version of /table/:id/rescan_values. Takes an abstract table selection as /table/edit does.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
}
}
}
},
"tags" : [ "/api/ee/data-studio/table" ]
}
},
"/api/ee/data-studio/table/sync-schema" : {
"post" : {
"summary" : "POST /api/ee/data-studio/table/sync-schema",
"description" : "Batch version of /table/:id/sync_schema. Takes an abstract table selection as /table/edit does.\n - Currently checks policy before returning (so you might receive a 4xx on e.g. AuthZ policy failure)\n - The sync itself is however, asyncronous. This call may return before all tables synced.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
}
}
}
},
"tags" : [ "/api/ee/data-studio/table" ]
}
},
"/api/ee/database-replication/connection/{database-id}" : {
"post" : {
"summary" : "POST /api/ee/database-replication/connection/{database-id}",
@@ -17760,6 +18168,61 @@
"tags" : [ "/api/ee/gsheets" ]
}
},
"/api/ee/library/" : {
"post" : {
"summary" : "POST /api/ee/library/",
"description" : "Creates the Library if it doesn't exist. Returns the created collection.\n\n Requires superuser permissions.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/library" ]
},
"get" : {
"summary" : "GET /api/ee/library/",
"description" : "Get the Library. If no library exists, it doesn't fail but returns an empty response.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/library" ]
}
},
"/api/ee/library/tree" : {
"get" : {
"summary" : "GET /api/ee/library/tree",
"description" : "This matches /api/collection/tree but only returns the library collection.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/library" ]
}
},
"/api/ee/logs/query_execution/{yyyy-mm}" : {
"get" : {
"summary" : "GET /api/ee/logs/query_execution/{yyyy-mm}",
@@ -21087,6 +21550,149 @@
"tags" : [ "/api/ee/stale" ]
}
},
"/api/ee/support-access-grant/" : {
"post" : {
"summary" : "POST /api/ee/support-access-grant/",
"description" : "Create a new support access grant.\n\n Requires superuser permissions. Only one active grant can exist at a time.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Schema for a support access grant object in API responses.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
}
}
}
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.create-grant-request"
}
}
}
},
"tags" : [ "/api/ee/support-access-grant" ]
},
"get" : {
"summary" : "GET /api/ee/support-access-grant/",
"description" : "List support access grants with optional filtering and pagination.\n\n Requires superuser permissions.\n\n Query parameters:\n - ticket-number: Filter by ticket number\n - user-id: Filter by user who created the grant\n - include-revoked: Include revoked grants (default false)",
"parameters" : [ {
"in" : "query",
"name" : "ticket-number",
"required" : false,
"schema" : {
"type" : "string"
}
}, {
"in" : "query",
"name" : "user-id",
"required" : false,
"schema" : {
"type" : "integer",
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
}, {
"in" : "query",
"name" : "include-revoked",
"required" : false,
"schema" : {
"type" : "boolean"
}
} ],
"responses" : {
"2XX" : {
"description" : "Schema for GET /api/ee/support-access-grants response (paginated list).",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.list-grants-response"
}
}
}
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/support-access-grant" ]
}
},
"/api/ee/support-access-grant/current" : {
"get" : {
"summary" : "GET /api/ee/support-access-grant/current",
"description" : "Get the currently active support access grant, if one exists.\n\n Requires superuser permissions.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Schema for GET /api/ee/support-access-grants/current response.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.current-grant-response"
}
}
}
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/support-access-grant" ]
}
},
"/api/ee/support-access-grant/{id}/revoke" : {
"put" : {
"summary" : "PUT /api/ee/support-access-grant/{id}/revoke",
"description" : "Revoke an existing support access grant.\n\n Requires superuser permissions. Any admin can revoke any grant.",
"parameters" : [ {
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "integer",
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
} ],
"responses" : {
"2XX" : {
"description" : "Schema for a support access grant object in API responses.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
}
}
}
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/ee/support-access-grant" ]
}
},
"/api/ee/transform-job/" : {
"post" : {
"summary" : "POST /api/ee/transform-job/",
@@ -25813,6 +26419,25 @@
"tags" : [ "/api/preview_embed" ]
}
},
"/api/preview_embed/dashboard/{token}/params/{param-key}/search/{prefix}" : {
"get" : {
"summary" : "GET /api/preview_embed/dashboard/{token}/params/{param-key}/search/{prefix}",
"description" : "Embedded version of chain filter search endpoint.",
"parameters" : [ ],
"responses" : {
"2XX" : {
"description" : "Successful response"
},
"4XX" : {
"description" : "Client error response"
},
"5XX" : {
"description" : "Server error response"
}
},
"tags" : [ "/api/preview_embed" ]
}
},
"/api/preview_embed/dashboard/{token}/params/{param-key}/values" : {
"get" : {
"summary" : "GET /api/preview_embed/dashboard/{token}/params/{param-key}/values",
@@ -27761,6 +28386,15 @@
"type" : "boolean",
"default" : false
}
}, {
"in" : "query",
"name" : "collection",
"required" : false,
"schema" : {
"type" : "integer",
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
}, {
"in" : "query",
"name" : "table_db_id",
@@ -28771,7 +29405,63 @@
"get" : {
"summary" : "GET /api/table/",
"description" : "Get all `Tables`.",
"parameters" : [ ],
"parameters" : [ {
"in" : "query",
"name" : "term",
"required" : false,
"schema" : {
"type" : "string"
}
}, {
"in" : "query",
"name" : "visibility-type",
"required" : false,
"schema" : {
"type" : "string"
}
}, {
"in" : "query",
"name" : "data-layer",
"required" : false,
"schema" : {
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-layers"
}
}, {
"in" : "query",
"name" : "data-source",
"required" : false,
"schema" : {
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-sources"
}
}, {
"in" : "query",
"name" : "owner-user-id",
"required" : false,
"schema" : {
"type" : "integer"
}
}, {
"in" : "query",
"name" : "owner-email",
"required" : false,
"schema" : {
"type" : "string"
}
}, {
"in" : "query",
"name" : "orphan-only",
"required" : false,
"schema" : {
"type" : "boolean"
}
}, {
"in" : "query",
"name" : "unused-only",
"required" : false,
"schema" : {
"type" : "boolean"
}
} ],
"responses" : {
"2XX" : {
"description" : "Successful response"
@@ -28787,7 +29477,7 @@
},
"put" : {
"summary" : "PUT /api/table/",
"description" : "Update all `Table` in `ids`.",
"description" : "Update all `Table` in `ids`.\n\n Deprecated, should use PUT /table/edit from now on.",
"parameters" : [ ],
"responses" : {
"2XX" : {
@@ -28816,9 +29506,18 @@
"data_authority" : {
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
},
"data_layer" : {
"type" : "string"
},
"show_in_getting_started" : {
"type" : "boolean"
},
"owner_email" : {
"type" : "string"
},
"owner_user_id" : {
"type" : "integer"
},
"ids" : {
"type" : "array",
"items" : {
@@ -28831,6 +29530,9 @@
"type" : "string",
"minLength" : 1
},
"data_source" : {
"type" : "string"
},
"caveats" : {
"type" : "string"
},
@@ -28979,13 +29681,25 @@
"data_authority" : {
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
},
"data_layer" : {
"type" : "string"
},
"show_in_getting_started" : {
"type" : "boolean"
},
"owner_email" : {
"type" : "string"
},
"owner_user_id" : {
"type" : "integer"
},
"display_name" : {
"type" : "string",
"minLength" : 1
},
"data_source" : {
"type" : "string"
},
"caveats" : {
"type" : "string"
},

View File

@@ -11,9 +11,16 @@ Admins can enable editable table data in Metabase.
![Edit table](./images/edit-table.png)
## Databases that support editable tables
Currently, editable tables are available for the following databases:
- PostgreSQL
- MySQL
- Metabase Sample Database
## Setting up editable tables
_Currently, editable table data is only available for PostgreSQL and MySQL._
To set up table editing, admins can:

View File

@@ -13,6 +13,12 @@ Both components are built and assembled together into a single JAR file. In the
## Quick start
First, make sure your local dev tooling matches the Metabase requirements. Run:
```
./bin/dev-requirements
```
To spin up a development environment, run:
```
@@ -152,10 +158,12 @@ use Metabase, don't forget to start the frontend as well (e.g. with `yarn build-
By default Rspack runs the development server on port `8088`. You can run multiple instances of Metabase on the same machine by specifying a different port for each instance.
Frontend:
- If you are running the frontend with `yarn build-hot`, set the `MB_FRONTEND_DEV_PORT` environment variable: `MB_FRONTEND_DEV_PORT=8089 MB_EDITION=ee yarn build-hot`
- If you are building the frontend statically with `yarn build`, there is nothing different to do
Backend:
- Set the `MB_JETTY_PORT` environment variable and `MB_FRONTEND_DEV_PORT` to the same one as for the frontend.
### The application database

View File

@@ -4,6 +4,12 @@ title: Driver interface changelog
# Driver Interface Changelog
## Metabase 0.58.0
- Added a `:collate` feature for drivers that support collation settings on text fields
- Added `metabase.driver/compile-insert` to implement incremental transforms.
## Metabase 0.57.0
- `driver/field-reference-mlv2` is now deprecated, and is no longer used. Please remove your implementations.

View File

@@ -122,28 +122,27 @@ One great feature of Cypress is that you can use the Chrome inspector after each
`yarn build` and `yarn build-hot` each overwrite an HTML template to reference the correct JavaScript files. If you run `yarn build` before building an Uberjar for Cypress tests, you won’t see changes to your JavaScript reflected even if you then start `yarn build-hot`.
### Running Cypress on M1 machines
### Running Cypress on Apple Silicon
You might run into problems when running Cypress on Apple Silicon processors.
You might run into problems when running Cypress on M1 machine.
This is caused by the `@bahmutov/cypress-esbuild-preprocessor` that is using `esbuild` as a dependency. The error might look [like this](https://github.com/evanw/esbuild/issues/1819#issuecomment-1018771557). [The solution](https://github.com/evanw/esbuild/issues/1819#issuecomment-1080720203) is to install NodeJS using one of the Node version managers like [nvm](https://github.com/nvm-sh/nvm) or [n](https://github.com/tj/n).
Another issue you will almost surely face is the inability to connect to our Mongo QA Database. You can solve it by providing the following env:
Another issue you will almost surely face is the inability to connect to our Mongo QA Database. The supported Docker image is incompatible (AMD64). You can solve it by providing the following env:
```shell
export EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
```
Please note that some users experienced Mongo connection timeouts even with this env var set. If that happens, try using OrbStack instead of Docker Desktop.
### Running tests that depend on Docker images
A subset 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. The default cypress command will spin up all necessary docker containers for these tests to function properly, but you can toggle them off if you want
```sh
START_CONTAINERS=false yarn test-cypress
```
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
Tests that depend on Snowplow expect a running server. This is enabled by default. You can manually enable them as well by spinning up the snowplow micro docker container and setting the appropriate environment variables:
Tests that depend on Snowplow expect a running server. This is enabled by default. You can manually enable them as well by spinning up the Snowplow micro Docker container and setting the appropriate environment variables:
```
docker-compose -f ./snowplow/docker-compose.yml up -d

View File

@@ -18,7 +18,7 @@ Metabase will print out the help text for available commands.
## `api-documentation`
Generate a markdown file containing documentation for all API endpoints. This is written to a file called `docs/api-documentation.md`.
Generate an HTML file and a JSON file for Scalar docs for the Metabase API.
## `command-documentation`

View File

@@ -1,40 +1,42 @@
const cypress = require("cypress");
const { FAILURE_EXIT_CODE } = require("./constants/exit-code");
/**
* This simply runs Cypress through the Cypress Module API rather than the CLI.
* Runs Cypress through the Module API and throws on any failure.
* See: https://docs.cypress.io/app/references/module-api
*
* @param {Partial<CypressCommandLine.CypressRunOptions> | Partial<CypressCommandLine.CypressOpenOptions>} options - Cypress configuration options for running or opening tests
* @param {function(number): Promise<void>} exitFunction - Async function to call on exit with the exit code
* @returns {Promise<CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult | void>}
* @throws {Error} When Cypress fails to run tests
* Errors are propagated to the parent process rather than handled here.
* The caller is responsible for catching errors and performing cleanup.
*
* This wrapper exists because cypress.run() resolves successfully even when
* tests fail - it only throws when Cypress itself fails to start (e.g., invalid
* config). We convert test failures into thrown errors for consistent handling.
*
* @param {Partial<CypressCommandLine.CypressRunOptions> | Partial<CypressCommandLine.CypressOpenOptions>} options - Cypress configuration options
* @returns {Promise<CypressCommandLine.CypressRunResult>} - Resolves with results if all tests pass
* @throws {Error} When Cypress fails to run (e.g., config error) or any tests fail
*/
const runCypress = async (options, exitFunction) => {
const runCypress = async (options) => {
const openMode = process.env.OPEN_UI === "true";
try {
const { status, message, totalFailed, failures } = openMode
? await cypress.open(options)
: await cypress.run(options);
// At least one test failed
if (totalFailed > 0) {
await exitFunction(FAILURE_EXIT_CODE);
}
// Something went wrong and Cypress failed to even run tests
if (status === "failed" && failures) {
console.error(message);
await exitFunction(failures);
}
} catch (e) {
console.error("Failed to run Cypress!\n", e);
await exitFunction(FAILURE_EXIT_CODE);
// Open mode is interactive - no results to validate
if (openMode) {
await cypress.open(options);
return;
}
const result = await cypress.run(options);
// Cypress failed to even run tests (e.g., config error, browser crash)
if (result.status === "failed") {
throw new Error(`Cypress failed to run: ${result.message}`);
}
// Tests ran but at least one failed
if (result.totalFailed > 0) {
throw new Error(`${result.totalFailed} test(s) failed`);
}
return result;
};
module.exports = runCypress;

View File

@@ -22,7 +22,7 @@ const CypressBackend = {
this.server = server;
},
async start() {
async start(jarPath = "target/uberjar/metabase.jar") {
if (!this.server) {
this.createServer();
}
@@ -33,6 +33,11 @@ const CypressBackend = {
"-Djava.awt.headless=true", // when running on macOS prevent little Java icon from popping up in Dock
"-Dmail.smtps.ssl.trust=*", // trust self signed certs for testing
"-Duser.timezone=US/Pacific",
// FIXME @nemanjaglumac 2025-11-28
// This is a bit convoluted because this particular log4j config outputs all logs to a file (logs/test.log)
// See: https://github.com/metabase/metabase/pull/21360
// If we want them in the console, we'd have to unset the log4j config, to let stdio inherit the logs.
// Override in your .env if you need to see the logs in the console locally.
process.env.SHOW_BACKEND_LOGS === "true"
? null
: `-Dlog4j.configurationFile=file:${__dirname}/../../frontend/test/__runner__/log4j2.xml`,
@@ -50,24 +55,22 @@ const CypressBackend = {
MB_CONFIG_FILE_PATH: "__cypress__", // ignore config.yml
MB_HTTP_CHANNEL_HOST_STRATEGY: "allow-all", // we use a local webhook service for testing
MB_IS_CYPRESS: "true", // custom flag so we can detect we're running in Cypress, used in tests.
MB_SNOWPLOW_AVAILABLE: true,
MB_SNOWPLOW_URL: "http://localhost:9090",
};
this.server.process = spawn(
"java",
[...javaFlags, "-jar", "target/uberjar/metabase.jar"],
{
env: {
...process.env,
...metabaseConfig,
},
stdio:
process.env["DISABLE_LOGGING"] ||
process.env["DISABLE_LOGGING_BACKEND"]
? "ignore"
: "inherit",
detached: true,
this.server.process = spawn("java", [...javaFlags, "-jar", jarPath], {
env: {
...process.env,
...metabaseConfig,
},
);
stdio:
process.env["DISABLE_LOGGING"] ||
process.env["DISABLE_LOGGING_BACKEND"]
? "ignore"
: "inherit",
detached: true,
});
}
if (!(await isReady(this.server.host))) {

View File

@@ -19,7 +19,7 @@ const userOptions = {
...process.env,
} as const;
printBold(`Running Cypress Sample App Tests with options:
printBold(`Setting up the Sample App environment with options:
- EMBEDDING_SDK_VERSION : ${userOptions.EMBEDDING_SDK_VERSION}
- SAMPLE_APP_BRANCH_NAME : ${userOptions.SAMPLE_APP_BRANCH_NAME}
`);

View File

@@ -37,7 +37,7 @@ const startServer = async () => {
const runTests = async (config, cliArguments = []) => {
const userOverrides = await parseArguments(cliArguments);
await runCypress({ ...config, ...userOverrides }, process.exit);
await runCypress({ ...config, ...userOverrides });
};
// Custom "modes"
@@ -46,7 +46,10 @@ if (command === "start") {
}
if (command === "snapshot") {
runTests({ configFile: "e2e/support/cypress-snapshots.config.js" });
runTests(
{ configFile: "e2e/support/cypress-snapshots.config.js" },
cliArguments,
);
}
// Metabase component or e2e tests

View File

@@ -0,0 +1,46 @@
import { FAILURE_EXIT_CODE, SUCCESS_EXIT_CODE } from "./constants/exit-code";
import runCypress from "./cypress-node-js-runner";
import { booleanify, printBold } from "./cypress-runner-utils";
import { startHostAppContainers } from "./embedding-sdk/host-apps/start-host-app-containers";
import { startSampleAppContainers } from "./embedding-sdk/sample-apps/start-sample-app-containers";
import { resolveSdkE2EConfig } from "./resolve-sdk-e2e-config";
// if you want to change these, set them as environment variables in your shell
const options = {
SDK_TEST_SUITE: "vite-6-host-app-e2e", // one of the many sample-app, or host-app Embedding SDK suites
OPEN_UI: false,
...booleanify(process.env),
};
printBold(`Running Cypress with options:
- SDK_TEST_SUITE : ${options.SDK_TEST_SUITE}
- OPEN_UI : ${options.OPEN_UI}
`);
const init = async () => {
switch (options.SDK_TEST_SUITE) {
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
case "metabase-nextjs-sdk-embedding-sample-e2e":
case "shoppy-e2e":
await startSampleAppContainers(options.SDK_TEST_SUITE);
break;
case "vite-6-host-app-e2e":
case "next-15-app-router-host-app-e2e":
case "next-15-pages-router-host-app-e2e":
case "angular-20-host-app-e2e":
await startHostAppContainers(options.SDK_TEST_SUITE);
break;
}
printBold("⏳ Starting Sample/Host App Cypress Tests");
const config = resolveSdkE2EConfig(options.SDK_TEST_SUITE);
await runCypress(config);
};
init()
.then(() => process.exit(SUCCESS_EXIT_CODE))
.catch((e) => {
console.error(e);
process.exit(FAILURE_EXIT_CODE);
});

View File

@@ -1,5 +1,3 @@
import fs from "fs";
import os from "os";
import path from "path";
import { BACKEND_PORT } from "./constants/backend-port";
@@ -13,109 +11,73 @@ import {
shell,
unBooleanify,
} from "./cypress-runner-utils";
import { startHostAppContainers } from "./embedding-sdk/host-apps/start-host-app-containers";
import { startSampleAppContainers } from "./embedding-sdk/sample-apps/start-sample-app-containers";
import { resolveSdkE2EConfig } from "./resolve-sdk-e2e-config";
let tempSampleDBDir: string | null = null;
// if you want to change these, set them as environment variables in your shell
const userOptions = {
const options = {
CYPRESS_TESTING_TYPE: "e2e", // e2e | component
SDK_TEST_SUITE: undefined, // one of the many sample-app, or host-app Embedding SDK suites
MB_EDITION: "ee", // ee | oss
START_CONTAINERS: true,
STOP_CONTAINERS: false,
BUILD_BACKEND: true,
BACKEND_PORT: BACKEND_PORT, // override with MB_JETTY_PORT in your env
OPEN_UI: true,
SHOW_BACKEND_LOGS: false,
GENERATE_SNAPSHOTS: true,
QUIET: false,
TZ: "UTC",
...booleanify(process.env),
};
const derivedOptions = {
QA_DB_ENABLED: userOptions.START_CONTAINERS,
BUILD_JAR: userOptions.BACKEND_PORT === 4000,
START_BACKEND: userOptions.BACKEND_PORT === 4000,
MB_SNOWPLOW_AVAILABLE: true,
MB_SNOWPLOW_URL: "http://localhost:9090",
};
const options = {
...derivedOptions,
...userOptions,
};
process.env = unBooleanify(options);
const missingTokens = [
"MB_ALL_FEATURES_TOKEN",
"MB_STARTER_CLOUD_TOKEN",
"MB_PRO_CLOUD_TOKEN",
"MB_PRO_SELF_HOSTED_TOKEN",
"CYPRESS_MB_ALL_FEATURES_TOKEN",
"CYPRESS_MB_STARTER_CLOUD_TOKEN",
"CYPRESS_MB_PRO_CLOUD_TOKEN",
"CYPRESS_MB_PRO_SELF_HOSTED_TOKEN",
].filter((token) => !process.env[token]);
if (options.MB_EDITION === "ee" && missingTokens.length > 0) {
printBold(
`⚠️ Missing tokens: ${missingTokens.join(", ")}. Either set them or run with MB_EDITION=oss`,
);
process.exit(FAILURE_EXIT_CODE);
}
printBold(`Running Cypress with options:
- CYPRESS_TESTING_TYPE : ${options.CYPRESS_TESTING_TYPE}
- SDK_TEST_SUITE : ${options.SDK_TEST_SUITE}
- MB_EDITION : ${options.MB_EDITION}
- START_CONTAINERS : ${options.START_CONTAINERS}
- STOP_CONTAINERS : ${options.STOP_CONTAINERS}
- BUILD_JAR : ${options.BUILD_JAR}
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
- BACKEND_PORT : ${options.BACKEND_PORT}
- START_BACKEND : ${options.START_BACKEND}
- BUILD_BACKEND : ${options.BUILD_BACKEND}
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
- OPEN_UI : ${options.OPEN_UI}
- SHOW_BACKEND_LOGS : ${options.SHOW_BACKEND_LOGS}
- TZ : ${options.TZ}
`);
const init = async () => {
const cliArguments = process.argv.slice(2);
const userOverrides = await parseArguments(cliArguments);
if (options.START_CONTAINERS) {
printBold("⏳ Starting containers");
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml up -d");
}
printBold("⏳ Starting containers");
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml up -d");
if (options.BUILD_JAR) {
if (options.BUILD_BACKEND) {
printBold("⏳ Building backend");
shell("./bin/build-for-test");
shell("./bin/build-backend-for-test");
if (options.START_BACKEND) {
const isBackendRunning = shell(
`lsof -ti:${options.BACKEND_PORT} || echo ""`,
{ quiet: true },
const isBackendRunning = shell(
`lsof -ti:${options.BACKEND_PORT} || echo ""`,
{ quiet: true },
);
if (isBackendRunning) {
printBold(
"⚠️ Your backend is already running, you may want to kill pid " +
isBackendRunning,
);
if (isBackendRunning) {
printBold(
"⚠️ Your backend is already running, you may want to kill pid " +
isBackendRunning,
);
process.exit(FAILURE_EXIT_CODE);
}
// Use a temporary copy of the sample db so it won't use and lock the db used for local development
tempSampleDBDir = path.join(
os.tmpdir(),
`metabase-sample-db-e2e-${process.pid}`,
);
fs.mkdirSync(tempSampleDBDir, { recursive: true });
process.env.MB_INTERNAL_DO_NOT_USE_SAMPLE_DB_DIR = tempSampleDBDir;
printBold("⏳ Starting backend");
await CypressBackend.start();
process.exit(FAILURE_EXIT_CODE);
}
// Use a temporary copy of the sample db so it won't use and lock the db used for local development
process.env.MB_INTERNAL_DO_NOT_USE_SAMPLE_DB_DIR = path.resolve(
__dirname,
"../../e2e/tmp", // already exists and is .gitignored
);
printBold("⏳ Starting backend");
await CypressBackend.start("target/uberjar/metabase-backend.jar");
} else {
printBold(
`Not building a jar, expecting metabase to be running on port ${options.BACKEND_PORT}. Make sure your metabase instance is running with an h2 app db and the following environment variables:
@@ -131,10 +93,12 @@ const init = async () => {
printBold("⏳ Generating app db snapshots");
process.env.OPEN_UI = "false";
await runCypress(
{ configFile: "e2e/support/cypress-snapshots.config.js" },
cleanup,
);
await runCypress({
configFile: "e2e/support/cypress-snapshots.config.js",
...(options.CYPRESS_TESTING_TYPE === "component" && {
env: { grepTags: "-@external" }, // component tests do not need QA DB snapshots for now
}),
});
process.env.OPEN_UI = `${options.OPEN_UI}`;
} else {
printBold("Skipping snapshot generation, beware of stale snapshot caches");
@@ -148,71 +112,31 @@ const init = async () => {
);
}
if (options.SDK_TEST_SUITE) {
switch (options.SDK_TEST_SUITE) {
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
case "metabase-nextjs-sdk-embedding-sample-e2e":
case "shoppy-e2e":
await startSampleAppContainers(options.SDK_TEST_SUITE);
break;
case "vite-6-host-app-e2e":
case "next-15-app-router-host-app-e2e":
case "next-15-pages-router-host-app-e2e":
case "angular-20-host-app-e2e":
await startHostAppContainers(options.SDK_TEST_SUITE);
break;
}
printBold("⏳ Starting Sample/Host App Cypress Tests");
const config = resolveSdkE2EConfig(options.SDK_TEST_SUITE);
await runCypress(config, cleanup);
}
if (options.CYPRESS_TESTING_TYPE === "component") {
printBold("⏳ Starting Cypress SDK component tests");
await runCypress(
{
configFile:
"e2e/support/cypress-embedding-sdk-component-test.config.js",
testingType: "component",
},
cleanup,
);
await runCypress({
configFile: "e2e/support/cypress-embedding-sdk-component-test.config.js",
testingType: "component",
});
}
if (options.CYPRESS_TESTING_TYPE === "e2e") {
const config = { configFile: "e2e/support/cypress.config.js" };
printBold("⏳ Starting Cypress");
await runCypress({ ...config, ...userOverrides }, cleanup);
await runCypress({ ...config, ...userOverrides });
}
};
const cleanup = async (exitCode: string | number = SUCCESS_EXIT_CODE) => {
if (options.BUILD_JAR) {
if (options.BUILD_BACKEND) {
printBold("⏳ Cleaning up...");
await CypressBackend.stop();
}
// Add cleanup for the temporary sample database directory
if (tempSampleDBDir) {
try {
fs.rmSync(tempSampleDBDir, { recursive: true, force: true });
printBold(
`🗑️ Cleaned up temporary sample database directory: ${tempSampleDBDir}`,
);
} catch (e) {
console.error(
`Error cleaning up temporary sample database directory: ${e}`,
);
}
}
if (options.STOP_CONTAINERS) {
printBold("⏳ Stopping containers");
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml down");
}
printBold(
"🧹 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)

View File

@@ -9,29 +9,29 @@ import {
describe("qa databases snapshots", { tags: "@external" }, () => {
it("creates snapshots for supported qa databases", () => {
if (Cypress.env("QA_DB_MONGO") === true) {
restoreAndAuthenticate();
addMongoDatabase();
snapshot("mongo-5");
} else {
// Postgres
restoreAndAuthenticate();
// Mongo
restoreAndAuthenticate();
addPostgresDatabase();
snapshot("postgres-12");
addMongoDatabase();
snapshot("mongo-5");
convertToWritable("postgres");
snapshot("postgres-writable");
// Postgres
restoreAndAuthenticate();
// MySQL
restoreAndAuthenticate();
addPostgresDatabase();
snapshot("postgres-12");
addMySQLDatabase({});
snapshot("mysql-8");
convertToWritable("postgres");
snapshot("postgres-writable");
convertToWritable("mysql");
snapshot("mysql-writable");
}
// MySQL
restoreAndAuthenticate();
addMySQLDatabase({});
snapshot("mysql-8");
convertToWritable("mysql");
snapshot("mysql-writable");
restore("blank");
});

View File

@@ -5,7 +5,7 @@ slug: synced_collection
archive_operation_id: null
name: Synced Collection
personal_owner_id: null
type: remote-synced
type: null
is_sample: false
parent_id: null
serdes/meta:
@@ -16,3 +16,4 @@ archived_directly: null
entity_id: AF_m-cEw4pFjXYd90_tnj
namespace: null
created_at: '2025-10-23T09:38:19.057632-07:00'
is_remote_synced: true

View File

@@ -2,15 +2,10 @@ const { defineConfig } = require("cypress");
const { defaultConfig } = require("./config");
const isQaDatabase = process.env["QA_DB_ENABLED"] === "true";
module.exports = defineConfig({
e2e: {
...defaultConfig,
specPattern: "e2e/snapshot-creators/**/*.cy.snap.js",
excludeSpecPattern: !isQaDatabase
? "e2e/snapshot-creators/qa-db.cy.snap.js"
: undefined,
video: false,
},
});

View File

@@ -0,0 +1,15 @@
import type { Collection } from "metabase-types/api";
type LibraryResponse = Collection & {
effective_children?: Collection[];
};
export const createLibrary = (): Cypress.Chainable<
Cypress.Response<LibraryResponse>
> => {
cy.log("Initialize library");
return cy
.request("POST", "/api/ee/library")
.then(() => cy.request<LibraryResponse>("GET", "/api/ee/library"));
};

View File

@@ -0,0 +1,20 @@
import type { Collection } from "metabase-types/api";
export type SnippetFolderDetails = {
name: string;
description?: string | null;
parent_id?: number | null;
};
export function createSnippetFolder({
name,
description = null,
parent_id = null,
}: SnippetFolderDetails): Cypress.Chainable<Cypress.Response<Collection>> {
return cy.request("POST", "/api/collection", {
name,
description,
parent_id,
namespace: "snippets",
});
}

View File

@@ -41,7 +41,7 @@ export function createTransform(
cy.wrap(body.id).as(idAlias);
}
if (visitTransform) {
cy.visit(`/admin/transforms/${body.id}`);
cy.visit(`/data-studio/transforms/${body.id}`);
}
});
}

View File

@@ -40,7 +40,7 @@ export function createTransformJob(
cy.wrap(body.id).as(idAlias);
}
if (visitTransformJob) {
cy.visit(`/admin/transforms/jobs/${body.id}`);
cy.visit(`/data-studio/transforms/jobs/${body.id}`);
}
});
}

View File

@@ -7,6 +7,7 @@ export { createApiKey } from "./createApiKey";
export { createCollection } from "./createCollection";
export { createComment } from "./createComment";
export { createDashboard } from "./createDashboard";
export { createLibrary } from "./createLibrary";
export type { DashboardDetails } from "./createDashboard";
export { createDashboardWithQuestions } from "./createDashboardWithQuestions";
export { createDashboardWithTabs } from "./createDashboardWithTabs";
@@ -26,6 +27,7 @@ export { createQuestionAndAddToDashboard } from "./createQuestionAndAddToDashboa
export { createQuestionAndDashboard } from "./createQuestionAndDashboard";
export { createReaction } from "./createReaction";
export { createSnippet } from "./createSnippet";
export { createSnippetFolder } from "./createSnippetFolder";
export { createTimeline } from "./createTimeline";
export { createTimelineEvent } from "./createTimelineEvent";
export { createTimelineWithEvents } from "./createTimelineWithEvents";

View File

@@ -0,0 +1,110 @@
import { codeMirrorHelpers } from "./e2e-codemirror-helpers";
const modelingSidebar = () => cy.findByTestId("modeling-sidebar");
const collectionsSection = () => cy.findByTestId("collections-section");
const snippetsSection = () => cy.findByTestId("snippets-section");
const glossarySection = () => cy.findByTestId("glossary-section");
const newSnippetPage = () => cy.findByTestId("new-snippet-page");
const editSnippetPage = () => cy.findByTestId("edit-snippet-page");
const metricOverviewPage = () => cy.findByTestId("metric-overview-page");
const metricQueryEditor = () => cy.findByTestId("metric-query-editor");
const modelOverviewPage = () => cy.findByTestId("model-overview-page");
const modelQueryEditor = () => cy.findByTestId("model-query-editor");
const modelFieldsPage = () => cy.findByTestId("model-fields-page");
const collectionPage = () => cy.findByTestId("collection-page");
const modelingEmptyPage = () =>
cy.findByText("Pick a collection or create a new model or metric");
export const DataStudio = {
header: () => cy.findByTestId("data-studio-header"),
transformsButton: () => DataStudio.header().findByText("Transforms"),
jobsButton: () => DataStudio.header().findByText("Jobs"),
runsButton: () => DataStudio.header().findByText("Runs"),
exitButton: () => DataStudio.header().findByText("Exit data studio"),
Transforms: {
header: () => cy.findByTestId("transforms-header"),
content: () => cy.findByTestId("transforms-content"),
sidebar: () => cy.findByTestId("transforms-sidebar"),
saveChangesButton: () => DataStudio.Transforms.queryEditor().button("Save"),
queryEditor: () => cy.findByTestId("transform-query-editor"),
runTab: () => DataStudio.Transforms.header().findByText("Run"),
targetTab: () => DataStudio.Transforms.header().findByText("Target"),
dependenciesTab: () =>
DataStudio.Transforms.header().findByText("Dependencies"),
},
Jobs: {
header: () => cy.findByTestId("jobs-header"),
content: () => cy.findByTestId("transforms-content"),
sidebar: () => cy.findByTestId("jobs-sidebar"),
},
PythonLibrary: {
header: () => cy.findByTestId("python-library-header"),
},
ModelingSidebar: {
root: modelingSidebar,
collectionsSection,
collectionsTree: () => collectionsSection().findByRole("tree"),
snippetsSection,
snippetsTree: () => snippetsSection().findByRole("tree"),
snippetsTreeItem: (name: string) =>
snippetsSection()
.findByRole("tree")
.findByText(name)
.closest("[role='menuitem']"),
glossarySection,
glossaryLink: () => glossarySection().findByText("Glossary"),
createCardMenuButton: () =>
collectionsSection().findByLabelText("Create model or metric"),
createSnippetButton: () =>
snippetsSection().findByLabelText("Create snippet"),
snippetCollectionOptionsButton: () =>
snippetsSection().findByLabelText("Snippet collection options"),
},
Snippets: {
newPage: newSnippetPage,
editPage: editSnippetPage,
nameInput: () => newSnippetPage().findByPlaceholderText("New SQL snippet"),
descriptionInput: () => cy.findByPlaceholderText("No description"),
saveButton: () => cy.findByRole("button", { name: "Save" }),
cancelButton: () => cy.findByRole("button", { name: "Cancel" }),
editor: codeMirrorHelpers("snippet-editor", {}),
},
Metrics: {
overviewPage: metricOverviewPage,
queryEditor: metricQueryEditor,
nameInput: () => metricQueryEditor().findByPlaceholderText("New metric"),
saveButton: () =>
metricQueryEditor().findByRole("button", { name: "Save" }),
cancelButton: () =>
metricQueryEditor().findByRole("button", { name: "Cancel" }),
header: () => cy.findByTestId("metric-header"),
moreMenu: () => DataStudio.Metrics.header().icon("ellipsis"),
overviewTab: () => DataStudio.Metrics.header().findByText("Overview"),
definitionTab: () => DataStudio.Metrics.header().findByText("Definition"),
dependenciesTab: () =>
DataStudio.Metrics.header().findByText("Dependencies"),
},
Models: {
overviewPage: modelOverviewPage,
queryEditor: modelQueryEditor,
fieldsPage: modelFieldsPage,
nameInput: () => modelQueryEditor().findByPlaceholderText("New model"),
saveButton: () => modelQueryEditor().findByRole("button", { name: "Save" }),
cancelButton: () =>
modelQueryEditor().findByRole("button", { name: "Cancel" }),
header: () => cy.findByTestId("model-header"),
moreMenu: () => DataStudio.Models.header().icon("ellipsis"),
overviewTab: () => DataStudio.Models.header().findByText("Overview"),
definitionTab: () => DataStudio.Models.header().findByText("Definition"),
fieldsTab: () => DataStudio.Models.header().findByText("Fields"),
dependenciesTab: () =>
DataStudio.Models.header().findByText("Dependencies"),
},
Modeling: {
emptyPage: modelingEmptyPage,
collectionPage: collectionPage,
collectionTitle: () => collectionPage().findByRole("heading"),
metricItem: (name: string) => cy.findByTestId("metric-name").contains(name),
modelItem: (name: string) => cy.findByTestId("dataset-name").contains(name),
},
};

View File

@@ -7,6 +7,7 @@ import type {
export const DataModel = {
visit,
visitDataStudio,
get: getDataModel,
TablePicker: {
get: getTablePicker,
@@ -32,6 +33,7 @@ export const DataModel = {
getFieldDescriptionInput: getTableSectionFieldDescriptionInput,
getSortableField: getTableSectionSortableField,
getSortableFields: getTableSectionSortableFields,
getVisibilityTypeInput: getTableSectionVisibilityTypeInput,
clickField: clickTableSectionField,
},
FieldSection: {
@@ -64,19 +66,24 @@ export const DataModel = {
},
};
const DEFAULT_BASE_PATH = "/admin/datamodel";
function visit({
databaseId,
schemaId,
tableId,
fieldId,
skipWaiting = false,
basePath,
}: {
databaseId?: DatabaseId;
fieldId?: FieldId;
schemaId?: SchemaId;
tableId?: TableId;
skipWaiting?: boolean;
basePath?: string;
} = {}) {
const normalizedBasePath = getNormalizedBasePath(basePath);
cy.intercept("GET", "/api/database").as("datamodel/visit/databases");
cy.intercept("GET", "/api/database/*").as("datamodel/visit/database");
cy.intercept("GET", "/api/table/*/query_metadata*").as(
@@ -94,7 +101,7 @@ function visit({
fieldId != null
) {
cy.visit(
`/admin/datamodel/database/${databaseId}/schema/${schemaId}/table/${tableId}/field/${fieldId}`,
`${normalizedBasePath}/database/${databaseId}/schema/${schemaId}/table/${tableId}/field/${fieldId}`,
);
if (!skipWaiting) {
@@ -112,7 +119,7 @@ function visit({
if (databaseId != null && schemaId != null && tableId != null) {
cy.visit(
`/admin/datamodel/database/${databaseId}/schema/${schemaId}/table/${tableId}`,
`${normalizedBasePath}/database/${databaseId}/schema/${schemaId}/table/${tableId}`,
);
if (!skipWaiting) {
@@ -128,7 +135,7 @@ function visit({
}
if (databaseId != null && schemaId != null) {
cy.visit(`/admin/datamodel/database/${databaseId}/schema/${schemaId}`);
cy.visit(`${normalizedBasePath}/database/${databaseId}/schema/${schemaId}`);
if (!skipWaiting) {
cy.wait([
@@ -141,7 +148,7 @@ function visit({
}
if (databaseId != null) {
cy.visit(`/admin/datamodel/database/${databaseId}`);
cy.visit(`${normalizedBasePath}/database/${databaseId}`);
if (!skipWaiting) {
cy.wait([
@@ -154,10 +161,25 @@ function visit({
return;
}
cy.visit("/admin/datamodel");
cy.visit(normalizedBasePath);
cy.wait(["@datamodel/visit/databases"]);
}
function visitDataStudio(options?: Parameters<typeof visit>[0]) {
visit({ ...options, basePath: "/data-studio/data" });
}
function getNormalizedBasePath(path?: string) {
const resolvedPath = path ?? DEFAULT_BASE_PATH;
if (resolvedPath.length === 0) {
return DEFAULT_BASE_PATH;
}
return resolvedPath.endsWith("/")
? resolvedPath.slice(0, resolvedPath.length - 1)
: resolvedPath;
}
function getDataModel() {
return cy.findByTestId("data-model");
}
@@ -226,33 +248,37 @@ function getTableDescriptionInput() {
}
function getTableSortButton() {
return getTableSection().button(/Sorting/);
return getTableSection().findByRole("button", { name: "Sorting" });
}
function getTableSortDoneButton() {
return getTableSection().button(/Done/);
return getTableSection().findByRole("button", { name: "Done" });
}
function getTableSortOrderInput() {
return getTableSection().findByLabelText("Column order");
return getTableSection().findByRole("radiogroup", { name: "Column order" });
}
function getTableSyncOptionsButton() {
return getTableSection().button(/Sync options/);
return getTableSection().findByRole("button", { name: /Sync/ });
}
function getTableSectionField(name: string) {
return getTableSection().findByLabelText(name);
return getTableSection().findByRole("listitem", { name });
}
function getTableSectionSortableField(name: string) {
return getTableSection().findByLabelText(name);
return getTableSection().findByRole("listitem", { name });
}
function getTableSectionSortableFields() {
return getTableSection().findAllByRole("listitem");
}
function getTableSectionVisibilityTypeInput() {
return getTableSection().findByRole("textbox", { name: "Visibility type" });
}
function getTableSectionFieldNameInput(name: string) {
return getTableSectionField(name).findByPlaceholderText(
"Give this field a name",

View File

@@ -0,0 +1,7 @@
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

@@ -32,6 +32,10 @@ export function runButtonOverlay() {
return cy.findByTestId("run-button-overlay");
}
export function runButtonInOverlay() {
return runButtonOverlay().findByTestId("run-button");
}
/**
* Intercepts a request and returns resolve function that allows
* the request to continue

View File

@@ -3,8 +3,9 @@ import type { CyHttpMessages } from "cypress/types/net-stubbing";
import {
entityPickerModal,
entityPickerModalTab,
miniPicker,
miniPickerBrowseAll,
popover,
shouldDisplayTabs,
} from "e2e/support/helpers/e2e-ui-elements-helpers";
import type { NotebookStepType } from "metabase/querying/notebook/types";
import type { IconName } from "metabase/ui";
@@ -177,8 +178,8 @@ export function joinTable(
lhsColumnName?: string,
rhsColumnName?: string,
) {
entityPickerModal().within(() => {
entityPickerModalTab("Tables").click();
miniPicker().within(() => {
cy.realType(tableName);
cy.findByText(tableName).click();
});
@@ -204,22 +205,17 @@ export function selectSavedQuestionsToJoin(
secondQuestionName: string,
) {
cy.intercept("GET", "/api/table/*/query_metadata").as("joinedTableMetadata");
entityPickerModal().within(() => {
shouldDisplayTabs(["Tables", "Collections"]);
entityPickerModalTab("Collections").click();
cy.findByText(firstQuestionName).click();
});
miniPickerBrowseAll().click();
entityPickerModal().findByText(firstQuestionName).click();
cy.wait("@joinedTableMetadata");
// join to question b
cy.icon("join_left_outer").click();
entityPickerModal().within(() => {
shouldDisplayTabs(["Tables", "Collections"]);
entityPickerModalTab("Collections").click();
cy.findByText(secondQuestionName).click();
});
miniPickerBrowseAll().click();
entityPickerModalTab("Data").click();
entityPickerModal().findByText(secondQuestionName).click();
}
export function selectFilterOperator(operatorName: string) {

View File

@@ -96,7 +96,7 @@ export const wrapSyncedCollection = (alias = "syncedCollection", n = 0) => {
cy.request("/api/collection").then(({ body: collections }) => {
const syncedCollection = collections.find(
(c: Collection) => c.type === "remote-synced" && c.location === "/",
(c: Collection) => c.is_remote_synced && c.location === "/",
);
if (syncedCollection) {

View File

@@ -19,8 +19,6 @@ export function snapshot(name) {
* } name
*/
export function restore(name = "default") {
cy.skipOn(name.includes("mongo") && Cypress.env("QA_DB_MONGO") !== true);
cy.log("Restore Data Set");
// automatically reset the data db if this is a test that uses a writable db

View File

@@ -11,7 +11,7 @@ import { createTransform } from "./api";
import { getTableId } from "./e2e-qa-databases-helpers";
export function visitTransform(transformId: TransformId) {
cy.visit(`/admin/transforms/${transformId}`);
cy.visit(`/data-studio/transforms/${transformId}`);
}
const WAIT_TIMEOUT = 10000;
@@ -160,3 +160,44 @@ export function createPythonTransform({
{ wrapId: true, visitTransform },
);
}
/**
* Creates an MBQL transform and runs it to create a table.
* @return: information about the created transform
*/
export function createAndRunMbqlTransform({
sourceTable,
targetTable,
targetSchema,
tagIds,
databaseId,
name,
}: {
sourceTable: string;
targetTable: string;
targetSchema: string | null;
tagIds?: TransformTagId[];
name?: string;
databaseId?: number;
}): Cypress.Chainable<{
transformId: TransformId;
}> {
return createMbqlTransform({
sourceTable,
targetTable,
targetSchema,
tagIds,
databaseId,
name: name ?? "Test transform",
visitTransform: false,
}).then(({ body: transform }) => {
// Run the transform
cy.request("POST", `/api/ee/transform/${transform.id}/run`);
// Wait for it to complete successfully
waitForSucceededTransformRuns();
return cy.wrap({
transformId: transform.id,
});
});
}

View File

@@ -45,6 +45,18 @@ export function selectDropdown() {
return popover().findByRole("listbox");
}
export function miniPicker() {
return cy.findByTestId("mini-picker");
}
export function miniPickerBrowseAll() {
return miniPicker().findByText("Browse all");
}
export function miniPickerHeader() {
return cy.findByTestId("mini-picker-header");
}
export function entityPickerModal() {
return cy.findByTestId("entity-picker-modal");
}
@@ -121,7 +133,7 @@ export function assertNavigationSidebarItemSelected(name, value = "true") {
export function assertNavigationSidebarBookmarkSelected(name, value = "true") {
navigationSidebar()
.findByRole("tab", { name: "Bookmarks" })
.findByRole("section", { name: "Bookmarks" })
.findByRole("listitem", { name })
.should("have.attr", "aria-selected", value);
}
@@ -288,6 +300,10 @@ export const moveColumnDown = (column, distance) => {
.trigger("mouseup", 0, distance * 50, { force: true });
};
/**
* @deprecated Use `moveDnDKitElementByAlias` instead.
* Otherwise, the chain will be broken due to "element was removed from the DOM" error
*/
export const moveDnDKitElement = (
element,
{ horizontal = 0, vertical = 0, onBeforeDragEnd = () => {} } = {},
@@ -407,6 +423,10 @@ export const undoToastList = () => {
return cy.findAllByTestId("toast-undo");
};
export const undoToastListContainer = () => {
return cy.findByTestId("undo-list");
};
export function dashboardCards() {
return cy.get("[data-element-id=dashboard-cards-container]");
}

View File

@@ -0,0 +1,9 @@
export function selectHasValue(label: string, value: string) {
return cy.findByRole("textbox", { name: label }).should("have.value", value);
}
export function selectIsDisabled(label: string) {
return cy
.findByRole("textbox", { name: label })
.should("have.attr", "disabled");
}

View File

@@ -15,6 +15,8 @@ export * from "./e2e-dashboard-helpers";
export * from "./e2e-dashboard-visualizer-helpers";
export * from "./e2e-database-metadata-helpers";
export * from "./e2e-datamodel-helpers";
export * from "./e2e-data-studio-helpers";
export * from "./e2e-dependency-graph-helpers";
export * from "./e2e-detail-view-helpers";
export * from "./e2e-dimension-list-helpers";
export * from "./e2e-document-helpers";
@@ -52,6 +54,7 @@ export * from "./e2e-token-helpers";
export * from "./e2e-transform-helpers";
export * from "./e2e-ui-elements-helpers";
export * from "./e2e-ui-elements-overflow-helpers";
export * from "./e2e-ui-select";
export * from "./e2e-upload-helpers";
export * from "./e2e-users-helpers";
export * from "./e2e-visual-tests-helpers";

View File

@@ -0,0 +1,93 @@
const { H } = cy;
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import type { StructuredQuestionDetails } from "e2e/support/helpers/api";
import type { Collection } from "metabase-types/api";
const { ORDERS_ID } = SAMPLE_DATABASE;
type StructuredQuestionDetailsWithName = StructuredQuestionDetails & {
name: string;
};
type LibraryResponse = Collection & {
effective_children?: Collection[];
};
export const TRUSTED_ORDERS_MODEL: StructuredQuestionDetailsWithName = {
name: "Trusted Orders Model",
query: {
"source-table": ORDERS_ID,
},
};
export const TRUSTED_ORDERS_METRIC: StructuredQuestionDetailsWithName = {
name: "Trusted Orders Metric",
query: {
"source-table": ORDERS_ID,
aggregation: [["count"]],
},
};
export function createLibraryWithItems() {
return H.createLibrary().then(
(response: Cypress.Response<LibraryResponse>) => {
const body = response.body;
const modelsCollection = body.effective_children?.find(
(child) => child.name === "Data",
);
const metricsCollection = body.effective_children?.find(
(child) => child.name === "Metrics",
);
return H.createQuestion(TRUSTED_ORDERS_MODEL, {
wrapId: true,
idAlias: "trustedOrdersModelId",
}).then(() =>
cy.get("@trustedOrdersModelId").then((modelId) =>
cy
.request("PUT", `/api/card/${modelId}`, {
type: "model",
collection_id: modelsCollection?.id,
})
.then(() =>
H.createQuestion(TRUSTED_ORDERS_METRIC, {
wrapId: true,
idAlias: "trustedOrdersMetricId",
}),
)
.then(() =>
cy.get("@trustedOrdersMetricId").then((metricId) =>
cy.request("PUT", `/api/card/${metricId}`, {
type: "metric",
collection_id: metricsCollection?.id,
}),
),
),
),
);
},
);
}
export function createLibraryWithModel() {
return H.createLibrary().then(
(response: Cypress.Response<LibraryResponse>) => {
const body = response.body;
const modelsCollection = body.effective_children?.find(
(child) => child.name === "Data",
);
return H.createQuestion(TRUSTED_ORDERS_MODEL, {
wrapId: true,
idAlias: "trustedOrdersModelId",
}).then(() =>
cy.get("@trustedOrdersModelId").then((modelId) =>
cy.request("PUT", `/api/card/${modelId}`, {
type: "model",
collection_id: modelsCollection?.id,
}),
),
);
},
);
}

View File

@@ -5,10 +5,7 @@ import {
WRITABLE_DB_ID,
} from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
ORDERS_DASHBOARD_ID,
ORDERS_MODEL_ID,
} from "e2e/support/cypress_sample_instance_data";
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
import {
createMockActionParameter,
createMockParameter,
@@ -278,44 +275,76 @@ describe("issue 51020", () => {
}
describe("when primary key is called 'id'", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.setActionsEnabledForDB(SAMPLE_DB_ID);
function createTemporaryTable() {
H.queryWritableDB(
"CREATE TABLE IF NOT EXISTS foo (id INT PRIMARY KEY, name VARCHAR)",
);
H.queryWritableDB(
"INSERT INTO foo (id, name) VALUES (1, 'Foo'), (2, 'Bar')",
);
}
H.visitModel(ORDERS_MODEL_ID);
function dropTemporaryTable() {
H.queryWritableDB("ALTER TABLE IF EXISTS foo DROP CONSTRAINT foo_pkey");
H.queryWritableDB("DROP TABLE IF EXISTS foo");
}
beforeEach(() => {
H.restore("postgres-writable");
cy.signInAsAdmin();
dropTemporaryTable();
createTemporaryTable();
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "foo" });
H.getTableId({ name: "foo" }).then((tableId) => {
H.createQuestion(
{
name: "Model 51020",
type: "model",
database: WRITABLE_DB_ID,
query: {
"source-table": tableId,
},
},
{
visitQuestion: true,
},
);
});
setupBasicActionsInModel();
setupDashboard({
modelName: "Orders Model",
questionName: "Orders Model",
modelName: "Model 51020",
questionName: "Model 51020",
columnName: "ID",
});
});
afterEach(() => {
dropTemporaryTable();
});
it("should pass primary key attribute to execute action endpoint when it's populated with click behavior or URL (metabase#51020)", () => {
cy.log(
"check when primary key parameter is populated with click behavior",
);
H.getDashboardCard(0).findAllByText("1").eq(0).click();
H.getDashboardCard(1).findByText("Click Me").click();
H.modal().findByLabelText("Discount").type("987");
H.modal().findByLabelText("Name").type(" Baz");
H.modal().button("Update").click();
H.modal().should("not.exist");
H.undoToast().findByText("Successfully updated").should("be.visible");
H.getDashboardCard(0).should("contain.text", "987");
H.getDashboardCard(0).should("contain.text", "Foo Baz");
cy.log("check when primary key parameter is populated with URL");
cy.reload();
H.getDashboardCard(1).findByText("Click Me").click();
H.modal()
.findByLabelText("Discount")
.type("{backspace}{backspace}{backspace}654");
H.modal().findByLabelText("Name").type(" Baz");
H.modal().button("Update").click();
H.modal().should("not.exist");
H.undoToast().findByText("Successfully updated").should("be.visible");
H.getDashboardCard(0).should("contain.text", "654");
H.getDashboardCard(0).should("contain.text", "Foo Baz Baz");
});
});
@@ -350,12 +379,9 @@ describe("issue 51020", () => {
cy.findByTestId("new-model-options")
.findByText("Use the notebook editor")
.click();
H.entityPickerModalTab("Collections").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.findByPlaceholderText("Search this collection or everywhere…").type(
"foo",
);
cy.findByText("Everywhere").click();
cy.findByPlaceholderText("Search…").type("foo");
cy.findByText("Foo").click();
});
cy.findByTestId("run-button").click();
@@ -370,7 +396,8 @@ describe("issue 51020", () => {
setupBasicActionsInModel();
H.newButton("Question").click();
H.entityPickerModalTab("Collections").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalTab("Data").click();
H.entityPickerModalItem(1, "Model 51020").click();
H.saveQuestion("Question 51020", undefined, {
tab: "Browse",

View File

@@ -108,8 +108,7 @@ describe("issue 41765", { tags: "@external" }, () => {
H.appBar().findByText("New").click();
H.popover().findByText("Question").click();
H.entityPickerModal().within(() => {
cy.findByText("Tables").click();
H.miniPicker().within(() => {
cy.findByText(WRITABLE_DB_DISPLAY_NAME).click();
cy.findByText(TEST_TABLE_DISPLAY_NAME).click();
});

View File

@@ -261,8 +261,11 @@ describe("admin > database > database routing", () => {
cy.log("should not see database in data picker");
cy.visit("/question/notebook");
H.miniPicker()
.findByText(BASE_POSTGRES_DESTINATION_DB_INFO.name)
.should("not.exist");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText(BASE_POSTGRES_DESTINATION_DB_INFO.name).should(
"not.exist",
);

View File

@@ -522,8 +522,8 @@ describe("scenarios > admin > datamodel", () => {
verifyAndCloseToast("Hid Orders");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").should("be.visible");
cy.findByText("Orders").should("not.exist");
});
@@ -540,8 +540,8 @@ describe("scenarios > admin > datamodel", () => {
verifyAndCloseToast("Unhid Orders");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").should("be.visible");
cy.findByText("Orders").should("be.visible");
});
@@ -741,8 +741,8 @@ describe("scenarios > admin > datamodel", () => {
// It shouldn't show in a new question data picker
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.contains("Products").should("exist");
cy.contains("Orders").should("not.exist");
});
@@ -938,8 +938,8 @@ describe("scenarios > admin > datamodel", () => {
TableSection.getNameInput().should("have.value", "New orders");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").should("be.visible");
cy.findByText("New orders").should("be.visible");
});
@@ -964,8 +964,8 @@ describe("scenarios > admin > datamodel", () => {
cy.signInAsNormalUser();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").should("be.visible");
cy.findByText("New orders").should("be.visible");
});
@@ -1879,8 +1879,8 @@ describe("scenarios > admin > datamodel", () => {
mode: "notebook",
});
cy.icon("join_left_outer").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
cy.findByLabelText("Left column").should("contain.text", "User ID");
@@ -1936,8 +1936,8 @@ describe("scenarios > admin > datamodel", () => {
mode: "notebook",
});
cy.icon("join_left_outer").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
cy.findByLabelText("Left column").should("contain.text", "User ID");
@@ -3009,6 +3009,7 @@ describe("scenarios > admin > datamodel", () => {
FieldSection.getNameInput().should("have.value", "A");
FieldSection.get()
.findByTestId("name-prefix")
.scrollIntoView()
.should("be.visible")
.and("have.text", "Json:");
verifyTablePreview({
@@ -3454,7 +3455,7 @@ describe("scenarios > admin > datamodel", () => {
cy.log("discard field values");
H.modal().button("Discard cached field values").click();
verifyAndCloseToast("Failed to discard values");
cy.realPress("Escape");
H.modal().findByLabelText("Close").click();
cy.log("field name");
TableSection.getFieldNameInput("Quantity").type("a").blur();
@@ -3503,7 +3504,9 @@ describe("scenarios > admin > datamodel", () => {
verifyAndCloseToast("Failed to update display values of Quantity");
cy.log("JSON unfolding");
TablePicker.getDatabase("Writable Postgres12").click();
// navigating away would cause onChange to be triggered in InputBlurChange and TextareaBlurChange
// components, so new undos will appear - this makes this test flaky, so we navigate with page reload instead
H.DataModel.visit({ databaseId: WRITABLE_DB_ID });
TablePicker.getTable("Many Data Types").click();
TableSection.clickField("Json");
FieldSection.getUnfoldJsonInput().click();
@@ -3511,6 +3514,7 @@ describe("scenarios > admin > datamodel", () => {
verifyAndCloseToast("Failed to disable JSON unfolding for Json");
cy.log("formatting");
TablePicker.getDatabase("Sample Database").click();
TablePicker.getTable("Orders").click();
TableSection.clickField("Quantity");
FieldSection.getPrefixInput().type("5").blur();
@@ -3903,7 +3907,7 @@ function verifyToastAndUndo(message: string) {
H.undoToast().should("contain.text", message);
H.undoToast().button("Undo").click();
H.undoToast().should("contain.text", "Change undone");
H.undoToast().icon("close").click();
H.undoToast().icon("close").click({ force: true });
}
function verifyTablesVisible(tables: string[]) {

View File

@@ -441,8 +441,8 @@ describe("issue 55619", () => {
cy.findByLabelText("Create a new model").click();
cy.findByText("Use the notebook editor").click();
});
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.runButtonOverlay().click();

View File

@@ -512,6 +512,31 @@ describe("Remote Sync", () => {
});
});
it("should disable 'Set up Remote Sync' button if git url is not set (#65653)", () => {
cy.visit("/admin/settings/remote-sync");
cy.button("Set up Remote Sync").should("be.disabled");
cy.findByRole("switch", { name: "Auto-sync with git" }).click({
force: true,
});
// Trivial dirty state should not be enough to enable the button
cy.button("Set up Remote Sync").should("be.disabled");
cy.findByLabelText(/Access Token/i)
.clear()
.type("SecretToken");
// Still disabled - url is not set
cy.button("Set up Remote Sync").should("be.disabled");
cy.findByLabelText(/repository url/i)
.clear()
.type(LOCAL_GIT_URL);
// Enabled now - url is set
cy.button("Set up Remote Sync").should("be.enabled");
});
it("shows an error if git settings are invalid", () => {
cy.intercept("PUT", "/api/ee/remote-sync/settings").as("saveSettings");
cy.visit("/admin/settings/remote-sync");

View File

@@ -17,8 +17,8 @@ describe("binning related reproductions", () => {
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("16327").click();
});
@@ -89,8 +89,8 @@ describe("binning related reproductions", () => {
);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("17975").click();
});
@@ -131,8 +131,8 @@ describe("binning related reproductions", () => {
cy.icon("join_left_outer").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("18646").click();
});
@@ -177,8 +177,8 @@ describe("binning related reproductions", () => {
// it is essential for this repro to find question following these exact steps
// (for example, visiting `/collection/root` would yield different result)
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("11439").click();
});
@@ -370,8 +370,8 @@ describe("binning related reproductions", () => {
function openSummarizeOptions(questionType) {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("16379").click();
});

View File

@@ -57,8 +57,8 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
beforeEach(() => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("QB Binning").click();
});
@@ -124,8 +124,8 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
beforeEach(() => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("QB Binning").click();
});
@@ -197,8 +197,8 @@ describe("scenarios > binning > from a saved QB question with explicit joins", (
beforeEach(() => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("QB Binning").click();
});

View File

@@ -99,12 +99,21 @@ describe("issue 24660", () => {
it("should properly show contents of different collections with the same name (metabase#24660)", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findAllByText(collectionName).first().click();
cy.findByText(questions[ORDERS_QUESTION_ID]).should("exist");
cy.findByText(questions[ORDERS_COUNT_QUESTION_ID]).should("not.exist");
cy.realType("{esc}");
});
cy.findByPlaceholderText("Search for tables and more...").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findAllByText(collectionName).first().click();
cy.findByText(questions[ORDERS_QUESTION_ID]).should("exist");
cy.findByText(questions[ORDERS_COUNT_QUESTION_ID]).should("not.exist");
});
});
});

View File

@@ -77,8 +77,12 @@ describe("scenarios > collections > trash", () => {
});
H.popover().findByText("Question").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").should("exist");
cy.findByText("Trash").should("not.exist");
});
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText("Our analytics").should("exist");
cy.findByText("Trash").should("not.exist");
cy.button("Close").click();
@@ -994,7 +998,7 @@ function assertTrashSelectedInNavigationSidebar() {
}
function ensureBookmarkVisible(bookmark) {
cy.findByRole("tab", { name: /bookmarks/i })
cy.findByRole("section", { name: "Bookmarks" })
.findByText(bookmark)
.should("be.visible");
}

View File

@@ -394,8 +394,7 @@ describe("exercise today() function", () => {
function startNewQuestion() {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Products").click();
});

View File

@@ -109,8 +109,7 @@ describe("issue 13751", { tags: "@external" }, () => {
cy.signInAsAdmin();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(PG_DB_NAME).should("be.visible").click();
cy.findByTextEnsureVisible("People").click();
});
@@ -816,16 +815,10 @@ describe("issue 25189", { tags: "@skip" }, () => {
it("should display all summarize options if the only numeric field is a custom column (metabase#27745)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByPlaceholderText("Search this collection or everywhere…").type(
"colors",
);
cy.findByText("Everywhere").click();
H.miniPicker().within(() => {
cy.realType("colors");
cy.wait("@search");
cy.findByTestId("result-item")
.contains(/colors/i)
.click();
cy.contains(/colors/i).click();
});
cy.findByLabelText("Custom column").click();
H.enterCustomColumnDetails({
@@ -1295,8 +1288,10 @@ describe("issue 49305", () => {
// This bug does not reproduce if the base question is created via H.createQuestion or H.visitQuestionAdhoc, so create it manually in the UI.
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(2, "Products").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.getNotebookStep("data").button("Custom column").click();
H.enterCustomColumnDetails({
formula: 'concat("49305 ", [Title])',
@@ -2311,7 +2306,7 @@ describe("Issue 38498", { tags: "@external" }, () => {
cy.signInAsAdmin();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -2341,8 +2336,8 @@ describe("issue 52451", () => {
});
H.popover().button("Done").click();
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
H.popover().findByText("Expr").click();
@@ -2382,13 +2377,13 @@ describe("issue 56602", () => {
H.createQuestion(productsModelDetails);
H.createQuestion(ordersModelDetails);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.realType(productsModelDetails.name);
cy.findByText(productsModelDetails.name).click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.realType(ordersModelDetails.name);
cy.findByText(ordersModelDetails.name).click();
});
H.addCustomColumn();

View File

@@ -65,9 +65,9 @@ describe("scenarios > question > custom column", () => {
H.createQuestion({ name, query: { "source-table": ORDERS_ID } });
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText(name).click();
H.entityPickerModalLevel(1).findByText(name).click();
});
cy.button("Custom column").click();
H.enterCustomColumnDetails({ formula: "[cre", blur: false });
@@ -268,8 +268,9 @@ describe("scenarios > question > custom column", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Join data").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalLevel(0).findByText("Databases").click();
cy.findByText("Products").click();
});
@@ -1080,8 +1081,7 @@ describe(
it("should understand date functions", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -1738,8 +1738,7 @@ describe("scenarios > question > custom column > splitPart", () => {
cy.signInAsAdmin();
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("People").click();
});
@@ -1797,8 +1796,7 @@ describe("exercise today() function", () => {
it("should show today's date", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Products").click();
});

View File

@@ -33,9 +33,9 @@ describe("scenarios > dashboard > filters > query stages + temporal unit paramet
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(2, "Orders").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.getNotebookStep("filter")

View File

@@ -26,8 +26,10 @@ describe("Dashboard > Dashboard Questions", () => {
cy.visit(`/dashboard/${S.ORDERS_DASHBOARD_ID}`);
H.newButton("Question").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModal().findByText("Orders Model").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders Model").click();
});
H.getNotebookStep("filter")
.findByText("Add filters to narrow your answer")
.click();
@@ -375,8 +377,8 @@ describe("Dashboard > Dashboard Questions", () => {
cy.findByLabelText("Navigation bar").findByText("New").click();
H.popover().findByText("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cy.findByTestId("qb-header").findByText("Save").click();
@@ -421,7 +423,8 @@ describe("Dashboard > Dashboard Questions", () => {
});
H.startNewQuestion();
H.entityPickerModalTab("Collections").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Our analytics").click();
H.entityPickerModal().findByText("Orders in a dashboard").click();
H.entityPickerModal()
.findByText("Total Orders Dashboard Question")
@@ -684,11 +687,9 @@ describe("Dashboard > Dashboard Questions", () => {
H.visitDashboard(dashboardId);
H.openDashboardMenu("Move to trash");
H.modal().button("Move to trash").click();
cy.findByText(/gone wrong/, { timeout: 0 }).should("not.exist");
cy.findByTestId("archive-banner").findByText(/is in the trash/);
cy.findByTestId("sidebar-toggle").click();
H.openNavigationSidebar();
// restore it
H.navigationSidebar().findByText("Trash").click();
@@ -1056,8 +1057,8 @@ describe("Dashboard > Dashboard Questions", () => {
H.visitDashboard(S.ORDERS_DASHBOARD_ID);
H.newButton("Question").click();
H.entityPickerModalTab("Tables").click();
H.entityPickerModal().findByText("Products").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Products").click();
H.queryBuilderHeader().button("Save").click();
// should not show dashboard you can't write to

View File

@@ -54,7 +54,6 @@ describe("scenarios > dashboard", () => {
"pressing escape should only close the entity picker modal, not the new dashboard modal",
);
H.modal().findByTestId("collection-picker-button").click();
H.entityPickerModal().findByText("Select a collection");
cy.realPress("Escape");
H.modal().findByText("New dashboard").should("be.visible");
@@ -88,12 +87,9 @@ describe("scenarios > dashboard", () => {
cy.findByTestId("dashboard-empty-state").button("Add a chart").click();
cy.findByTestId("new-button-bar").findByText("New Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByPlaceholderText("Search this collection or everywhere…").type(
"Pro",
);
cy.findByText("Everywhere").click();
cy.findByPlaceholderText("Search…").type("Pro");
cy.findByText("Products").click();
});

View File

@@ -0,0 +1,252 @@
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
const { H } = cy;
const { TablePicker } = H.DataModel;
interface TablesActionRequest {
database_ids: number[];
schema_ids: number[];
table_ids: number[];
}
interface TablesActionsResponse {
status: string;
message: string;
}
interface Table {
display_name: string;
id: number;
}
interface PublishModelResponse {
models: {
id: number;
}[];
}
describe("bulk table operations", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
cy.intercept("POST", "/api/ee/data-studio/table/sync-schema").as(
"syncSchema",
);
cy.intercept("POST", "/api/ee/data-studio/table/rescan-values").as(
"rescanValues",
);
cy.intercept("POST", "/api/ee/data-studio/table/discard-values").as(
"discardValues",
);
cy.intercept(
"GET",
`/api/database/${WRITABLE_DB_ID}/schema/public?include_hidden=true&include_editable_data_model=true`,
).as("getSchema");
cy.intercept("POST", "/api/ee/data-studio/table/publish-model").as(
"publishModel",
);
});
it("syncing multiple tables", { tags: ["@external"] }, () => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12").click();
cy.wait("@getSchema").then(({ response }) => {
const tables = response?.body ?? [];
const accountTableId = getTableId(tables, "Accounts");
const feedbackTableId = getTableId(tables, "Feedback");
cy.wrap([accountTableId, feedbackTableId]).as("tableIds");
});
TablePicker.getTable("Accounts").find('input[type="checkbox"]').check();
TablePicker.getTable("Feedback").find('input[type="checkbox"]').check();
cy.findByRole("heading", { name: /2 tables selected/ });
cy.findByRole("button", { name: /Sync settings/ }).click();
cy.findByRole("button", { name: /Sync table schemas/ }).click();
cy.findByRole("button", { name: /Sync triggered!/ }).should("be.visible");
cy.get<number[]>("@tableIds").then((tableIds) => {
cy.wait<TablesActionRequest, TablesActionsResponse>("@syncSchema").then(
({ request, response }) => {
expect(request.body.table_ids).to.deep.eq(tableIds);
expect(response?.body.status).to.eq("ok");
},
);
});
cy.findByRole("button", { name: /Re-scan tables/ }).click();
cy.findByRole("button", { name: /Scan triggered!/ }).should("be.visible");
cy.get<number[]>("@tableIds").then((tableIds) => {
cy.wait<TablesActionRequest, TablesActionsResponse>("@rescanValues").then(
({ request, response }) => {
expect(request.body.table_ids).to.deep.eq(tableIds);
expect(response?.body.status).to.eq("ok");
},
);
});
cy.findByRole("button", { name: /Discard cached field values/ }).click();
cy.findByRole("button", { name: /Discard triggered!/ }).should(
"be.visible",
);
cy.get<number[]>("@tableIds").then((tableIds) => {
cy.wait<TablesActionRequest, TablesActionsResponse>(
"@discardValues",
).then(({ request, response }) => {
expect(request.body.table_ids).to.deep.eq(tableIds);
expect(response?.body.status).to.eq("ok");
});
});
});
it("allows publishing multiple tables", { tags: ["@external"] }, () => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
cy.signInAsAdmin();
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12").click();
TablePicker.getTable("Accounts").find('input[type="checkbox"]').check();
TablePicker.getTable("Feedback").find('input[type="checkbox"]').check();
cy.findByRole("button", { name: /Publish/ }).click();
cy.findByLabelText("Don’t show this to me again").check();
cy.findByRole("button", { name: /Got it/ }).click();
H.pickEntity({
tab: "Collections",
path: ["Our analytics"],
});
cy.findByRole("button", { name: /Publish here/ }).click();
cy.wait<PublishModelResponse>("@publishModel").then(({ response }) => {
expect(response?.body.created_count).to.eq(2);
});
H.undoToast().within(() => {
cy.findByText("Published").should("be.visible");
cy.findByRole("button", { name: /See it/ }).click();
});
cy.findByTestId("collection-caption").within(() => {
cy.findByText("Our analytics").should("be.visible");
});
});
it("allows to edit attributes for tables", { tags: ["@external"] }, () => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
cy.signInAsAdmin();
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12").click();
TablePicker.getTable("Accounts").find('input[type="checkbox"]').check();
TablePicker.getTable("Feedback").find('input[type="checkbox"]').check();
H.selectHasValue("Owner", "").click();
H.selectDropdown().contains("Bobby Tables").click();
H.selectHasValue("Visibility type", "").click();
H.selectDropdown().contains("Gold").click();
H.selectHasValue("Entity type", "").click();
H.selectDropdown().contains("Person").click();
H.selectHasValue("Source", "").click();
H.selectDropdown().contains("Ingested").click();
H.undoToastList().should("have.length", 4);
TablePicker.getTable("Accounts")
.findByTestId("table-owner")
.should("have.text", "Bobby Tables");
TablePicker.getTable("Feedback")
.findByTestId("table-owner")
.should("have.text", "Bobby Tables");
});
it("allows to edit attributes for db", { tags: ["@external"] }, () => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
cy.signInAsAdmin();
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12")
.find('input[type="checkbox"]')
.check();
H.selectHasValue("Owner", "").click();
H.selectDropdown().contains("Bobby Tables").click();
H.selectHasValue("Visibility type", "").click();
H.selectDropdown().contains("Gold").click();
H.selectHasValue("Entity type", "").click();
H.selectDropdown().contains("Person").click();
H.selectHasValue("Source", "").click();
H.selectDropdown().contains("Ingested").click();
cy.findByRole("button", { name: /Publish/ }).click();
cy.findByRole("button", { name: /Got it/ }).click();
H.pickEntity({
tab: "Collections",
path: ["Our analytics"],
});
cy.findByRole("button", { name: /Publish here/ }).click();
TablePicker.getDatabase("Writable Postgres12").click();
cy.findAllByTestId("tree-item")
.filter('[data-type="table"]')
.each((table) => {
cy.wrap(table)
.findByTestId("table-owner")
.should("have.text", "Bobby Tables");
cy.wrap(table)
.findByTestId("table-published")
.findByLabelText("Published")
.should("be.visible");
});
});
it("allows to edit attributes for schema", { tags: ["@external"] }, () => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
H.resetTestTable({ type: "postgres", table: "many_schemas" });
cy.signInAsAdmin();
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "Animals" });
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12").click();
TablePicker.getSchema("Schema A").find('input[type="checkbox"]').check();
TablePicker.getSchema("Schema B").find('input[type="checkbox"]').check();
cy.findByRole("heading", { name: /Multiple tables selected/ }).click();
H.selectHasValue("Owner", "").click();
H.selectDropdown().contains("Bobby Tables").click();
H.selectHasValue("Visibility type", "").click();
H.selectDropdown().contains("Gold").click();
H.selectHasValue("Entity type", "").click();
H.selectDropdown().contains("Person").click();
H.selectHasValue("Source", "").click();
H.selectDropdown().contains("Ingested").click();
TablePicker.getSchema("Schema A").click();
TablePicker.getSchema("Schema B").click();
cy.findByTestId("loading-placeholder").should("not.exist");
cy.findAllByTestId("tree-item")
.filter('[data-type="table"]')
.each((table) => {
cy.wrap(table)
.findByTestId("table-owner")
.should("have.text", "Bobby Tables");
});
});
});
function getTableId(tables: Table[], tableName: string) {
return tables.find((table: Table) => table.display_name === tableName)?.id;
}

View File

@@ -0,0 +1,200 @@
import { WRITABLE_DB_ID } from "e2e/support/cypress_data";
const { H } = cy;
const { TablePicker } = H.DataModel;
interface MetadataResponse {
updated_at: string;
data_layer: string;
view_count: number;
}
interface PublishModelResponse {
models: {
id: number;
}[];
}
describe("Table editing", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
cy.intercept("GET", "/api/database?*").as("databases");
cy.intercept("GET", "/api/database/*/schemas?*").as("schemas");
cy.intercept("GET", "/api/table/*/query_metadata*").as("metadata");
cy.intercept("GET", "/api/database/*/schema/*").as("schema");
cy.intercept("POST", "/api/dataset*").as("dataset");
cy.intercept("GET", "/api/field/*/values").as("fieldValues");
cy.intercept("PUT", "/api/field/*", cy.spy().as("updateFieldSpy")).as(
"updateField",
);
cy.intercept("PUT", "/api/table/*/fields/order").as("updateFieldOrder");
cy.intercept("POST", "/api/field/*/values").as("updateFieldValues");
cy.intercept("POST", "/api/field/*/dimension").as("updateFieldDimension");
cy.intercept("PUT", "/api/table").as("updateTables");
cy.intercept("PUT", "/api/table/*").as("updateTable");
cy.intercept("POST", "/api/ee/data-studio/table/publish-model").as(
"publishModel",
);
});
it("should display metadata information", { tags: ["@external"] }, () => {
H.restore("mysql-8");
H.activateToken("bleeding-edge");
H.DataModel.visitDataStudio();
TablePicker.getDatabase("QA MySQL8").click();
TablePicker.getTable("Orders").click();
cy.wait<MetadataResponse>("@metadata").then(({ response }) => {
const updatedAt = response?.body.updated_at ?? "";
const expectedDate = new Date(updatedAt).toLocaleString();
const viewCount = response?.body.view_count ?? 0;
cy.findByLabelText("Name in the database").should("have.text", "ORDERS");
cy.findByLabelText("Last updated at").should("have.text", expectedDate);
cy.findByLabelText("View count").should("have.text", viewCount);
cy.findByLabelText("Est. row count").should("not.exist");
cy.findByLabelText("Dependencies").should("have.text", "0");
cy.findByLabelText("Dependents").should("have.text", "0");
cy.findByRole("link", { name: "Dependency graph" }).click();
cy.findByRole("heading", { name: "Dependency graph" }).should(
"be.visible",
);
});
});
it(
"should publish single table to a collection",
{ tags: ["@external"] },
() => {
H.restore("mysql-8");
H.activateToken("bleeding-edge");
H.DataModel.visitDataStudio();
TablePicker.getDatabase("QA MySQL8").click();
TablePicker.getTable("Orders").click();
// Shows publish model information modal
cy.findByRole("button", { name: /Publish/ }).click();
cy.findByRole("button", { name: /Got it/ }).click();
H.modal().within(() => {
cy.findByRole("button", { name: /Cancel/ }).click();
});
// Don't show this again, info should not be shown later
cy.findByRole("button", { name: /Publish/ }).click();
cy.findByLabelText("Don’t show this to me again").check();
cy.findByRole("button", { name: /Got it/ }).click();
H.modal().within(() => {
cy.findByRole("button", { name: /Cancel/ }).click();
});
cy.findByRole("button", { name: /Publish/ }).click();
H.pickEntity({
tab: "Collections",
path: ["Our analytics"],
});
cy.findByRole("button", { name: /Publish here/ }).click();
cy.wait<PublishModelResponse>("@publishModel").then(() => {
H.undoToast().within(() => {
cy.findByText("Published").should("be.visible");
cy.findByRole("button", { name: /See it/ }).click();
});
cy.findByTestId("head-crumbs-container").within(() => {
cy.findByText("Our analytics").should("be.visible");
cy.findByText("Model based on ORDERS").should("be.visible");
});
});
// Should not show info modal again
cy.go("back");
cy.findByRole("button", { name: /Publish/ }).click();
H.modal().within(() => {
cy.findByText("Pick the collection to publish this table in").should(
"be.visible",
);
});
},
);
it("should allow to edit attributes", { tags: ["@external"] }, () => {
H.restore("postgres-12");
H.activateToken("bleeding-edge");
H.DataModel.visitDataStudio();
TablePicker.getDatabase("QA Postgres12").click();
TablePicker.getTable("Orders").click();
H.selectHasValue("Owner", "Unspecified").click();
H.selectDropdown().contains("Bobby Tables").click();
H.undoToastListContainer()
.findByText("Table owner updated")
.should("be.visible");
H.selectHasValue("Visibility type", "Bronze").click();
H.selectDropdown().contains("Gold").click();
H.undoToastListContainer()
.findByText("Table visibility type updated")
.should("be.visible");
H.selectHasValue("Entity type", "Transaction").click();
H.selectDropdown().contains("Person").click();
H.undoToastListContainer()
.findByText("Entity type updated")
.should("be.visible");
H.selectHasValue("Source", "Unspecified").click();
H.selectDropdown().contains("Ingested").click();
H.undoToastListContainer()
.findByText("Table data source updated")
.should("be.visible");
// navigate away and back
TablePicker.getTable("Products").click();
TablePicker.getTable("Orders").click();
H.selectHasValue("Owner", "Bobby Tables");
H.selectHasValue("Visibility type", "Gold");
H.selectHasValue("Entity type", "Person");
H.selectHasValue("Source", "Ingested");
});
it(
"transform-created table should have link and disabled source edit",
{ tags: ["@external"] },
() => {
H.restore("postgres-writable");
H.activateToken("bleeding-edge");
H.resetTestTable({ type: "postgres", table: "many_schemas" });
const SOURCE_TABLE = "Animals";
const TARGET_TABLE = "transform_table";
const TARGET_SCHEMA = "Schema A";
const TRANSFORM_TABLE_DISPLAY_NAME = "Transform Table";
const TRANSFORM_NAME = "Test transform for animals";
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: SOURCE_TABLE });
// Create and run a transform to create a table
H.createAndRunMbqlTransform({
sourceTable: SOURCE_TABLE,
targetTable: TARGET_TABLE,
targetSchema: TARGET_SCHEMA,
name: TRANSFORM_NAME,
}).then(() => {
H.DataModel.visitDataStudio();
TablePicker.getDatabase("Writable Postgres12").click();
TablePicker.getSchema(TARGET_SCHEMA).click();
TablePicker.getTable(TRANSFORM_TABLE_DISPLAY_NAME).click();
cy.findByRole("link", { name: new RegExp(TRANSFORM_NAME) }).should(
"be.visible",
);
H.selectIsDisabled("Source");
});
},
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
ALL_USERS_GROUP_ID,
ORDERS_MODEL_ID,
} from "e2e/support/cypress_sample_instance_data";
import {
createLibraryWithItems,
createLibraryWithModel,
} from "e2e/support/test-library-data";
const { H } = cy;
describe("scenarios > data studio > modeling > library", () => {
beforeEach(() => {
H.restore();
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
});
it("should create library via UI and verify collections", () => {
cy.intercept("POST", "/api/ee/library").as("createLibrary");
cy.intercept("GET", "/api/collection/tree*").as("getCollectionTree");
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Click Library sidebar section to open modal");
H.DataStudio.ModelingSidebar.root()
.findByText("Library")
.should("be.visible")
.click();
cy.log("Create library via modal");
H.modal().within(() => {
cy.findByText("Create your Library").should("be.visible");
cy.button("Create my Library").click();
});
cy.wait("@createLibrary");
cy.wait("@getCollectionTree");
cy.log("Verify library collections appear in sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().within(() => {
cy.findByText("Data").should("be.visible");
cy.findByText("Metrics").should("be.visible");
});
cy.log("Verify empty state shows on library root");
H.DataStudio.Modeling.collectionPage().within(() => {
cy.findByText("No models or metrics yet").should("be.visible");
cy.findByText(
"Models and metrics in this collection will appear here.",
).should("be.visible");
});
cy.log("Select Data collection and verify it's empty");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Verify no models yet message");
H.DataStudio.Modeling.collectionPage().within(() => {
cy.findByText("No models yet").should("be.visible");
});
});
it("should be available in the data picker", () => {
createLibraryWithItems();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Library").click();
H.entityPickerModalItem(1, "Data").click();
H.entityPickerModalItem(2, "Trusted Orders Model").click();
cy.log("Ensure that the we can build the path from a value");
cy.button(/Trusted Orders Model/).click();
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Library").should(
"have.attr",
"data-active",
"true",
);
H.entityPickerModalItem(1, "Data").should(
"have.attr",
"data-active",
"true",
);
H.entityPickerModalItem(2, "Trusted Orders Model").should(
"have.attr",
"data-active",
"true",
);
});
it("should let you move models and metrics into the library, even when empty", () => {
H.createLibrary();
H.visitModel(ORDERS_MODEL_ID);
H.openQuestionActions("Duplicate");
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Library").click();
H.entityPickerModalItem(1, "Metrics").should(
"have.attr",
"data-disabled",
"true",
);
H.entityPickerModalItem(1, "Data").click();
H.entityPickerModal().button("Select this collection").click();
H.modal().button("Duplicate").click();
});
it("should show the library collection even if only 1 child collection has items", () => {
createLibraryWithModel();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Library").click();
H.entityPickerModalItem(1, "Data").click();
H.entityPickerModalItem(2, "Trusted Orders Model").should("exist");
});
it("should be available in the question picker modal", () => {
createLibraryWithItems();
const library = () => H.entityPickerModalLevel(0).findByText("Library");
cy.visit(
`/admin/permissions/data/group/${ALL_USERS_GROUP_ID}/database/${SAMPLE_DB_ID}/schema/PUBLIC/${SAMPLE_DATABASE.ACCOUNTS_ID}/segmented`,
);
H.modal()
.findByText(/Use a saved question/)
.click();
H.modal()
.button(/Select a question/)
.click();
H.entityPickerModal().within(() => {
// Questions tab should not show the library, because the library can only contain models and metrics
H.entityPickerModalTab("Questions").click();
library().should("not.exist");
H.entityPickerModalItem(0, "Our analytics").should("exist");
// Question picker should show library in models tab
H.entityPickerModalTab("Models").click();
H.entityPickerModalItem(0, "Our analytics").should("exist");
library().click();
H.entityPickerModalItem(1, "Metrics").should(
"have.attr",
"data-disabled",
"true",
);
H.entityPickerModalItem(1, "Data").click();
H.entityPickerModalItem(2, "Trusted Orders Model").should("exist");
});
});
});

View File

@@ -0,0 +1,414 @@
const { H } = cy;
import { createLibraryWithItems } from "e2e/support/test-library-data";
describe("scenarios > data studio > modeling > metrics", () => {
beforeEach(() => {
H.restore();
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
cy.intercept("POST", "/api/card").as("createCard");
cy.intercept("PUT", "/api/card/*").as("updateCard");
cy.intercept("POST", "/api/collection").as("createCollection");
cy.intercept("PUT", "/api/collection/*").as("updateCollection");
createLibraryWithItems();
});
describe("empty state", () => {
it("should show empty state when no collections are selected", () => {
visitModelingPage();
H.DataStudio.Modeling.emptyPage().should("be.visible");
H.DataStudio.Modeling.emptyPage()
.parent()
.findByText("Build your semantic layer with models and metrics.")
.should("be.visible");
});
});
it("should create a new metric with proper validation and save to collection", () => {
visitModelingPage();
cy.log("Create a new metric");
H.DataStudio.ModelingSidebar.createCardMenuButton().click();
H.popover().findByText("Metric").click();
H.DataStudio.Metrics.queryEditor().should("be.visible");
H.DataStudio.Metrics.saveButton().should("be.disabled");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.findByText("Databases").click();
cy.findByText("Orders").click();
});
H.getNotebookStep("summarize")
.findByText("Pick a column to group by")
.click();
H.popover().findByText("Created At").click();
H.DataStudio.Metrics.saveButton().should("be.enabled").click();
H.modal().within(() => {
cy.findByText("Save your metric").should("be.visible");
cy.findByLabelText("Name").type("Total Revenue");
cy.findByLabelText("Description").type(
"Sum of all order totals across the store",
);
cy.findByText("Where do you want to save this?").should("be.visible");
cy.button("Save").click();
});
cy.wait("@createCard");
cy.log("Verify metric overview page");
cy.url().should("match", /\/data-studio\/modeling\/metrics\/\d+$/);
H.DataStudio.Metrics.overviewPage().within(() => {
cy.findByText("Total Revenue").should("be.visible");
cy.findByText("Sum of all order totals across the store").should(
"be.visible",
);
cy.findByText("Sample Database").should("be.visible");
cy.findByText("PUBLIC").should("be.visible");
cy.findByText("Orders").should("be.visible");
cy.findByText("Creator and last editor").should("be.visible");
cy.findAllByText(/by Bobby Tables/).should("be.visible");
});
cy.log("Ensure chart is visible");
H.echartsContainer().findByText("Count").should("be.visible");
cy.log("Verify metric definition page");
H.DataStudio.Metrics.definitionTab().click();
cy.log("Verify notebook state");
H.DataStudio.Metrics.queryEditor().should("be.visible");
H.getNotebookStep("data").findByText("Orders").should("be.visible");
H.getNotebookStep("summarize")
.findByText("Created At: Month")
.should("be.visible");
H.runButtonInOverlay().click();
cy.log("Ensure chart is visible");
H.echartsContainer().findByText("Count").should("be.visible");
cy.log("Verify metric dependencies page");
H.DataStudio.Metrics.dependenciesTab().click();
H.DependencyGraph.graph().within(() => {
cy.findByText("Orders").should("be.visible");
cy.findByText("Total Revenue").should("be.visible");
});
});
it("should edit metric definition and save changes", function () {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Verify metric overview page displays correct data");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.should("be.visible");
cy.log("Update the metric name");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.clear()
.type("Updated Orders Metric{enter}");
cy.wait("@updateCard");
cy.log("Verify updated name appears in overview");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Updated Orders Metric")
.should("be.visible");
cy.log("Verify updated name appears in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.metricItem("Updated Orders Metric").should(
"be.visible",
);
});
it("should cancel editing and revert changes", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Navigate to definition tab");
H.DataStudio.Metrics.definitionTab().click();
cy.log("Change aggregation");
H.getNotebookStep("summarize").findByText("Count").click();
H.popover().findByText("Sum of ...").click();
H.popover().findByText("Total").click();
cy.log("Verify save button is enabled, then cancel");
H.DataStudio.Metrics.saveButton().should("be.enabled");
H.DataStudio.Metrics.cancelButton().click();
cy.log("Verify changes were reverted");
H.getNotebookStep("summarize").findByText("Count").should("be.visible");
});
it("should show unsaved changes warning when navigating away", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Navigate to definition tab");
H.DataStudio.Metrics.definitionTab().click();
cy.log("Change aggregation from Count to Sum");
H.DataStudio.Metrics.queryEditor().should("be.visible");
H.getNotebookStep("summarize").findByText("Count").click();
H.popover().findByText("Sum of ...").click();
H.popover().findByText("Total").click();
cy.log("Try to navigate away");
H.DataStudio.ModelingSidebar.glossaryLink().click();
cy.log("Verify unsaved changes modal appears");
H.modal().within(() => {
cy.findByText("Discard your changes?").should("be.visible");
cy.button("Cancel").click();
});
cy.log("Verify we're still on the definition tab");
H.DataStudio.Metrics.queryEditor().should("be.visible");
});
it("should archive and restore a metric", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Wait for collection page to load");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.log("Click on the metric from the collection view");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Verify metric is loaded before archiving");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.should("be.visible");
cy.log("Archive the metric");
H.DataStudio.Metrics.moreMenu().click();
H.popover().findByText("Move to trash").click();
cy.log("Confirm archiving in modal");
H.modal().button("Move to trash").click();
cy.wait("@updateCard");
cy.log("Verify redirected to modeling page");
cy.url().should("include", "/data-studio/modeling");
cy.log("Verify metric is removed from collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.collectionPage()
.findByText("No metrics yet")
.should("be.visible");
cy.log("Navigate to trash");
cy.visit("/trash");
cy.log("Verify metric appears in trash");
cy.findByRole("table")
.findByText("Trusted Orders Metric")
.should("be.visible");
cy.log("Restore the metric");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.findByTestId("archive-banner").should("be.visible");
cy.findByTestId("archive-banner").findByText("Restore").click();
cy.wait("@updateCard");
cy.log("Verify metric is restored in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table")
.findByText("Trusted Orders Metric")
.should("be.visible");
});
it("should view metric in question view via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Verify metric is loaded");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.should("be.visible");
cy.log("Verify View link opens in new tab");
H.DataStudio.Metrics.moreMenu().click();
H.popover()
.findByText("View")
.closest("a")
.should("have.attr", "target", "_blank")
.should("have.attr", "href")
.and("match", /\/metric\/\d+/);
});
it("should duplicate metric via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Verify metric is loaded");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.should("be.visible");
cy.log("Open more menu and click Duplicate");
H.DataStudio.Metrics.moreMenu().click();
H.popover().findByText("Duplicate").click();
cy.log("Save duplicate metric");
H.modal()
.findByText('Duplicate "Trusted Orders Metric"')
.should("be.visible");
H.modal()
.findByLabelText("Name")
.should("have.value", "Trusted Orders Metric - Duplicate");
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModal().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Library").click();
cy.findByText("Metrics").click();
cy.button("Select this collection").click();
});
H.modal().button("Duplicate").click();
cy.wait("@createCard");
H.modal().should("not.exist");
cy.log("Verify duplicate metric is created");
H.DataStudio.Metrics.overviewPage()
.findByText("Trusted Orders Metric - Duplicate")
.should("be.visible");
cy.log("Verify both metrics appear in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").within(() => {
cy.findByText("Trusted Orders Metric").should("be.visible");
cy.findByText("Trusted Orders Metric - Duplicate").should("be.visible");
});
});
it("should move metric to different collection via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Metrics collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
cy.log("Click on the metric from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Metric").click();
cy.log("Verify metric is loaded");
H.DataStudio.Metrics.overviewPage()
.findByDisplayValue("Trusted Orders Metric")
.should("be.visible");
cy.log("Open more menu and click Move");
H.DataStudio.Metrics.moreMenu().click();
H.popover().findByText("Move").click();
cy.log("Select First collection as destination");
H.entityPickerModal().findByText("First collection").click();
H.entityPickerModal().button("Move").click();
cy.wait("@updateCard");
cy.log("Verify metric is in First collection");
cy.findByTestId("move-card-toast").findByText("First collection").click();
cy.log("Verify metric is no longer in Metrics collection");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree()
.findByText("Metrics")
.click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.collectionPage()
.findByText("No metrics yet")
.should("be.visible");
});
});
function visitModelingPage() {
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.root().should("be.visible");
}

View File

@@ -0,0 +1,281 @@
const { H } = cy;
import { createLibraryWithItems } from "e2e/support/test-library-data";
describe("scenarios > data studio > modeling > models", () => {
beforeEach(() => {
H.restore();
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
cy.intercept("POST", "/api/card").as("createCard");
cy.intercept("PUT", "/api/card/*").as("updateCard");
cy.intercept("POST", "/api/collection").as("createCollection");
cy.intercept("PUT", "/api/collection/*").as("updateCollection");
cy.intercept("POST", "/api/dataset").as("dataset");
createLibraryWithItems();
});
it("should edit model name and description", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Click on the model from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify model overview page displays correct data");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Update the model name");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.clear()
.type("Updated Orders Model{enter}");
cy.wait("@updateCard");
cy.log("Verify updated name appears in overview");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Updated Orders Model")
.should("be.visible");
cy.log("Verify updated name appears in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.modelItem("Updated Orders Model").should(
"be.visible",
);
});
it("should navigate between tabs", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Click on the model from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify Overview tab displays model name");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Navigate to Definition tab");
H.DataStudio.Models.definitionTab().click();
H.DataStudio.Models.queryEditor().findByText("Orders").should("be.visible");
cy.log("Navigate to Fields tab");
H.DataStudio.Models.fieldsTab().click();
H.DataStudio.Models.fieldsPage().within(() => {
cy.findByText("ID").should("be.visible");
cy.findByText("User ID").should("be.visible");
cy.findByText("Product ID").should("be.visible");
});
cy.log("Navigate to Dependencies tab");
H.DataStudio.Models.dependenciesTab().click();
H.DependencyGraph.graph().within(() => {
cy.findByText("Orders").should("be.visible");
cy.findByText("Trusted Orders Model").should("be.visible");
});
cy.log("Navigate back to Overview tab");
H.DataStudio.Models.overviewTab().click();
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
});
it("should archive and restore a model", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Wait for collection page to load");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.log("Click on the model from the collection view");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify model is loaded before archiving");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Archive the model");
H.DataStudio.Models.moreMenu().click();
H.popover().findByText("Move to trash").click();
cy.log("Confirm archiving in modal");
H.modal().button("Move to trash").click();
cy.wait("@updateCard");
cy.log("Verify redirected to modeling page");
cy.url().should("include", "/data-studio/modeling");
cy.log("Verify model is removed from collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.collectionPage()
.findByText("No models yet")
.should("be.visible");
cy.log("Navigate to trash");
cy.visit("/trash");
cy.log("Verify model appears in trash");
cy.findByRole("table")
.findByText("Trusted Orders Model")
.should("be.visible");
cy.log("Restore the model");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.findByTestId("archive-banner").should("be.visible");
cy.findByTestId("archive-banner").findByText("Restore").click();
cy.wait("@updateCard");
cy.log("Verify model is restored in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table")
.findByText("Trusted Orders Model")
.should("be.visible");
});
it("should view model in question view via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Click on the model from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify model is loaded");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Verify View link opens in new tab");
H.DataStudio.Models.moreMenu().click();
H.popover()
.findByText("View")
.closest("a")
.should("have.attr", "target", "_blank")
.should("have.attr", "href")
.and("match", /\/model\/\d+/);
});
it("should duplicate model via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Click on the model from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify model is loaded");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Open more menu and click Duplicate");
H.DataStudio.Models.moreMenu().click();
H.popover().findByText("Duplicate").click();
cy.log("Save duplicate model");
H.modal()
.findByText('Duplicate "Trusted Orders Model"')
.should("be.visible");
H.modal()
.findByLabelText("Name")
.should("have.value", "Trusted Orders Model - Duplicate");
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModal().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Library").click();
cy.findByText("Data").click();
cy.button("Select this collection").click();
});
H.modal().button("Duplicate").click();
cy.wait("@createCard");
H.modal().should("not.exist");
cy.log("Verify duplicate model is created");
H.DataStudio.Models.overviewPage()
.findByText("Trusted Orders Model - Duplicate")
.should("be.visible");
cy.log("Verify both models appear in collection view");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").within(() => {
cy.findByText("Trusted Orders Model").should("be.visible");
cy.findByText("Trusted Orders Model - Duplicate").should("be.visible");
});
});
it("should move model to different collection via more menu", () => {
cy.log("Navigate to Data Studio Modeling");
cy.visit("/data-studio/modeling");
cy.log("Select Models collection from sidebar");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
cy.log("Click on the model from the collection view");
H.DataStudio.Modeling.collectionPage().should("be.visible");
cy.findByRole("table").findByText("Trusted Orders Model").click();
cy.log("Verify model is loaded");
H.DataStudio.Models.overviewPage()
.findByDisplayValue("Trusted Orders Model")
.should("be.visible");
cy.log("Open more menu and click Move");
H.DataStudio.Models.moreMenu().click();
H.popover().findByText("Move").click();
cy.log("Select First collection as destination");
H.entityPickerModal().findByText("First collection").click();
H.entityPickerModal().button("Move").click();
cy.wait("@updateCard");
cy.log("Verify model is in First collection");
cy.findByTestId("move-card-toast").findByText("First collection").click();
cy.log("Verify model is no longer in Models collection");
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
H.DataStudio.Modeling.collectionPage().should("be.visible");
H.DataStudio.Modeling.collectionPage()
.findByText("No models yet")
.should("be.visible");
});
});

View File

@@ -0,0 +1,301 @@
const { H } = cy;
describe("scenarios > data studio > snippets", () => {
beforeEach(() => {
H.restore();
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
cy.intercept("POST", "/api/native-query-snippet").as("createSnippet");
cy.intercept("PUT", "/api/native-query-snippet/*").as("updateSnippet");
});
describe("creation", () => {
it("should create a new snippet with proper validation", () => {
visitModelingPage();
H.DataStudio.ModelingSidebar.createSnippetButton().click();
H.popover().findByText("New snippet").click();
H.DataStudio.Snippets.newPage().should("be.visible");
H.DataStudio.Snippets.saveButton().should("be.disabled");
H.DataStudio.Snippets.editor.type("SELECT * FROM orders");
H.DataStudio.Snippets.saveButton().should("be.disabled");
H.DataStudio.Snippets.nameInput().type("Test snippet");
H.DataStudio.Snippets.saveButton().should("be.enabled");
H.DataStudio.Snippets.descriptionInput().type(
"This is a test snippet description",
);
H.DataStudio.Snippets.saveButton().click();
H.modal().within(() => {
cy.findByText("Select a folder for your snippet").should("be.visible");
cy.button("Select").click();
});
cy.wait("@createSnippet");
H.DataStudio.Snippets.editPage().should("be.visible");
H.DataStudio.Snippets.editPage().within(() => {
cy.findByText(/by Bobby Tables/).should("be.visible");
});
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.should("be.visible")
.closest("[role='menuitem']")
.should("have.attr", "data-selected", "true");
});
});
describe("editing", () => {
it("should be able to edit snippet content", () => {
H.createSnippet({
name: "Test snippet",
content: "SELECT * FROM orders",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("SQL snippets")
.click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.click();
H.DataStudio.Snippets.editPage().should("be.visible");
H.DataStudio.Snippets.editor.type(" WHERE id = 1");
H.DataStudio.Snippets.saveButton().should("be.enabled").click();
cy.wait("@updateSnippet");
cy.reload();
H.DataStudio.Snippets.editor
.get()
.should("contain.text", "SELECT * FROM orders WHERE id = 1");
});
it("should be able to cancel editing", () => {
H.createSnippet({
name: "Test snippet",
content: "SELECT * FROM orders",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("SQL snippets")
.click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.click();
H.DataStudio.Snippets.editor.type(" WHERE id = 1");
H.DataStudio.Snippets.cancelButton().click();
H.DataStudio.Snippets.editor
.get()
.should("not.contain.text", "SELECT * FROM orders WHERE id = 1");
H.DataStudio.Snippets.editor
.get()
.should("contain.text", "SELECT * FROM orders");
});
it("should show unsaved changes warning when navigating away", () => {
H.createSnippet({
name: "Test snippet",
content: "SELECT * FROM orders",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("SQL snippets")
.click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.click();
H.DataStudio.Snippets.editor.type(" WHERE id = 1");
H.DataStudio.ModelingSidebar.glossaryLink().click();
H.modal().within(() => {
cy.findByText("Discard your changes?").should("be.visible");
cy.button("Cancel").click();
});
H.DataStudio.Snippets.editPage().should("be.visible");
});
});
describe("description", () => {
it("should support markdown in description", () => {
H.createSnippet({
name: "Test snippet",
content: "SELECT * FROM orders",
description: "**Bold text** and *italic text*",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("SQL snippets")
.click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.click();
H.DataStudio.Snippets.editPage().within(() => {
cy.findByText("Bold text").should("have.css", "font-weight", "700");
cy.findByText("italic text").should("have.css", "font-style", "italic");
});
});
});
describe("deletion", () => {
it("should be able to delete a snippet", () => {
H.createSnippet({
name: "Test snippet",
content: "SELECT * FROM orders",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("SQL snippets")
.click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.click();
cy.findByTestId("snippet-header").findByRole("button").click();
H.popover().findByText("Delete").click();
H.modal().within(() => {
cy.findByText("Delete snippet?").should("be.visible");
cy.button("Delete").click();
cy.wait("@updateSnippet");
});
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test snippet")
.should("not.exist");
});
});
describe("snippet folders", () => {
beforeEach(() => {
cy.intercept("POST", "/api/collection").as("createCollection");
cy.intercept("PUT", "/api/collection/*").as("updateCollection");
cy.intercept("DELETE", "/api/collection/*").as("deleteCollection");
});
it("should be able to create a folder and snippet inside it", () => {
visitModelingPage();
H.DataStudio.ModelingSidebar.createSnippetButton().click();
H.popover().findByText("New folder").click();
H.modal().within(() => {
cy.findByLabelText("Give your folder a name").type("Test Folder");
cy.findByLabelText("Add a description").type(
"Folder for test snippets",
);
cy.button("Create").click();
cy.wait("@createCollection");
});
H.DataStudio.ModelingSidebar.snippetsTree()
.click()
.findByText("Test Folder")
.should("be.visible");
H.DataStudio.ModelingSidebar.createSnippetButton().click();
H.popover().findByText("New snippet").click();
H.DataStudio.Snippets.nameInput().type("Folder snippet");
H.DataStudio.Snippets.editor.type("SELECT 1");
H.DataStudio.Snippets.saveButton().click();
H.modal().within(() => {
cy.findByText("Test Folder").click();
cy.button("Select").click();
});
cy.wait("@createSnippet");
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Folder snippet")
.should("be.visible");
});
it("should be able to edit folder details", () => {
H.createSnippetFolder({
name: "Test Folder",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTreeItem("SQL snippets").click();
H.DataStudio.ModelingSidebar.snippetsTreeItem("Test Folder")
.findByLabelText("Collection options")
.click();
H.popover().findByText("Edit folder details").click();
H.modal().within(() => {
cy.findByLabelText("Give your folder a name")
.clear()
.type("Updated Folder");
cy.button("Update").click();
cy.wait("@updateCollection");
});
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Updated Folder")
.should("be.visible");
});
it("should be able to delete a folder", () => {
H.createSnippetFolder({
name: "Test Folder",
});
visitModelingPage();
H.DataStudio.ModelingSidebar.snippetsTreeItem("SQL snippets").click();
H.DataStudio.ModelingSidebar.snippetsTreeItem("Test Folder")
.findByLabelText("Collection options")
.click();
H.popover().findByText("Archive").click();
cy.wait("@updateCollection");
H.DataStudio.ModelingSidebar.snippetsTreeItem("SQL snippets").click();
H.DataStudio.ModelingSidebar.snippetsTree()
.findByText("Test Folder")
.should("not.exist");
});
});
});
function visitModelingPage() {
cy.visit("/data-studio/modeling");
H.DataStudio.ModelingSidebar.root().should("be.visible");
}

View File

@@ -190,11 +190,10 @@ describe("scenarios > dependencies > dependency checks", () => {
cy.log("make breaking changes");
cy.get<number>("@transformId").then(H.visitTransform);
cy.findByTestId("transform-page").findByText("Edit query").click();
H.NativeEditor.clear().type('SELECT name FROM "Schema A"."Animals"');
cy.log("cancel breaking changes");
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
H.modal().within(() => {
cy.findByText("Question with fields").should("be.visible");
cy.findByText("Question without fields").should("not.exist");
@@ -204,7 +203,7 @@ describe("scenarios > dependencies > dependency checks", () => {
cy.get("@updateTransform.all").should("have.length", 0);
cy.log("confirm breaking changes");
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
H.modal().within(() => {
cy.findByText("Question with fields").should("be.visible");
cy.findByText("Question without fields").should("not.exist");
@@ -217,11 +216,10 @@ describe("scenarios > dependencies > dependency checks", () => {
it("should not show a confirmation if there are no breaking changes when updating a SQL transform after it was run", () => {
createSqlTransformWithDependentMbqlQuestions();
cy.get<number>("@transformId").then(H.visitTransform);
cy.findByTestId("transform-page").findByText("Edit query").click();
H.NativeEditor.clear().type(
'SELECT score, name FROM "Schema A"."Animals"',
);
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
cy.wait("@updateTransform");
});
@@ -230,12 +228,11 @@ describe("scenarios > dependencies > dependency checks", () => {
cy.log("make breaking changes");
cy.get<number>("@transformId").then(H.visitTransform);
cy.findByTestId("transform-page").findByText("Edit query").click();
H.getNotebookStep("data").findByLabelText("Pick columns").click();
H.popover().findByLabelText("Score").click();
cy.log("cancel breaking changes");
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
H.modal().within(() => {
cy.findByText("Score transform").should("be.visible");
cy.findByText("Name transform").should("not.exist");
@@ -245,7 +242,7 @@ describe("scenarios > dependencies > dependency checks", () => {
cy.get("@updateTransform.all").should("have.length", 0);
cy.log("confirm breaking changes");
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
H.modal().within(() => {
cy.findByText("Score transform").should("be.visible");
cy.findByText("Name transform").should("not.exist");
@@ -257,10 +254,9 @@ describe("scenarios > dependencies > dependency checks", () => {
it("should not show a confirmation if there are no breaking changes when updating a MBQL transform before it was run", () => {
createMbqlTransformWithDependentSqlTransforms();
cy.get<number>("@transformId").then(H.visitTransform);
cy.findByTestId("transform-page").findByText("Edit query").click();
H.getNotebookStep("data").button("Sort").click();
H.popover().findByText("Score").click();
cy.findByTestId("transform-query-editor").button("Save changes").click();
H.DataStudio.Transforms.saveChangesButton().click();
cy.wait("@updateTransform");
});
});

View File

@@ -19,7 +19,7 @@ import type {
TransformId,
} from "metabase-types/api";
const BASE_URL = "/admin/tools/dependencies";
const BASE_URL = "/data-studio/dependencies";
const TABLE_NAME = "scoreboard_actions";
const TABLE_DISPLAY_NAME = "Scoreboard Actions";
const TABLE_ID_ALIAS = "tableId";
@@ -65,14 +65,14 @@ describe("scenarios > dependencies > dependency graph", () => {
isRecentItem: boolean;
}) {
cy.log(`verify that "${itemName}" can be found via search`);
graphEntrySearchInput().clear().type(itemName);
H.DependencyGraph.entrySearchInput().clear().type(itemName);
H.popover().findByText(itemName).click();
graphEntryButton().should("have.text", itemName);
graphEntryButton().icon(itemIcon).should("be.visible");
graphEntryButton().icon("close").click();
H.DependencyGraph.entryButton().should("have.text", itemName);
H.DependencyGraph.entryButton().icon(itemIcon).should("be.visible");
H.DependencyGraph.entryButton().icon("close").click();
if (isRecentItem) {
graphEntrySearchInput().click();
H.DependencyGraph.entrySearchInput().click();
H.popover().findByText(itemName).should("be.visible");
}
}
@@ -89,17 +89,17 @@ describe("scenarios > dependencies > dependency graph", () => {
itemIcon: IconName;
}) {
cy.log(`verify that "${itemName}" can be selected in the picker`);
graphEntrySearchInput().click();
H.DependencyGraph.entrySearchInput().click();
H.popover().findByText("Browse all").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tabName).click();
H.entityPickerModalItem(itemLevel, itemName).click();
});
graphEntryButton().should("have.text", itemName);
graphEntryButton().icon(itemIcon).should("be.visible");
H.DependencyGraph.entryButton().should("have.text", itemName);
H.DependencyGraph.entryButton().icon(itemIcon).should("be.visible");
cy.log(`verify that "${itemName}" is selected when the picker is opened`);
graphEntryButton().click();
H.DependencyGraph.entryButton().click();
H.entityPickerModal().within(() => {
H.entityPickerModalItem(itemLevel, itemName).should(
"have.attr",
@@ -108,10 +108,10 @@ describe("scenarios > dependencies > dependency graph", () => {
);
cy.findByLabelText("Close").click();
});
graphEntryButton().icon("close").click();
H.DependencyGraph.entryButton().icon("close").click();
cy.log(`verify that "${itemName}" can be found via search"`);
graphEntrySearchInput().click();
H.DependencyGraph.entrySearchInput().click();
H.popover().findByText("Browse all").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tabName).click();
@@ -119,9 +119,9 @@ describe("scenarios > dependencies > dependency graph", () => {
cy.findByText(/result for/).should("exist");
cy.findByText(itemName).click();
});
graphEntryButton().should("have.text", itemName);
graphEntryButton().icon(itemIcon).should("be.visible");
graphEntryButton().icon("close").click();
H.DependencyGraph.entryButton().should("have.text", itemName);
H.DependencyGraph.entryButton().icon(itemIcon).should("be.visible");
H.DependencyGraph.entryButton().icon("close").click();
}
it("should be able to use inline search for all supported entity types", () => {
@@ -153,7 +153,7 @@ describe("scenarios > dependencies > dependency graph", () => {
});
testEntitySearch({
itemName: TABLE_BASED_TRANSFORM_NAME,
itemIcon: "refresh_downstream",
itemIcon: "transform",
isRecentItem: false,
});
});
@@ -168,7 +168,7 @@ describe("scenarios > dependencies > dependency graph", () => {
testEntityPicker({
tabName: "Tables",
itemName: "Products",
itemLevel: 2,
itemLevel: 3,
itemIcon: "table",
});
testEntityPicker({
@@ -193,7 +193,7 @@ describe("scenarios > dependencies > dependency graph", () => {
tabName: "Transforms",
itemName: TABLE_BASED_TRANSFORM_NAME,
itemLevel: 0,
itemIcon: "refresh_downstream",
itemIcon: "transform",
});
});
});
@@ -206,9 +206,9 @@ describe("scenarios > dependencies > dependency graph", () => {
});
});
graphSelectionButton().click();
H.DependencyGraph.selectionButton().click();
H.popover().findByText(TABLE_DISPLAY_NAME).click();
dependencyGraph().within(() => {
H.DependencyGraph.graph().within(() => {
cy.findByLabelText(TABLE_DISPLAY_NAME).should("be.visible");
cy.findByLabelText(TABLE_BASED_QUESTION_NAME).should("be.visible");
});
@@ -225,17 +225,21 @@ describe("scenarios > dependencies > dependency graph", () => {
groupTitle: string;
dependentItemTitle: string;
}) {
dependencyGraph()
H.DependencyGraph.graph()
.findByLabelText(itemTitle)
.findByText(groupTitle)
.click();
graphDependencyPanel()
H.DependencyGraph.dependencyPanel()
.findByLabelText(dependentItemTitle)
.findByText(dependentItemTitle)
.click();
graphEntryButton().findByText(dependentItemTitle).should("be.visible");
H.DependencyGraph.entryButton()
.findByText(dependentItemTitle)
.should("be.visible");
cy.go("back");
graphEntryButton().findByText(itemTitle).should("be.visible");
H.DependencyGraph.entryButton()
.findByText(itemTitle)
.should("be.visible");
}
it("should display dependencies for a table and navigate to them", () => {
@@ -443,16 +447,16 @@ describe("scenarios > dependencies > dependency graph", () => {
visibleItems: string[];
hiddenItems: string[];
}) {
graphDependencyPanel().icon("filter").click();
H.DependencyGraph.dependencyPanel().icon("filter").click();
H.popover().findByText(filterName).click();
graphDependencyPanel().within(() => {
H.DependencyGraph.dependencyPanel().within(() => {
visibleItems.forEach((item) =>
cy.findByText(item).should("be.visible"),
);
hiddenItems.forEach((item) => cy.findByText(item).should("not.exist"));
});
H.popover().findByText(filterName).click();
graphDependencyPanel().icon("filter").click();
H.DependencyGraph.dependencyPanel().icon("filter").click();
}
it("should be able to filter questions", () => {
@@ -490,7 +494,7 @@ describe("scenarios > dependencies > dependency graph", () => {
});
visitGraphForEntity(tableId, "table");
});
dependencyGraph()
H.DependencyGraph.graph()
.findByLabelText(TABLE_DISPLAY_NAME)
.findByText("6 questions")
.click();
@@ -534,26 +538,6 @@ function visitGraphForEntity(id: DependencyId, type: DependencyType) {
return cy.visit(BASE_URL, { qs: { id, type } });
}
function dependencyGraph() {
return cy.findByTestId("dependency-graph");
}
function graphEntryButton() {
return cy.findByTestId("graph-entry-button");
}
function graphEntrySearchInput() {
return cy.findByTestId("graph-entry-search-input");
}
function graphSelectionButton() {
return cy.findByTestId("graph-selection-button");
}
function graphDependencyPanel() {
return cy.findByTestId("graph-dependency-panel");
}
function getScoreboardTableId() {
return cy.get<number>(`@${TABLE_ID_ALIAS}`);
}

View File

@@ -101,7 +101,7 @@ describe("documents", () => {
});
H.navigationSidebar()
.findByRole("tab", { name: "Bookmarks" })
.findByRole("section", { name: "Bookmarks" })
.findByText("Test Document")
.click();
@@ -927,9 +927,9 @@ describe("documents", () => {
H.commandSuggestionItem(/New Question/).click();
cy.log("Create a simple query in the notebook editor");
cy.findByTestId("entity-picker-modal").within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(2, "Orders").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders").click();
});
cy.log("Save and use the new question");
@@ -1099,10 +1099,10 @@ describe("documents", () => {
H.commandSuggestionItem(/New Question/).click();
cy.log("Create a time series query with Orders table");
cy.findByRole("dialog", { name: "Pick your starting data" }).should(
"be.visible",
);
H.entityPickerModalItem(2, "Orders").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders").click();
});
H.addSummaryField({ metric: "Sum of ...", field: "Total" });
H.addSummaryGroupingField({ field: "Created At" });
@@ -1217,7 +1217,7 @@ describe("documents", () => {
H.commandSuggestionItem(/New SQL query/).should("not.exist");
cy.log("Verify notebook modal opens automatically");
cy.findByRole("dialog", { name: "Pick your starting data" }).should(
cy.findByRole("dialog", { name: "Create new question" }).should(
"be.visible",
);
});

View File

@@ -37,7 +37,7 @@ describe("scenarios - embedding hub", () => {
"be.visible",
);
// Click on the first available table
cy.get("[data-testid='picker-item']").first().click();
H.entityPickerModalItem(3, "Accounts").click();
});
cy.log("Should navigate to auto dashboard creation");

View File

@@ -1892,6 +1892,14 @@ describe("scenarios > embedding > full app - jwt sso integration", () => {
H.updateSetting("jwt-shared-secret", jwtSecret);
H.updateSetting("jwt-enabled", true);
cy.intercept("POST", `/api/card/${ORDERS_QUESTION_ID}/query`).as(
"getCardQuery",
);
addLinkClickBehavior({
dashboardId,
linkTemplate: "/question/{{test_attribute}}",
});
cy.signOut(); // we *need* to sign out, otherwise the SSO process won't kick in
});
@@ -1937,6 +1945,43 @@ describe("scenarios > embedding > full app - jwt sso integration", () => {
cy.url().should("equal", `${baseUrl}/dashboard/${dashboardId}`);
H.main().findByText("Orders in a dashboard").should("be.visible");
});
it("should pass JWT user attributes to click behavior custom destinations (metabase#65942)", () => {
const jwtAttributeValue = ORDERS_QUESTION_ID;
// 1) set up a click behavior that uses a user attribute in the URL
// Added in beforeeach so it uses the admin user
// 2) sign a jwt for the user with a custom attribute
cy.task("signJwt", {
payload: {
email: USERS.normal.email,
exp: Math.round(Date.now() / 1000) + 10 * 60,
test_attribute: jwtAttributeValue,
},
secret: jwtSecret,
}).then((jwtToken) => {
// 3) mock the JWT provider to redirect to the auth/sso endpoint with the JWT
cy.intercept(/http:\/\/localhost:8888\/.*/, (req) => {
const redirectUrl = `${baseUrl}/auth/sso?jwt=${jwtToken}&return_to=/dashboard/${dashboardId}`;
req.redirect(redirectUrl);
}).as("jwt-provider");
});
// 4) visit the dashboard as embedded
H.visitFullAppEmbeddingUrl({ url: `/dashboard/${dashboardId}` });
cy.wait("@jwt-provider");
// 5) verify user is on dashboard
cy.url().should("equal", `${baseUrl}/dashboard/${dashboardId}`);
cy.findAllByRole("gridcell").first().click();
cy.wait("@getCardQuery");
cy.findByTestId("question-filter-header").realHover();
cy.findByTestId("main-logo").should("be.visible");
});
});
const visitQuestionUrl = (urlOptions) => {

View File

@@ -153,8 +153,7 @@ describe("issue 20683", { tags: "@external" }, () => {
it("should filter postgres with the 'current quarter' filter (metabase#20683)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -710,9 +709,8 @@ describe("issue 29094", () => {
it("disallows adding a filter using non-boolean custom expression (metabase#29094)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -997,8 +995,7 @@ describe("issue 45252", { tags: "@external" }, () => {
H.startNewQuestion();
cy.log("filter picker - new filter");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Writable Postgres12").click();
cy.findByText("Many Data Types").click();
});

View File

@@ -115,8 +115,8 @@ describe("scenarios > joins > custom expressions", () => {
H.openOrdersTable({ mode: "notebook" });
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
H.popover().within(() => {
@@ -151,8 +151,8 @@ describe("scenarios > joins > custom expressions", () => {
H.openNotebook();
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders Model").click();
});
H.popover().within(() => {
@@ -181,8 +181,8 @@ describe("scenarios > joins > custom expressions", () => {
H.openOrdersTable({ mode: "notebook" });
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
H.popover().within(() => {

View File

@@ -181,15 +181,14 @@ describe("issue 15342", { tags: "@external" }, () => {
it("should correctly order joins for MySQL queries (metabase#15342)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(MYSQL_DB_NAME).click();
cy.findByText("People").click();
});
cy.icon("join_left_outer").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(MYSQL_DB_NAME).click();
cy.findByText("Orders").click();
});
H.getNotebookStep("join").findByLabelText("Right column").click();
@@ -197,8 +196,8 @@ describe("issue 15342", { tags: "@external" }, () => {
// eslint-disable-next-line no-unsafe-element-filtering
cy.icon("join_left_outer").last().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(MYSQL_DB_NAME).click();
cy.findByText("Products").click();
});
H.getNotebookStep("join").icon("join_left_outer").click();
@@ -239,8 +238,8 @@ describe("issue 15578", () => {
H.openProductsTable({ mode: "notebook" });
cy.button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(JOINED_QUESTION_NAME).click();
});
@@ -271,8 +270,8 @@ describe("issue 17710", () => {
H.openOrdersTable({ mode: "notebook" });
cy.button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
@@ -321,8 +320,8 @@ describe("issue 17767", () => {
cy.findByText("Join data").click();
// Join "Previous results" with
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
@@ -354,8 +353,8 @@ describe("issue 17968", () => {
// eslint-disable-next-line no-unsafe-element-filtering
cy.findAllByTestId("action-buttons").last().button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.popover().findByText("Count").click();
@@ -481,8 +480,8 @@ describe("issue 18512", () => {
describe("issue 18589", () => {
function joinTable(table) {
cy.findByText("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText(table).click();
});
}
@@ -757,8 +756,8 @@ describe("issue 22859 - multiple levels of nesting", () => {
it("third level of nesting with joins should result in proper column aliasing (metabase#22859-2)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("22859-Q2").click();
});
@@ -976,8 +975,8 @@ describe("issue 29795", () => {
cy.icon("join_left_outer").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(NATIVE_QUESTION).click();
});
@@ -1123,8 +1122,8 @@ describe("issue 39448", () => {
it("should load joined table metadata for suggested join conditions (metabase#39448)", () => {
H.openOrdersTable({ mode: "notebook" });
cy.findByTestId("action-buttons").button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.getNotebookStep("join").within(() => {
@@ -1159,8 +1158,8 @@ describe("issue 27521", () => {
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -1186,8 +1185,8 @@ describe("issue 27521", () => {
cy.log("Create second question (Products + Q1)");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").click();
});
@@ -1196,8 +1195,8 @@ describe("issue 27521", () => {
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Q1").click();
});
@@ -1249,21 +1248,22 @@ describe("issue 42385", { tags: "@external" }, () => {
it("should remove invalid draft join clause when query database changes (metabase#42385)", () => {
H.openOrdersTable({ mode: "notebook" });
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
H.getNotebookStep("data").findByTestId("data-step-cell").click();
H.entityPickerModal().within(() => {
H.miniPickerHeader().click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Reviews").click();
});
H.getNotebookStep("join").within(() => {
cy.findByLabelText("Right table")
.findByText("Pick data…")
.should("be.visible");
cy.findByPlaceholderText("Search for tables and more...").should(
"be.visible",
);
cy.findByLabelText("Left column").should("not.exist");
cy.findByLabelText("Right column").should("not.exist");
});
@@ -1272,8 +1272,8 @@ describe("issue 42385", { tags: "@external" }, () => {
it("should remove invalid join clause in incomplete draft state when query database changes (metabase#42385)", () => {
H.openOrdersTable({ mode: "notebook" });
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
@@ -1282,10 +1282,11 @@ describe("issue 42385", { tags: "@external" }, () => {
.findByText("Products")
.click();
H.entityPickerModal().findByText("Reviews").click();
H.miniPicker().findByText("Reviews").click();
H.getNotebookStep("data").findByTestId("data-step-cell").click();
H.entityPickerModal().within(() => {
H.miniPickerHeader().click();
H.miniPicker().within(() => {
cy.findByText("QA Postgres12").click();
cy.findByText("Reviews").click();
});
@@ -1363,8 +1364,8 @@ describe("issue 46675", () => {
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.getNotebookStep("data").findByLabelText("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Reviews").click();
});
H.popover().findByText("ID").click();
@@ -1373,23 +1374,22 @@ describe("issue 46675", () => {
it("should reset the draft join state when the source table changes (metabase#46675)", () => {
cy.log("change the source table and verify that the state was reset");
H.getNotebookStep("data").findByText("Orders").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Products").click();
});
H.getNotebookStep("join").within(() => {
cy.findByLabelText("Left table").should("have.text", "Products");
cy.findByLabelText("Right table").should("have.text", "Pick data…");
cy.findByLabelText("Left column").should("not.exist");
cy.findByPlaceholderText("Search for tables and more...").should(
"be.visible",
);
});
cy.log("complete the join and make sure the query can be executed");
H.getNotebookStep("join")
.findByLabelText("Right table")
.findByText("Pick data…")
.findByPlaceholderText("Search for tables and more...")
.click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.visualize();
@@ -1402,8 +1402,7 @@ describe("issue 46675", () => {
.findByLabelText("Right table")
.findByText("Reviews")
.click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Orders").click();
});
H.getNotebookStep("join").within(() => {

View File

@@ -341,7 +341,7 @@ describe("scenarios > question > joined questions", () => {
);
H.getNotebookStep("data").findByTestId("data-step-cell").click();
H.entityPickerModal().findByText("People").click();
H.miniPicker().findByText("People").click();
H.getNotebookStep("join").should("not.exist");

View File

@@ -7,7 +7,7 @@ const { H } = cy;
const SOURCE_TABLE = "Animals";
// Helper functions for metabot transform interactions
const visitTransformListPage = () => cy.visit("/admin/transforms");
const visitTransformListPage = () => cy.visit("/data-studio/transforms");
const suggestions = () => cy.findAllByTestId("metabot-chat-suggestion");
// eslint-disable-next-line no-unsafe-element-filtering
@@ -49,8 +49,12 @@ const assertSuggestionInSidebar = (values: {
const assertEditorDiffState = (opts: { exists: boolean }) => {
const should = opts.exists ? "exist" : "not.exist";
cy.findByRole("button", { name: /apply|create/i }).should(should);
cy.findByRole("button", { name: /reject/i }).should(should);
H.DataStudio.Transforms.content()
.findByRole("button", { name: /apply|create/i })
.should(should);
H.DataStudio.Transforms.content()
.findByRole("button", { name: /reject/i })
.should(should);
};
const assertAcceptRejectUI = (opts: { visible: boolean }) => {
@@ -59,391 +63,397 @@ const assertAcceptRejectUI = (opts: { visible: boolean }) => {
rejectSuggestionBtn().should(should);
};
describe("scenarios > metabot > transforms codegen", () => {
beforeEach(() => {
H.restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: "many_schemas" });
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: SOURCE_TABLE });
describe(
"scenarios > metabot > transforms codegen",
{ viewportWidth: 1600, viewportHeight: 1200 },
() => {
beforeEach(() => {
H.restore("postgres-writable");
H.resetTestTable({ type: "postgres", table: "many_schemas" });
H.resetSnowplow();
cy.signInAsAdmin();
H.activateToken("bleeding-edge");
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: SOURCE_TABLE });
cy.intercept("POST", "/api/ee/metabot-v3/agent-streaming").as("agentReq");
cy.intercept("POST", "/api/ee/transform").as("createTransform");
cy.intercept("PUT", "/api/ee/transform/*").as("updateTransform");
});
cy.intercept("POST", "/api/ee/metabot-v3/agent-streaming").as("agentReq");
cy.intercept("POST", "/api/ee/transform").as("createTransform");
cy.intercept("PUT", "/api/ee/transform/*").as("updateTransform");
});
describe("Native SQL transform tests", () => {
describe("create new transform", () => {
it("should create SQL transform via metabot", () => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
cy.log("Ask metabot for a new transform");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"I'll create a new transform that gets the number 1 for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 1"),
),
});
H.sendMetabotMessage(
"Create a new native SQL transform that gives me the number 1",
);
assertSuggestionInSidebar({ newSourcePartial: "SELECT 1" });
cy.log("Should be able to visit and the see new transform");
viewLastSuggestion();
cy.url().should("include", "/admin/transforms/new/native");
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
assertEditorContent("native", "SELECT 1");
// User should ask metabot for a change
cy.log("Ask metabot for an edit");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 2"),
),
});
H.sendMetabotMessage("Make this give me the number 2 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 1",
newSourcePartial: "SELECT 2",
});
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("native", "SELECT 2");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit("native", "SELECT 3");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 4"),
),
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 3",
newSourcePartial: "SELECT 4",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("native", "SELECT 3");
assertAcceptRejectUI({ visible: false });
});
it("should create Python transform via metabot", () => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
cy.log("Ask metabot for a new transform");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"I'll create a new transform that gets the number 1 for you.",
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [1]})",
),
),
});
H.sendMetabotMessage(
"Create a new native python transform that gives me the number 1",
);
assertSuggestionInSidebar({
newSourcePartial: "pd.DataFrame({'value': [1]})",
});
cy.log("Should be able to visit and the see new transform");
viewLastSuggestion();
cy.url().should("include", "/admin/transforms/new/python");
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
assertEditorContent("python", "pd.DataFrame({'value': [1]})");
// User should ask metabot for a change
cy.log("Ask metabot for an edit");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [2]})",
),
),
});
H.sendMetabotMessage("Make this give me the number 2 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [1]})",
newSourcePartial: "pd.DataFrame({'value': [2]})",
});
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [2]})");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit(
"python",
dedent`
import pandas as pd
def transform(metabase_table_df):
return pd.DataFrame({'value': [3]})`,
);
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [4]})",
),
),
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [3]})",
newSourcePartial: "pd.DataFrame({'value': [4]})",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [3]})");
assertAcceptRejectUI({ visible: false });
});
it("should create SQL transform with model reference via metabot and run successfully", () => {
// Create a model first
H.getTableId({ name: SOURCE_TABLE, databaseId: WRITABLE_DB_ID }).as(
"tableId",
);
cy.get("@tableId").then((tableId) => {
H.createQuestion({
name: "Test Model",
type: "model",
query: {
"source-table": tableId,
limit: 5,
},
}).as("model");
});
cy.get("@model").then(({ body: model }) => {
describe("Native SQL transform tests", () => {
describe("create new transform", () => {
it("should create SQL transform via metabot", () => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
cy.log("Ask metabot for a new transform that references the model");
const modelTagName = `#${model.id}-test-model`;
const queryWithModelRef = `SELECT * FROM {{${modelTagName}}}`;
cy.log("Ask metabot for a new transform");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"I'll create a transform that queries your model.",
createMockNativeTransformJSON(
"I'll create a new transform that gets the number 1 for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 1"),
),
});
H.sendMetabotMessage(
"Create a new native SQL transform that gives me the number 1",
);
assertSuggestionInSidebar({ newSourcePartial: "SELECT 1" });
cy.log("Should be able to visit and the see new transform");
viewLastSuggestion();
cy.url().should("include", "/data-studio/transforms/new/native");
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
assertEditorContent("native", "SELECT 1");
// User should ask metabot for a change
cy.log("Ask metabot for an edit");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 2"),
),
});
H.sendMetabotMessage("Make this give me the number 2 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 1",
newSourcePartial: "SELECT 2",
});
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("native", "SELECT 2");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit("native", "SELECT 3");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockNativeTransformJSON(null, WRITABLE_DB_ID, "SELECT 4"),
),
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 3",
newSourcePartial: "SELECT 4",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("native", "SELECT 3");
assertAcceptRejectUI({ visible: false });
});
it("should create Python transform via metabot", () => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
cy.log("Ask metabot for a new transform");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"I'll create a new transform that gets the number 1 for you.",
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
queryWithModelRef,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [1]})",
),
),
});
H.sendMetabotMessage(
"Create a transform that queries the Test Model",
"Create a new native python transform that gives me the number 1",
);
assertSuggestionInSidebar({ newSourcePartial: "SELECT * FROM" });
assertSuggestionInSidebar({
newSourcePartial: "pd.DataFrame({'value': [1]})",
});
cy.log("Should be able to visit the new transform");
cy.log("Should be able to visit and the see new transform");
viewLastSuggestion();
cy.url().should("include", "/admin/transforms/new/native");
assertEditorContent("native", "SELECT * FROM");
cy.url().should("include", "/data-studio/transforms/new/python");
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
assertEditorContent("python", "pd.DataFrame({'value': [1]})");
cy.log(
"Should be able to run the transform successfully (verifies template tags were parsed)",
);
cy.findByTestId("native-query-editor-container").icon("play").click();
cy.findByTestId("query-visualization-root").should("be.visible");
});
});
});
describe("update existing transform", () => {
it("should update existing SQL transform via metabot", () => {
H.createSqlTransform({
sourceQuery: "SELECT 1",
targetTable: "table_a",
targetSchema: "Schema A",
}).as("transformId");
visitTransformListPage();
H.openMetabotViaSearchButton(true);
// Ask metabot for a change to existing transform
cy.get("@transformId").then((transformId) => {
// User should ask metabot for a change
cy.log("Ask metabot for an edit");
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockNativeTransformJSON(
Number(transformId),
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
"SELECT 2",
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [2]})",
),
),
});
});
H.sendMetabotMessage(
"Update my SQL transform to select 2 instead of 1.",
);
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 1",
newSourcePartial: "SELECT 2",
});
H.sendMetabotMessage("Make this give me the number 2 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [1]})",
newSourcePartial: "pd.DataFrame({'value': [2]})",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to visit and the see the updated transform");
viewLastSuggestion();
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [2]})");
assertAcceptRejectUI({ visible: false });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("native", "SELECT 2");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit(
"python",
dedent`
import pandas as pd
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit("native", "SELECT 3");
cy.get("@transformId").then((transformId) => {
def transform(metabase_table_df):
return pd.DataFrame({'value': [3]})`,
);
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockNativeTransformJSON(
Number(transformId),
createMockPythonTransformJSON(
null,
WRITABLE_DB_ID,
"SELECT 4",
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(metabase_table_df):\\n return pd.DataFrame({'value': [4]})",
),
),
});
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 3",
newSourcePartial: "SELECT 4",
});
assertAcceptRejectUI({ visible: true });
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [3]})",
newSourcePartial: "pd.DataFrame({'value': [4]})",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("native", "SELECT 3");
assertAcceptRejectUI({ visible: false });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [3]})");
assertAcceptRejectUI({ visible: false });
});
it("should create SQL transform with model reference via metabot and run successfully", () => {
// Create a model first
H.getTableId({ name: SOURCE_TABLE, databaseId: WRITABLE_DB_ID }).as(
"tableId",
);
cy.get("@tableId").then((tableId) => {
H.createQuestion({
name: "Test Model",
type: "model",
query: {
"source-table": tableId,
limit: 5,
},
}).as("model");
});
cy.get("@model").then(({ body: model }) => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
cy.log("Ask metabot for a new transform that references the model");
const modelTagName = `#${model.id}-test-model`;
const queryWithModelRef = `SELECT * FROM {{${modelTagName}}}`;
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"I'll create a transform that queries your model.",
createMockNativeTransformJSON(
null,
WRITABLE_DB_ID,
queryWithModelRef,
),
),
});
H.sendMetabotMessage(
"Create a transform that queries the Test Model",
);
assertSuggestionInSidebar({ newSourcePartial: "SELECT * FROM" });
cy.log("Should be able to visit the new transform");
viewLastSuggestion();
cy.url().should("include", "/data-studio/transforms/new/native");
assertEditorContent("native", "SELECT * FROM");
cy.log(
"Should be able to run the transform successfully (verifies template tags were parsed)",
);
cy.findByTestId("native-query-editor-container")
.icon("play")
.click();
cy.findByTestId("query-visualization-root").should("be.visible");
});
});
});
it("should update existing Python transform via metabot", () => {
H.getTableId({ name: SOURCE_TABLE, databaseId: WRITABLE_DB_ID }).then(
(tableId) => {
H.createPythonTransform({
targetTable: "transform_table",
targetSchema: "Schema A",
body: dedent`
describe("update existing transform", () => {
it("should update existing SQL transform via metabot", () => {
H.createSqlTransform({
sourceQuery: "SELECT 1",
targetTable: "table_a",
targetSchema: "Schema A",
}).as("transformId");
visitTransformListPage();
H.openMetabotViaSearchButton(true);
// Ask metabot for a change to existing transform
cy.get("@transformId").then((transformId) => {
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockNativeTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
"SELECT 2",
),
),
});
});
H.sendMetabotMessage(
"Update my SQL transform to select 2 instead of 1.",
);
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 1",
newSourcePartial: "SELECT 2",
});
cy.log("Should be able to visit and the see the updated transform");
viewLastSuggestion();
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("native", "SELECT 2");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit("native", "SELECT 3");
cy.get("@transformId").then((transformId) => {
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockNativeTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
"SELECT 4",
),
),
});
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "SELECT 3",
newSourcePartial: "SELECT 4",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("native", "SELECT 3");
assertAcceptRejectUI({ visible: false });
});
it("should update existing Python transform via metabot", () => {
H.getTableId({ name: SOURCE_TABLE, databaseId: WRITABLE_DB_ID }).then(
(tableId) => {
H.createPythonTransform({
targetTable: "transform_table",
targetSchema: "Schema A",
body: dedent`
import pandas as pd
def transform(foo):
return pd.DataFrame({'value': [1]})
`,
sourceTables: { foo: tableId },
}).then((transformId) => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
sourceTables: { foo: tableId },
}).then((transformId) => {
visitTransformListPage();
H.openMetabotViaSearchButton(true);
// Ask metabot for a change to existing transform
cy.get("@transformId").then((transformId) => {
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockPythonTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
{ foo: tableId },
"import pandas as pd\\n\\ndef transform(foo):\\n return pd.DataFrame({'value': [2]})",
// Ask metabot for a change to existing transform
cy.get("@transformId").then((transformId) => {
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that update for you.",
createMockPythonTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
{ foo: tableId },
"import pandas as pd\\n\\ndef transform(foo):\\n return pd.DataFrame({'value': [2]})",
),
),
),
});
});
H.sendMetabotMessage(
"Update my SQL transform to select 2 instead of 1.",
);
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [1]})",
newSourcePartial: "pd.DataFrame({'value': [2]})",
});
});
H.sendMetabotMessage(
"Update my SQL transform to select 2 instead of 1.",
);
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [1]})",
newSourcePartial: "pd.DataFrame({'value': [2]})",
});
cy.log(
"Should be able to visit and the see the updated transform",
);
viewLastSuggestion();
assertAcceptRejectUI({ visible: true });
cy.log(
"Should be able to visit and the see the updated transform",
);
viewLastSuggestion();
assertAcceptRejectUI({ visible: true });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [2]})");
assertAcceptRejectUI({ visible: false });
// User can accept a change (editor should have new value)
cy.log("Should be able to select an edit");
acceptSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [2]})");
assertAcceptRejectUI({ visible: false });
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit(
"python",
dedent`
// User should make changes to the source
cy.log("Should diff with user edits");
makeManualEdit(
"python",
dedent`
import pandas as pd
def transform(foo):
return pd.DataFrame({'value': [3]})`,
);
);
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockPythonTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(foo):\\n return pd.DataFrame({'value': [4]})",
H.mockMetabotResponse({
body: createMockTransformSuggestionResponse(
"Let me make that change for you.",
createMockPythonTransformJSON(
Number(transformId),
WRITABLE_DB_ID,
{ metabase_table_df: 152 },
"import pandas as pd\\n\\ndef transform(foo):\\n return pd.DataFrame({'value': [4]})",
),
),
),
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [3]})",
newSourcePartial: "pd.DataFrame({'value': [4]})",
});
assertAcceptRejectUI({ visible: true });
});
H.sendMetabotMessage("Make this give me the number 4 instead");
assertSuggestionInSidebar({
oldSourcePartial: "pd.DataFrame({'value': [3]})",
newSourcePartial: "pd.DataFrame({'value': [4]})",
});
assertAcceptRejectUI({ visible: true });
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [3]})");
assertAcceptRejectUI({ visible: false });
});
},
);
cy.log("Should be able to reject an edit");
rejectSuggestion();
assertEditorContent("python", "pd.DataFrame({'value': [3]})");
assertAcceptRejectUI({ visible: false });
});
},
);
});
});
});
});
});
},
);
const createMockNativeTransformJSON = (
id: number | null,

View File

@@ -135,7 +135,10 @@ describe("scenarios > browse > metrics", () => {
cy.findByTestId("browse-metrics-header")
.findByLabelText("Create a new metric")
.click();
cy.findByTestId("entity-picker-modal").findByText("People").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").click();
});
cy.findByTestId("edit-bar")
.should("contain", "New metric")
.button("Save")

View File

@@ -119,8 +119,8 @@ describe("scenarios > metrics > editing", () => {
.findByLabelText("Create a new metric")
.click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
saveMetric({ name: "New metric" });
@@ -137,8 +137,8 @@ describe("scenarios > metrics > editing", () => {
it("should not crash when cancelling creation of a new metric (metabase#48024)", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cancelMetricEditing();
@@ -159,8 +159,8 @@ describe("scenarios > metrics > editing", () => {
describe("data source", () => {
it("should create a metric based on a table", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
addStringCategoryFilter({
@@ -174,8 +174,8 @@ describe("scenarios > metrics > editing", () => {
it("should create a metric based on a saved question", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders").click();
});
addStringCategoryFilter({
@@ -190,8 +190,8 @@ describe("scenarios > metrics > editing", () => {
it("should create a metric based on a multi-stage saved question", () => {
H.createQuestion(ORDERS_MULTI_STAGE_QUESTION);
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(ORDERS_MULTI_STAGE_QUESTION.name).click();
});
addNumberBetweenFilter({
@@ -205,8 +205,8 @@ describe("scenarios > metrics > editing", () => {
it("should create a metric based on a model", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders Model").click();
});
addStringCategoryFilter({
@@ -221,8 +221,8 @@ describe("scenarios > metrics > editing", () => {
it("should create a metric based on a multi-stage model", () => {
H.createQuestion({ ...ORDERS_MULTI_STAGE_QUESTION, type: "model" });
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(ORDERS_MULTI_STAGE_QUESTION.name).click();
});
addNumberBetweenFilter({
@@ -236,8 +236,8 @@ describe("scenarios > metrics > editing", () => {
it("should not allow to create a multi-stage metric", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders Model").click();
});
getActionButton("Summarize").should("not.exist");
@@ -245,8 +245,8 @@ describe("scenarios > metrics > editing", () => {
it("should allow to run the query from the metric empty state", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cy.intercept("POST", "/api/dataset").as("dataset");
@@ -259,13 +259,13 @@ describe("scenarios > metrics > editing", () => {
describe("joins", () => {
it("should join a table", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
startNewJoin();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewFilter();
@@ -282,22 +282,31 @@ describe("scenarios > metrics > editing", () => {
it("should not be possible to join a metric", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewJoin();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders").should("be.visible");
cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
cy.findByText("Our analytics").click(); // go back
});
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").should("be.visible");
H.entityPickerModalTab("Metrics").should("not.exist");
H.entityPickerModalTab("Data").click();
cy.findByText("Orders").should("be.visible");
// FIXME: metabase#66210
// cy.findByText(ORDERS_SCALAR_METRIC.name).should("not.exist");
});
});
it("should be possible to join data on the first stage of a metric-based query", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
});
H.getNotebookStep("data").within(() => {
@@ -310,8 +319,8 @@ describe("scenarios > metrics > editing", () => {
describe("custom columns", () => {
it("should be able to use custom columns in metric queries (metabase#42360)", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewCustomColumn();
@@ -331,8 +340,8 @@ describe("scenarios > metrics > editing", () => {
it("should be able to use implicitly joinable columns in custom columns in metric queries (metabase#42360)", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewCustomColumn();
@@ -354,8 +363,8 @@ describe("scenarios > metrics > editing", () => {
describe("breakouts", () => {
it("should create a timeseries metric", () => {
H.startNewMetric();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.getNotebookStep("summarize").findByText("Count").click();
@@ -377,8 +386,8 @@ describe("scenarios > metrics > editing", () => {
H.createQuestion(ORDERS_SCALAR_METRIC);
H.startNewMetric();
cy.intercept("POST", "/api/dataset/query_metadata").as("queryMetadata");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(ORDERS_SCALAR_METRIC.name).click();
});
cy.wait("@queryMetadata");
@@ -427,8 +436,8 @@ describe("scenarios > metrics > editing", () => {
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.createQuestion(PRODUCTS_SCALAR_METRIC);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewAggregation();
@@ -449,8 +458,8 @@ describe("scenarios > metrics > editing", () => {
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.createQuestion(PRODUCTS_SCALAR_METRIC);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewAggregation();
@@ -467,8 +476,8 @@ describe("scenarios > metrics > editing", () => {
it("should show the description for metrics", () => {
H.createQuestion(ORDERS_SCALAR_FILTER_METRIC);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
startNewAggregation();

View File

@@ -213,8 +213,8 @@ describe("issue 30574", () => {
cy.log("create the first metric");
H.main().findByText("Create metric").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cy.findByTestId("edit-bar").button("Save").click();
@@ -228,8 +228,8 @@ describe("issue 30574", () => {
H.openNavigationSidebar();
H.navigationSidebar().findByText("Metrics").click();
H.main().findByLabelText("Create a new metric").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.getNotebookStep("summarize").findByText("Count").click();

View File

@@ -51,7 +51,8 @@ describe("scenarios > models > create", () => {
cy.findByTestId("new-model-options")
.findByText("Use the notebook editor")
.click();
H.entityPickerModal().findByText("People").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("People").click();
cy.findByTestId("dataset-edit-bar").button("Save").click();
cy.findByTestId("save-question-modal")
.should("contain", "Save model")

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