Compare commits

...

629 Commits

Author SHA1 Message Date
Brad Anderson
92b4c699e7 UXW-2279 Changes Data Studio's Glossary page background color to background-secondary 2025-11-25 09:21:38 -05:00
Jakub Chodorowicz
c4f830074f Revert "fix: scroll position jump when entering field sort mode gdgt-1269"
This reverts commit bfecdec661.
2025-11-25 12:57:23 +01:00
Jakub Chodorowicz
bfecdec661 fix: scroll position jump when entering field sort mode gdgt-1269 2025-11-25 11:56:55 +01:00
Jakub Chodorowicz
8cdba689d2 ui: tweak search results checkbox spacing to match tree results 2025-11-25 11:00:56 +01:00
Jakub Chodorowicz
84c2b8374a fix: cannot scroll search results 2025-11-25 10:59:55 +01:00
Nathan Voxland
84a6016781 Fixing in-place support for collections 2025-11-25 00:35:22 -06:00
Ryan Laurie
36c3c77fa6 fix flaky join test 2025-11-24 18:34:02 -07:00
Aleksandr Lesnenko
a469e93bb6 update mini picker specs after a fix 2025-11-24 20:17:15 -05:00
Aleksandr Lesnenko
364ae147e6 UXW-2372 2025-11-24 20:13:22 -05:00
Ryan Laurie
283bc8e108 filtering fixes for transforms 2025-11-24 17:41:28 -07:00
Ryan Laurie
0779b24d00 fix embedding data picker clicks 2025-11-24 17:41:06 -07:00
Brad Anderson
12bfb92194 Fix e2e test from group-14 2025-11-24 16:44:43 -05:00
Aleksandr Lesnenko
012a639d83 this element is not visible because you can't even open incompatible sample database db 2025-11-24 16:40:05 -05:00
Nathan Voxland
b6ec6e858d Fixed merge issues 2025-11-24 14:58:21 -06:00
Nick Fitzpatrick
f434a3b5aa Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-24 16:40:21 -04:00
Aleksandr Lesnenko
567618aceb UXW-2378 2025-11-24 15:38:58 -05:00
Nick Fitzpatrick
3f1dfd226a resolving more merge sadness 2025-11-24 16:38:22 -04:00
Aleksandr Lesnenko
00e7937b52 UXW-2367 2025-11-24 15:29:32 -05:00
Aleksandr Lesnenko
cacdf1322d fix dark mode 2025-11-24 15:29:32 -05:00
Nick Fitzpatrick
f2533fb48e happy types 2025-11-24 16:26:29 -04:00
Brad Anderson
76c27e6be0 Fix issue where confirmation modal closes then reopens again then closes again when you archive a dashboard 2025-11-24 15:15:15 -05:00
Ryan Laurie
54bfe2df5b fix hidden items some more 2025-11-24 11:06:21 -07:00
Nick Fitzpatrick
4cdaa1af9e create data studio plugin file 2025-11-24 13:34:23 -04:00
Nick Fitzpatrick
37f6092efe FileUploadStatus unit test, quesiton reproductions 2025-11-24 12:09:58 -04:00
Brad Anderson
76f4cbd3af Fix remaining e2e tests from group-32 2025-11-24 10:57:02 -05:00
Nathan Voxland
a1b29a297d Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-24 09:43:39 -06:00
Nathan Voxland
d0ca291b45 Fixed failing test 2025-11-24 09:43:30 -06:00
Jakub Chodorowicz
42d380cdd5 fix: refactor: publish collection link tests 2025-11-24 13:07:43 +01:00
Jakub Chodorowicz
45fd51a855 fix: published info not updated in the tree picker after deleting model 2025-11-24 10:51:51 +01:00
Nathan Voxland
dd39b05088 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-24 01:40:58 -06:00
Nathan Voxland
e5c7403b5c Fixed failing test 2025-11-24 01:40:25 -06:00
Kamil Mielnik
342d36d0c6 Translate label 2025-11-24 14:23:56 +07:00
Nathan Voxland
3f572e6b94 Fixed failing test 2025-11-24 00:49:34 -06:00
Nathan Voxland
93e3ca13a0 Trying CI with search_index.verified 2025-11-21 21:15:58 -06:00
Ryan Laurie
3fa7413b35 fix embedding column picking 2025-11-21 16:07:46 -07:00
Anderson
7460ab0940 Merge branch 'master' into workbench2 2025-11-21 20:07:05 -03:00
Nemanja Glumac
4beb5cca15 Fetch uberjar using a proper action (#66165) 2025-11-21 20:05:52 -03:00
dpsutton
adcfb1222e Prevent driver tests (#66156)
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
2025-11-21 20:05:52 -03:00
Denis Berezin
716d7d2bdb Fix timeseries value detection (#66161) 2025-11-21 20:05:52 -03:00
Nemanja Glumac
29340508d7 Clean up backend-test matrix (#66158) 2025-11-21 20:05:52 -03:00
John Swanson
35b580d74b Fix flaky Remote Sync test (#65862) 2025-11-21 20:05:52 -03:00
Anderson
3766730259 Quick fix in CollectionPicker/utils.ts 2025-11-21 20:05:52 -03:00
Nathan Voxland
1c47368211 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-21 16:58:42 -06:00
Nathan Voxland
a9d1140b1a Reapply "Added collection argument to search"
This reverts commit f28d9d5af9.
2025-11-21 16:53:14 -06:00
Aleksandr Lesnenko
69de5d7b2a revert fix 2025-11-21 17:44:28 -05:00
Nathan Voxland
21cd7ae941 Fixed failing test 2025-11-21 15:24:28 -06:00
Nathan Voxland
e5e68ca9c2 Lint fixes 2025-11-21 15:11:16 -06:00
Brad Anderson
5bb40b9b35 Fix most e2e tests from group-32 2025-11-21 15:59:42 -05:00
Aleksandr Lesnenko
74ad50b451 snapshot 2025-11-21 15:56:02 -05:00
Nathan Voxland
cae26d16d4 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-21 14:50:45 -06:00
Nathan Voxland
ed2e9ea0b6 Fixed :here in library API 2025-11-21 14:50:37 -06:00
Nick Fitzpatrick
3a4edea410 UXW-2358 publishing outside of the data studio 2025-11-21 16:41:06 -04:00
Nathan Voxland
ec755156b0 Fixed :here in library API 2025-11-21 14:27:41 -06:00
Nathan Voxland
b9b05a31b3 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-21 14:26:27 -06:00
Nathan Voxland
6c100d3d91 Fixed :here in library API 2025-11-21 14:26:18 -06:00
Aleksandr Lesnenko
ea203da09e fix minipicker shows items it should not show 2025-11-21 15:07:44 -05:00
Ryan Laurie
526e45a63b additional filtering test 2025-11-21 12:48:51 -07:00
Ryan Laurie
8d6639115f test recents and backparsing 2025-11-21 12:48:51 -07:00
Ryan Laurie
e1dccbf743 add recent views to mini picker 2025-11-21 12:48:51 -07:00
Metabase Automation
1fba5c95b1 Update Loki Snapshots 2025-11-21 19:24:16 +00:00
Aleksandr Lesnenko
69f5e949a6 Merge branch 'master' into workbench2 2025-11-21 14:07:08 -05:00
Ryan Laurie
fcc871f760 fix item filtering 2025-11-21 12:00:20 -07:00
Anderson
69fc1330c7 Fix remaining issue with e2e group 37 2025-11-21 15:53:45 -03:00
Nick Fitzpatrick
8048a10e81 unit test adjustments 2025-11-21 14:43:49 -04:00
Brad Anderson
042986670f Starts fixing e2e tests from group-32 2025-11-21 12:01:49 -05:00
Brad Anderson
8ff79dcdcb Fix straggler from oss group 2025-11-21 12:01:49 -05:00
Brad Anderson
76315411c3 Fix e2e test from group-26 2025-11-21 12:01:49 -05:00
Ryan Laurie
9ddfabe818 properly disable invalid items in MiniPicker 2025-11-21 09:57:00 -07:00
Ryan Laurie
70299d9646 fix allowing cross DB joins 2025-11-21 09:57:00 -07:00
Brad Anderson
2416c7985e Fix e2e test from group-25 2025-11-21 11:09:20 -05:00
Brad Anderson
db4ae27e1a Fix e2e test from group-22 2025-11-21 11:08:37 -05:00
Jakub Chodorowicz
678d02ea98 test: adjust publishing test 2025-11-21 17:06:50 +01:00
Aleksandr Lesnenko
6cf5078bdb bring back modeling empty state 2025-11-21 11:04:51 -05:00
Stas Gavrylov
4edf71026d 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
2025-11-21 17:48:20 +02:00
Kamil Mielnik
fb8817222f Fix flaky tests 2025-11-21 22:19:47 +07:00
Brad Anderson
aab2552107 Fix e2e tests from group-9 2025-11-21 10:09:29 -05:00
Kamil Mielnik
14a8527136 Fix e2e test 2025-11-21 22:05:04 +07:00
Jakub Chodorowicz
7752bee446 fix: test: datamodel toasts and errors 2025-11-21 15:53:33 +01:00
Kamil Mielnik
692fd60f63 Fix e2e test 2025-11-21 21:48:57 +07:00
Jakub Chodorowicz
2e61a1446b test: fix: filter unused table 2025-11-21 15:34:59 +01:00
Kamil Mielnik
3b4b235cc4 Fix Source input disabled when filtering by Metabase transform
- Closes GDGT-1259
2025-11-21 21:05:32 +07:00
Jakub Chodorowicz
7b86886d46 fix: test: selection in table picker 2025-11-21 13:34:24 +01:00
Jakub Chodorowicz
8bccb26278 test: fix search tables e2e test 2025-11-21 13:09:52 +01:00
Jakub Chodorowicz
50569ee4c6 test: unskip table metadata transform test 2025-11-21 12:48:04 +01:00
Kamil Mielnik
380b944284 Activate token after restore 2025-11-21 03:08:20 -08:00
Dan Stone
eb62d57555 Remove old comments 2025-11-21 10:59:02 +00:00
Dan Stone
38008218af Use actual anti join in GET /table query 2025-11-21 10:57:31 +00:00
Sloan Sparger
0a5b39049f fix bug / bad type in SchemaFormSelect (#65995) 2025-11-20 22:58:42 -05:00
Aleksandr Lesnenko
e796112439 specs 2025-11-20 22:55:43 -05:00
Aleksandr Lesnenko
4893ef8c5c specs 2025-11-20 22:42:25 -05:00
Aleksandr Lesnenko
8848219d99 specs 2025-11-20 22:28:57 -05:00
Aleksandr Lesnenko
d00d877c08 revert util 2025-11-20 21:41:05 -05:00
Aleksandr Lesnenko
f3bff106a0 ci 2025-11-20 21:21:32 -05:00
Nathan Voxland
1899ee9a35 Fixed accidental #p 2025-11-20 18:42:07 -06:00
Nathan Voxland
f861aa583a Fixed unused import 2025-11-20 18:35:36 -06:00
Aleksandr Lesnenko
9b9a4a6242 fix 2025-11-20 19:31:20 -05:00
Aleksandr Lesnenko
86c84566d0 fix import 2025-11-20 19:21:43 -05:00
Ryan Laurie
d2dab60c23 Merge branch 'uxw-2271-minipicker-library' into workbench2 2025-11-20 17:12:07 -07:00
Aleksandr Lesnenko
1b6225d8b9 Merge remote-tracking branch 'origin/master' into workbench2 2025-11-20 19:09:43 -05:00
Ryan Laurie
9c9faa0ac7 fix lots of unit tests 2025-11-20 17:03:51 -07:00
Ryan Laurie
0dc4aebe0c cleanup data studio plugin 2025-11-20 15:41:51 -07:00
Ryan Laurie
4553f2da22 fix library collection bugs 2025-11-20 15:35:42 -07:00
Aleksandr Lesnenko
e76536256a ci, fixes 2025-11-20 17:32:35 -05:00
Nathan Voxland
35b129cf56 Fixing tests 2025-11-20 16:13:28 -06:00
Ryan Laurie
18f5f86209 mini picker unit tests 2025-11-20 14:57:50 -07:00
Ryan Laurie
bc5cb1c325 fix embedding types 2025-11-20 14:57:50 -07:00
Nathan Voxland
1cd7b283f5 Avoid race condition on list-table-filtering-test 2025-11-20 15:26:03 -06:00
Nick Fitzpatrick
86001f2203 re-enable the recents tab on the data picker modal 2025-11-20 17:18:13 -04:00
Ryan Laurie
8c4fac5813 merge fixes 2025-11-20 13:27:30 -07:00
Ryan Laurie
a6039ccbcd Merge branch 'workbench2' into uxw-2271-minipicker-library
# Conflicts:
#	enterprise/frontend/src/metabase-enterprise/api/tags.ts
#	enterprise/frontend/src/metabase-enterprise/data-studio/index.ts
#	enterprise/frontend/src/metabase-enterprise/data-studio/utils.ts
#	enterprise/frontend/src/metabase-enterprise/library/index.ts
#	frontend/src/metabase/lib/icon.ts
#	frontend/src/metabase/plugins/index.ts
2025-11-20 13:02:42 -07:00
Aleksandr Lesnenko
be34af6a95 Merge remote-tracking branch 'origin/master' into workbench2 2025-11-20 15:02:14 -05:00
Brad Anderson
c3af2d357a Fix e2e test from group-42 2025-11-20 14:43:19 -05:00
Nathan Voxland
3b8f854d75 Fixed merge 2025-11-20 13:21:52 -06:00
Nick Fitzpatrick
21111813dd table picker fixes 2025-11-20 15:18:22 -04:00
Brad Anderson
6abb3957ca Fix some e2e tests from group-32 2025-11-20 14:02:45 -05:00
Nathan Voxland
c23e771219 Merge remote-tracking branch 'origin/master' into workbench2
# Conflicts:
#	resources/migrations/058_update_migrations.yaml
2025-11-20 11:55:17 -06:00
Nick Fitzpatrick
6379de4077 scoped search is back 2025-11-20 13:49:21 -04:00
Aleksandr Lesnenko
1bb3fb5a47 hack fix ci while i'm on meetings 2025-11-20 12:02:21 -05:00
Brad Anderson
bb57238a40 Fix e2e tests from group-15 2025-11-20 11:22:27 -05:00
Aleksandr Lesnenko
fd44a4a4ab specs 2025-11-20 10:55:20 -05:00
Uladzimir Havenchyk
3a7d694450 remove background from not active table in search 2025-11-20 18:41:08 +03:00
Brad Anderson
8c7ef1cb29 Starts fixing e2e tests from mongo group 2025-11-20 10:16:49 -05:00
Jakub Chodorowicz
13b6c5954f test: use default external db, skip transform test 2025-11-20 16:12:55 +01:00
Brad Anderson
2d423ab9eb Updates some e2e tests from oss group, removes some now-invalid tests 2025-11-20 10:00:39 -05:00
Nathan Voxland
5a776c67db Removed unused import 2025-11-20 08:10:57 -06:00
Uladzimir Havenchyk
511c8f4edc add check icon for published tables 2025-11-20 17:02:24 +03:00
Kamil Mielnik
0795705b5d Add missing token 2025-11-20 20:18:35 +07:00
Uladzimir Havenchyk
2cc624a9fb fix browse nav section test 2025-11-20 16:03:38 +03:00
Kamil Mielnik
eb4af32f81 Update e2e test 2025-11-20 20:02:46 +07:00
Kamil Mielnik
7d75cdd2a9 Update e2e test 2025-11-20 19:57:45 +07:00
Kamil Mielnik
a19a1792c3 Update e2e test 2025-11-20 19:56:10 +07:00
Jakub Chodorowicz
f1768f2c2f test: refactor: cypress table tests 2025-11-20 12:20:18 +01:00
Jakub Chodorowicz
ce6ac1e6c0 fix: test: failure due to repeated mocking of same route 2025-11-20 11:13:41 +01:00
Dan Stone
9140001a09 Remove ^:parallel from bulk edit tests
They can contend and cause deadlocks
2025-11-20 10:04:03 +00:00
Jakub Chodorowicz
1b075b6a17 fix: test: wrong mock in oss sync table schema 2025-11-20 10:41:22 +01:00
Jakub Chodorowicz
4742b245a3 fix: test: table rescan and discard mocks are wrong 2025-11-20 10:30:02 +01:00
Kamil Mielnik
010fc0c706 Make bulk edit inputs non-clearable
- This prevents sending "empty" requests when clearing values
- And clearing does not really make sense here
2025-11-20 16:16:59 +07:00
Kamil Mielnik
a69e9dff99 Merge branch 'master' into workbench2 2025-11-20 14:34:53 +07:00
Ngoc Khuat
b90fe0c82f tidy things up 2025-11-20 14:32:31 +09:00
Aleksandr Lesnenko
99d21c6245 change backend library flag validation to data-studio 2025-11-19 22:15:55 -05:00
Aleksandr Lesnenko
8f1fc35b57 merge library and data studio 2025-11-19 21:37:21 -05:00
Nathan Voxland
8df3bf8590 Added library/tree API 2025-11-19 20:32:43 -05:00
Aleksandr Lesnenko
54ca922f81 hide our analytics in the data studio 2025-11-19 20:03:19 -05:00
Ryan Laurie
35d51cc0aa wait to focus 2025-11-19 16:49:05 -07:00
Ryan Laurie
61e4b45a05 cleanup 2025-11-19 16:39:42 -07:00
Brad Anderson
2b9a76c68e Starts fixing e2e tests from oss group 2025-11-19 17:30:17 -05:00
Anderson
790879964d Fix e2e tests from group 37 (except one depending on Recents tab) 2025-11-19 19:24:15 -03:00
Nathan Voxland
7313895370 Fixing tests 2025-11-19 15:45:24 -06:00
Brad Anderson
01174fe314 Fix e2e tests from group-34 2025-11-19 16:41:22 -05:00
Brad Anderson
9684013374 Minor cleanup 2025-11-19 16:41:22 -05:00
Brad Anderson
d51f1cb8b6 Fix e2e tests from group-33 2025-11-19 16:41:22 -05:00
Nick Fitzpatrick
3ffc52dfa5 disable scoped search on the 'database collection', models.cy.spec should be passing now 2025-11-19 17:39:18 -04:00
Nathan Voxland
c53e1b1caa Fixing tests 2025-11-19 15:22:13 -06:00
Anderson
4abbcc05ba Fix some e2e tests from groups 40 and 42 2025-11-19 18:02:43 -03:00
Nathan Voxland
d74456fa8a Added library/tree API 2025-11-19 14:54:27 -06:00
Ryan Laurie
1de902db18 keyboard navigation 2025-11-19 13:28:07 -07:00
Nathan Voxland
728a23310b Fixed module config 2025-11-19 14:27:23 -06:00
Anderson
c5098ee299 Fix a couple more (something between groups 40 and 43) 2025-11-19 17:20:29 -03:00
Nick Fitzpatrick
6168e37bd2 segments editor, hasEnabledNestedQueries 2025-11-19 16:15:33 -04:00
Nick Fitzpatrick
2d67edd722 constuct initial paths from library collections 2025-11-19 16:15:33 -04:00
Brad Anderson
b35540ea65 Fix e2e tests from group-30 2025-11-19 14:53:00 -05:00
Brad Anderson
b3e02d7b35 Fix e2e tests from group-28 2025-11-19 14:53:00 -05:00
Brad Anderson
09f229e248 Fix e2e tests from group-27 2025-11-19 14:53:00 -05:00
Brad Anderson
5e8b08aea0 Fix e2e tests from group-26 2025-11-19 14:53:00 -05:00
Brad Anderson
1797f19eb4 Fix e2e tests from group-24 2025-11-19 14:53:00 -05:00
Brad Anderson
8e2706d20a Fix e2e tests from group-23 2025-11-19 14:53:00 -05:00
Brad Anderson
528c637866 Fix e2e tests from group-20 2025-11-19 14:53:00 -05:00
Uladzimir Havenchyk
a9317a3dab more tests 2025-11-19 22:11:31 +03:00
Anderson
feb1d1d1ad Fix e2e tests from group 41 / joins-repros 2025-11-19 15:48:04 -03:00
Anderson
fd85264a58 Merge branch 'workbench2' into uxw-2271-minipicker-library 2025-11-19 15:17:39 -03:00
Uladzimir Havenchyk
042424d082 add tests for filtering 2025-11-19 21:08:16 +03:00
Nathan Voxland
8f75a93d32 Fixed OSS loading of tables 2025-11-19 11:51:06 -06:00
Nathan Voxland
e879b29173 Fixed OSS loading of tables 2025-11-19 11:50:13 -06:00
Nathan Voxland
c4057c58b6 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-19 10:52:28 -06:00
Nathan Voxland
f28d9d5af9 Revert "Added collection argument to search"
This reverts commits:
- 21999e01
- b0f71c03ea
- 4fa0c3b422
2025-11-19 10:50:23 -06:00
Dan Stone
162045ca28 Fix table metadata for test 2025-11-19 16:24:32 +00:00
Dan Stone
2507d4a74e Move bulk-edit-test to ee 2025-11-19 16:21:12 +00:00
Dan Stone
0579457b07 Fix list filtering now default is bronze 2025-11-19 16:18:54 +00:00
Dan Stone
8b38056c2b Move edit test to ee 2025-11-19 16:18:44 +00:00
Nick Fitzpatrick
135377c55a default collections for metrics and models outside of data studio 2025-11-19 12:17:24 -04:00
Dan Stone
6e2cb29d39 Kondo the kondo config 2025-11-19 15:59:41 +00:00
Brad Anderson
edc9329042 Merge remote-tracking branch 'origin/workbench2' into uxw-2271-minipicker-library
# Conflicts:
#	enterprise/frontend/src/metabase-enterprise/data-studio/data-model/components/TablePicker/components/PublishModelsModal.tsx
2025-11-19 10:56:41 -05:00
Jakub Chodorowicz
b85c85aae2 test: restore default db as the first step 2025-11-19 16:47:39 +01:00
Brad Anderson
005e07ca7e Fix e2e tests from group-23 2025-11-19 10:33:35 -05:00
Brad Anderson
8b54aeb677 Fix e2e tests from group-22 2025-11-19 10:27:53 -05:00
Brad Anderson
8f804ee84e Fix e2e tests from group-20 2025-11-19 10:14:55 -05:00
Jakub Chodorowicz
34fa895152 test: refactor test setup action order 2025-11-19 16:11:46 +01:00
Jakub Chodorowicz
f75e99d852 fix: fix missing dependencies in useEffect gdgt-1245 2025-11-19 15:59:18 +01:00
Brad Anderson
87ddcd9cb6 Fix e2e tests from group-19 2025-11-19 09:57:39 -05:00
Jakub Chodorowicz
0291de7cd4 fox: reset bulk form values when selection changes 2025-11-19 15:39:27 +01:00
Nick Fitzpatrick
e13a07e9ce default table publish collection 2025-11-19 10:04:09 -04:00
Uladzimir Havenchyk
beaecf8593 add search tests and support shift click in search 2025-11-19 16:50:30 +03:00
Nick Fitzpatrick
2420f26c27 don't call getCollectionItems on database collection 2025-11-19 09:30:13 -04:00
Kamil Mielnik
752b2a4c14 Fill content 2025-11-19 19:55:12 +07:00
Kamil Mielnik
43ee6826fc Improve icon 2025-11-19 19:52:54 +07:00
Kamil Mielnik
5c181eec70 Fix Tree skeleton resizing multiple times during loading
- Closes GDGT-1253
2025-11-19 19:51:41 +07:00
Nick Fitzpatrick
701ea75b06 data picker using proper library endpoint. Fixing types I broke. Endless Shame 2025-11-19 08:47:34 -04:00
Uladzimir Havenchyk
c6c36b777c use helper 2025-11-19 15:45:10 +03:00
Uladzimir Havenchyk
dc8190f6d2 add select/deselect tests 2025-11-19 15:42:28 +03:00
Kamil Mielnik
f94d3b160c Remove redundant cy.reload 2025-11-19 19:41:01 +07:00
Kamil Mielnik
eb469813d9 Remove redundant cy.reload 2025-11-19 19:37:44 +07:00
Kamil Mielnik
6565756360 Fix global H.DataModel.visit overrides 2025-11-19 19:33:31 +07:00
Kamil Mielnik
c76e082b02 Make new bulk table APIs ee-only (fe)
- Closes GDGT-1251
2025-11-19 18:18:03 +07:00
Jakub Chodorowicz
dc63b94eff test: update new ee endpoints paths 2025-11-19 11:31:59 +01:00
Kamil Mielnik
78c8a0265a Update test 2025-11-19 17:30:05 +07:00
Kamil Mielnik
d53ff6e6a0 Copy undos tests 2025-11-19 17:14:27 +07:00
Kamil Mielnik
e0b8bd2acc Copy error handling tests 2025-11-19 17:12:51 +07:00
Kamil Mielnik
a359db80df Copy preview section tests 2025-11-19 17:11:07 +07:00
Uladzimir Havenchyk
b4474e719d fix more unit tests 2025-11-19 13:08:00 +03:00
Jakub Chodorowicz
bcf0c0065c test: bulk attribute actions for tables, schemas and dbs 2025-11-19 11:01:27 +01:00
Kamil Mielnik
fa92970adc Make sidebar sticky 2025-11-19 16:56:39 +07:00
Ngoc Khuat
a14451ba73 Move bulk editing apis to enterprise (#65968) 2025-11-19 18:16:11 +09:00
Uladzimir Havenchyk
c3a90bcf36 Merge branch 'master' into workbench2 2025-11-19 11:51:07 +03:00
Jakub Chodorowicz
ba719e9cd0 fix: publish property not validated in the tree view after table publishing 2025-11-19 09:47:11 +01:00
Uladzimir Havenchyk
8d515e92e3 rewrite unit tests and fix max depth problem 2025-11-19 10:59:42 +03:00
Uladzimir Havenchyk
9d3be84fae fix infinite re-render in tree 2025-11-19 10:59:42 +03:00
Nathan Voxland
8a913474d6 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-19 00:58:54 -06:00
Nathan Voxland
4fa0c3b422 Fixing tests 2025-11-19 00:42:16 -06:00
Aleksandr Lesnenko
d883a13568 show library in permissions 2025-11-18 21:37:12 -05:00
Aleksandr Lesnenko
70c6b519bb fetch full tree to have library collections visible 2025-11-18 21:37:12 -05:00
Aleksandr Lesnenko
818c44a1cd Revert "Revert "Fixed null type filtering""
This reverts commit 25e0d78408.
2025-11-18 21:37:12 -05:00
Aleksandr Lesnenko
72a63f66da Revert "Revert "Don't return library collections by default from APIs""
This reverts commit 814c49473b.
2025-11-18 21:37:12 -05:00
Ryan Laurie
2d5adffd78 Remove Browsing Memory Feature 2025-11-18 19:19:11 -07:00
Aleksandr Lesnenko
cc02de6a3a correct collection types 2025-11-18 21:07:22 -05:00
Aleksandr Lesnenko
8401bce4c4 more UXW-2315, UXW-2314 2025-11-18 19:27:43 -05:00
Aleksandr Lesnenko
627bbbb1b7 more UXW-2315, UXW-2314 2025-11-18 19:27:43 -05:00
Aleksandr Lesnenko
c36649a144 UXW-2294 2025-11-18 19:27:43 -05:00
Aleksandr Lesnenko
0548a4da87 UXW-2315, UXW-2314 2025-11-18 19:27:43 -05:00
Jakub Chodorowicz
e263f1737f tests: add bulk publishing scenario
fix wrong collection link in case of root collection
2025-11-19 00:29:47 +01:00
Brad Anderson
39f0ee723d Fix e2e tests from group-18 2025-11-18 17:26:00 -05:00
Brad Anderson
b35d28c281 Fix e2e tests from group-17 2025-11-18 17:26:00 -05:00
Anderson
4b35d5afee Fix a couple tests from group 42 2025-11-18 19:23:29 -03:00
Brad Anderson
12d44596b4 Fix e2e tests from group-16 2025-11-18 17:00:17 -05:00
Ryan Laurie
3118bf04f7 library collection search 2025-11-18 14:49:20 -07:00
Ryan Laurie
583c6590af do enterprise code correctly 2025-11-18 14:49:20 -07:00
Brad Anderson
64eb5336d3 Fix e2e tests from group-14 2025-11-18 16:47:27 -05:00
Brad Anderson
beb106808c Updates some more e2e tests from group-9 (some actual failures left) 2025-11-18 16:31:03 -05:00
Brad Anderson
fcbd4ad3e7 Fix group-6 straggler, some more of group-8 2025-11-18 15:46:56 -05:00
Brad Anderson
9ce326e5cf Potentially disastrous change (increases item-picker-level of SchemaList and TableList) to fix test in group-8 2025-11-18 14:54:33 -05:00
Brad Anderson
905afa2968 Fix group-3 straggler, some of group-8 2025-11-18 14:53:10 -05:00
Anderson
e3d3455a19 Fix e2e tests from groups 44 and 45 2025-11-18 16:32:18 -03:00
Anderson
3ce176b363 Fix e2e tests from groups 46 and 47 2025-11-18 16:19:32 -03:00
Anderson
914c5cddd0 Fix a few e2e tests from oss group 2025-11-18 16:07:19 -03:00
Nathan Voxland
3da6139783 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-18 12:18:06 -06:00
Nathan Voxland
06827b7c33 Fixed module config 2025-11-18 12:17:55 -06:00
Brad Anderson
ed528b774f Merge remote-tracking branch 'origin/workbench2' into uxw-2271-minipicker-library 2025-11-18 13:04:17 -05:00
Ryan Laurie
5d6fd986f1 fix column selection 2025-11-18 10:44:25 -07:00
Jakub Chodorowicz
7d2d042837 test: add bulk sync action e2e test 2025-11-18 18:36:18 +01:00
Brad Anderson
6accc082bf Fix some e2e tests from group-9 2025-11-18 12:27:24 -05:00
Nathan Voxland
aca2799153 Merge remote-tracking branch 'origin/master' into workbench2 2025-11-18 11:15:52 -06:00
Nathan Voxland
814c49473b Revert "Don't return library collections by default from APIs"
This reverts commit 2ec5edfb1b.
2025-11-18 10:58:27 -06:00
Nathan Voxland
25e0d78408 Revert "Fixed null type filtering"
This reverts commit 5e311d78d4.
2025-11-18 10:58:26 -06:00
Brad Anderson
2e12638d28 Fix e2e tests from group-10 2025-11-18 11:15:12 -05:00
Brad Anderson
9997588d3e Fix e2e tests from group-6 and group-7 2025-11-18 11:15:12 -05:00
Jakub Chodorowicz
b21e5da510 test: add data studio table transform e2e test 2025-11-18 16:55:57 +01:00
Nick Fitzpatrick
a830ebbf36 types are happy 2025-11-18 11:52:48 -04:00
Anderson
2408bfb5e7 Merge branch 'workbench2' into uxw-2271-minipicker-library 2025-11-18 12:10:21 -03:00
Anderson
31c5636350 Fix dashboard.cy.spec 2025-11-18 12:08:52 -03:00
Kamil Mielnik
352419559b Copy field section tests 2025-11-18 21:38:37 +07:00
Kamil Mielnik
0fefbe02e1 Cleanup killed api 2025-11-18 21:38:37 +07:00
Ngoc Khuat
a0d78a2826 default data layer to bronze 2025-11-18 23:25:35 +09:00
Kamil Mielnik
6b5ea7220b Copy table section tests 2025-11-18 21:01:44 +07:00
Kamil Mielnik
2287799774 Copy data loading tests 2025-11-18 20:22:05 +07:00
Uladzimir Havenchyk
306d62eb1e fix typecheck 2025-11-18 13:56:16 +03:00
Kamil Mielnik
99f8c03ca6 Update unit test 2025-11-18 17:08:47 +07:00
Uladzimir Havenchyk
68c02c5278 add unit tests 2025-11-18 13:06:12 +03:00
Kamil Mielnik
9494b5dced Remove Mantine Table leftovers 2025-11-18 16:18:39 +07:00
Kamil Mielnik
c4157319ae Make icons use currentColor 2025-11-18 16:18:08 +07:00
Kamil Mielnik
6765a205a8 Remove unused Mantine Table 2025-11-18 16:16:25 +07:00
Kamil Mielnik
967b2dec8e Pluralize 2025-11-18 16:14:32 +07:00
Kamil Mielnik
e533d5d67b Consistent (although new) paddings in admin panel 2025-11-18 16:12:52 +07:00
Kamil Mielnik
aab525fda4 Sort 2025-11-18 16:06:49 +07:00
Kamil Mielnik
ef2fdb4f87 Remove cast 2025-11-18 16:06:17 +07:00
Kamil Mielnik
2c9fe22dfe Remove unnecessary whitespace 2025-11-18 15:53:03 +07:00
Kamil Mielnik
f4524f4f01 Fix pluralization 2025-11-18 15:52:03 +07:00
Kamil Mielnik
dc76695d0d Remove unused component 2025-11-18 15:32:07 +07:00
Nathan Voxland
6cb477cd2a Fixed null type filtering 2025-11-17 21:29:21 -06:00
Nathan Voxland
e494ee1f8e Don't return library collections by default from APIs 2025-11-17 21:29:21 -06:00
Nathan Voxland
b18b6d676c Fixed module config 2025-11-17 21:27:42 -06:00
Nathan Voxland
b5046f7745 Merge remote-tracking branch 'origin/master' into workbench2
# Conflicts:
#	.clj-kondo/config/modules/config.edn
#	test/metabase/warehouse_schema_rest/api/table_test.clj
2025-11-17 21:22:51 -06:00
Aleksandr Lesnenko
ff8b3352b7 UXW-2295: only admins can access data studio for now 2025-11-17 21:50:00 -05:00
Aleksandr Lesnenko
d5ecc533fe specs on the modeling section of the data studio 2025-11-17 21:25:07 -05:00
Nathan Voxland
5e311d78d4 Fixed null type filtering 2025-11-17 18:32:12 -06:00
Ryan Laurie
31b081dc7e drill into library when only half has content 2025-11-17 17:16:38 -07:00
Ryan Laurie
d2dedc0702 better alignment 2025-11-17 16:50:48 -07:00
Ryan Laurie
32ef181154 make search state nicer 2025-11-17 16:36:59 -07:00
Ryan Laurie
644eabcfe4 don't show single schema 2025-11-17 16:36:59 -07:00
Nathan Voxland
2ec5edfb1b Don't return library collections by default from APIs 2025-11-17 17:16:45 -06:00
Brad Anderson
aafeba9544 Fix e2e tests from group-4 and group-5 2025-11-17 16:07:10 -05:00
Brad Anderson
dba6fe5808 Fix e2e tests from group-2 2025-11-17 15:18:45 -05:00
Brad Anderson
cb193dc2ae Fix e2e tests from group-1 and group-2 2025-11-17 14:20:02 -05:00
Nathan Voxland
b0f71c03ea lint fixes 2025-11-17 13:15:13 -06:00
Nathan Voxland
3a5fe4339a Finished merge from master and fixed code issue 2025-11-17 13:06:04 -06:00
Nathan Voxland
1e26430772 Merge remote-tracking branch 'origin/master' into workbench2
# Conflicts:
#	resources/migrations/058_update_migrations.yaml
2025-11-17 12:56:54 -06:00
Nathan Voxland
75167a125d Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-17 12:55:14 -06:00
Nathan Voxland
21999e0156 Added collection argument to search 2025-11-17 12:53:34 -06:00
Ryan Laurie
fc6ec01fb4 i'm so sorry nick (#65779)
nick - I can't believe my name is attached to this now
2025-11-17 14:07:44 -04:00
Jakub Chodorowicz
8b7792f7f7 test: single table edit and data display 2025-11-17 18:43:04 +01:00
Jakub Chodorowicz
0715e6877b test: add e2e data studio single table tests
metadata information and publish model flow
2025-11-17 17:12:58 +01:00
Ryan Laurie
4460b65e26 implement virtualization 2025-11-17 08:48:03 -07:00
Ryan Laurie
eb008a63f7 fix infinte spin 2025-11-17 08:48:03 -07:00
Ryan Laurie
8d676bebd4 use correct endpoint 2025-11-17 08:48:03 -07:00
Ryan Laurie
905270ad6e handle library 2025-11-17 08:48:03 -07:00
Ryan Laurie
b919085277 vanilla minipicker
make things better

pretty solid
2025-11-17 08:48:03 -07:00
Ngoc Khuat
f58d0ea87b test fixes 2025-11-18 00:39:02 +09:00
Jakub Chodorowicz
3368696328 fix: collection picker not showing after initial publish info modal acknowledged 2025-11-17 16:30:51 +01:00
Ngoc Khuat
908ddb5a76 fixed can't edit source and add tests 2025-11-18 00:28:49 +09:00
Kamil Mielnik
bff6043361 Refactor 2025-11-17 21:41:37 +07:00
Kamil Mielnik
9787dc33bf Refactor 2025-11-17 21:41:37 +07:00
Uladzimir Havenchyk
b7e1f0fbbe Merge remote-tracking branch 'origin/master' into workbench2 2025-11-17 17:25:02 +03:00
Kamil Mielnik
0f02e68901 Refactor 2025-11-17 20:11:44 +07:00
Kamil Mielnik
2da9deb077 Fix page object override 2025-11-17 19:41:26 +07:00
Jakub Chodorowicz
dc410f1754 refactor: use url factory function instead of hardcoded url 2025-11-17 11:27:56 +01:00
Jakub Chodorowicz
b7f1725579 refactor: naming of table section components 2025-11-17 11:08:37 +01:00
Uladzimir Havenchyk
29122a9d19 fix broken tests 2025-11-17 11:06:34 +03:00
Ngoc Khuat
0221887631 Fix having to update table datasource to metabase-transform (#65840) 2025-11-17 16:21:41 +09:00
Aleksandr Lesnenko
3caa0c3ee7 UXW-2250 Don't leave the data studio when viewing a published model 2025-11-14 20:56:27 -05:00
Aleksandr Lesnenko
f369233998 unit specs 2025-11-14 20:34:46 -05:00
Aleksandr Lesnenko
ca1b05af35 misc fixes, snippets tests 2025-11-14 20:34:46 -05:00
Aleksandr Lesnenko
4083ef4bf3 helpers 2025-11-14 20:34:46 -05:00
Alexander Polyankin
0d2883785c Fix width 2025-11-14 18:50:09 -05:00
Alexander Polyankin
6ed99ac363 Actual fix for tabs 2025-11-14 16:21:42 -05:00
Alexander Polyankin
9d4f79815d Actual fix for tabs 2025-11-14 16:15:27 -05:00
Alexander Polyankin
2df7fc960e Actual fix for tabs 2025-11-14 16:11:08 -05:00
Alexander Polyankin
ef069e67f1 Actual fix for tabs 2025-11-14 16:08:11 -05:00
Alexander Polyankin
56a4c43415 Do not overwrite sample db in tests 2025-11-14 16:03:49 -05:00
Alexander Polyankin
90ab235b7d Do not overwrite sample db in tests 2025-11-14 16:00:51 -05:00
Alexander Polyankin
665c22c1b2 Do not overwrite sample db in tests 2025-11-14 15:58:28 -05:00
Alexander Polyankin
63a852d94e Fix tests 2025-11-14 15:31:22 -05:00
Alexander Polyankin
ecf9534212 Fix tests 2025-11-14 15:27:52 -05:00
Alexander Polyankin
4130c16f0e Merge remote-tracking branch 'origin/workbench2' into workbench2
# Conflicts:
2025-11-14 15:12:14 -05:00
Alexander Polyankin
c31a6b3712 Fix tests 2025-11-14 15:11:57 -05:00
Alexander Polyankin
15fad56d26 Fix tests 2025-11-14 15:11:40 -05:00
Aleksandr Lesnenko
acb1d2e14a don't propagate clicks from rightSection of tree items 2025-11-14 14:11:28 -05:00
Aleksandr Lesnenko
611707669e spec 2025-11-14 14:01:14 -05:00
Alexander Polyankin
3b9a0fa2c2 Fix tests 2025-11-14 13:56:59 -05:00
Alexander Polyankin
51662399da Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-14 13:36:24 -05:00
Alexander Polyankin
3c105548de Fix test 2025-11-14 13:36:10 -05:00
Alexander Polyankin
dc4b244ff6 Fix test 2025-11-14 13:32:42 -05:00
Aleksandr Lesnenko
e20e7f38b5 modals 2025-11-14 13:23:58 -05:00
Alexander Polyankin
8b324dc75d Fix test 2025-11-14 13:19:39 -05:00
Alexander Polyankin
d3d98b9dec Fix test 2025-11-14 13:18:22 -05:00
Alexander Polyankin
7b17b208bb Fix test 2025-11-14 13:08:00 -05:00
Alexander Polyankin
ed2ed6de4b Fix test 2025-11-14 13:05:55 -05:00
Alexander Polyankin
d1c640dba4 Fix test 2025-11-14 13:00:34 -05:00
Alexander Polyankin
09b7251b3e Fix test 2025-11-14 12:59:14 -05:00
Alexander Polyankin
da986230cb Fix test 2025-11-14 12:51:58 -05:00
Alexander Polyankin
088126f392 Fix test 2025-11-14 12:16:32 -05:00
Alexander Polyankin
cad4359d51 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-14 12:09:26 -05:00
Alexander Polyankin
8121169b7b Fix test 2025-11-14 12:09:02 -05:00
Nathan Voxland
2d48062e18 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-14 11:06:30 -06:00
Nathan Voxland
54e8d5fb8e Translate Library, Data, and Metric collections 2025-11-14 11:06:19 -06:00
Aleksandr Lesnenko
5709b33da0 probably fixes many failures 2025-11-14 11:39:01 -05:00
Alexander Polyankin
bd58b27031 Fix test 2025-11-14 11:28:05 -05:00
Alexander Polyankin
8575acca08 fixes 2025-11-14 11:14:25 -05:00
Alexander Polyankin
5cf519a128 fixes 2025-11-14 11:13:08 -05:00
Alexander Polyankin
13021db8cd Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-14 11:07:20 -05:00
Alexander Polyankin
09dc306245 fixes 2025-11-14 11:07:07 -05:00
Jakub Chodorowicz
b38d1ae937 internal: add PublishModelsModal tests 2025-11-14 16:42:27 +01:00
Dan Stone
30b9f5dd3d Remove speculative comment about dirty flagging sync 2025-11-14 14:50:25 +00:00
Jakub Chodorowicz
5ae8b77d16 internal: move layer colors to color module 2025-11-14 15:19:45 +01:00
Alexander Polyankin
0affb1367b Fix tabs 2025-11-14 08:35:32 -05:00
Alexander Polyankin
fe98098726 EE plugin 2025-11-14 08:34:22 -05:00
Alexander Polyankin
47b209b095 EE plugin 2025-11-14 08:30:46 -05:00
Jakub Chodorowicz
2017a5959f Data structure (bulk table metadata editing) (#65372)
* Fix database-metadata-test

* Fix database-metadata-test for real

* Fix unused binding warnings

* kebab the get /table params and remove substitute-model api

* Fix some kebab left-overs

* feat: link to dependency graph from table metadata

* feat: link to models from list of dependencies

* Fix tooltip

* fix: show sync button when sorting

* style: add shared table metadata group styling

* fix: spacing when transform link is not there

* Field sort order (GDGT-1191)

* fix: table models section showing for no models

* style: adjust table metadata group padding

* fields

* fields

* fields

* fields

* Bulk sync/scan/discard (GDGT-1194)

* fields

* Remove old fields component

* fields

* fields

* Even buttons out

* Fix settings icon to use currentColor

* fields

* fields

* fields

* fields

* fields

* style: bulk metadata tweaks

* refactor tree and fix some bugs

* style: adjust spacing in table edit

* fields

* fields

* fields

* fields

* fix checkbox position

* Added /api/ee/library/create API

* Don't hard code library feature yet

* types

* types

* types

* types

* types

* types

* types

* types

* types

* types

* snippets

* cleanup

* Copy

* fix collapse/expand and active item logic

* another edge case

* style: fix: paddings in table metadata

* another edge case

* simplify logic

* Remove EditTableMetadataModal

* Styling

* simplify code and update styling

* Add bulk-edit of entity type, reorder inputs in bulk-edit, minor styling (bold labels)

* bulk edit entity_type support in api

* adjust colors

* feat: two columns layout for data model

* style: adjust empty view width

* feat: table metadata fields (name on disk, updated at)

* feat: add dependencies and dependants count

* feat: add view count and estimated row count

* render more data

* fixup

* fix: dependency graph link

* types

* types

* fix: hide estimated row count row when null

* Temporary published_as_model

* types

* feat: show field preview in 3rd column

* types

* types

* types

* types

* Revert "Added /api/ee/library/create API"

* types

* types

* types

* types

* types

* types

* drop library

* types

* types

* fix snippets

* fix command palette test

* use a token feature

* use a token feature

* use a token feature

* test fixes

* test fixes

* let scroll away from selected transform in the tree view

* Add move modal

* Add move modal

* Add move modal

* wip

* at least split megafile

* review

* move segments to modeling

* feat: format numbers in metadata section

* Delete substitute as model test (api removed)

* Check published table is mapped to the source table of the model

* fix(tree): sort fields properly

* refactor: use lib isNullOrUndefined

* fix search crash

* feat: add dependencies links to metadata info

* style: unify padding of columns

* do not show a placeholder for not selected table

* make checkbox area bigger

* Add move modal

* Kondo

* archive modal

* refactor: remove unused variable

* feat: new publish models UI flow

* fix: naming of the acknowledge user value

* style: :refactor adjust spacing, eslint fixes

* feat(tree): add range selection

* archive modal

* archive modal

* Fix menu

* fix not being able to create models and metrics in data studio

* fix

* Fix menu

* Fix menu

* Fix menu

* REVERT ME: library prototype wip

* Fix menu

* Remove nav

* hydrate transform for table api (#65655)

* Remove nav

* Remove nav

* Remove nav

* Remove nav

* Remove nav

* don't fail when there are no transforms

* feat: remove tooltip on publish model buttons

* hydrate transform for query metadata as well

* feat: add transform link in table metadata edit

* fix: data source input not showing transform source

* fix: transform url link

* Productionize bulk sync table apis (#65687)

* fix checkbox bug

* feat: reorganize order of metadata table sections

* add a missing util

* move migration to 58 and remove data_udpate_frequency

* drop fields from the tree

* 4 column layout

* return sorting

* refactor: enteprise plugin useDependenciesCount

* fix: Tree type errors

* fix: type errors related to wrongly typed setState functions

* Update filter popover styling and copy
- Closes GDGT-1220

* Style user input

* Fix types

* Fix types

* Types

* Types

* Types

* Types

* internal: remove unused modal

* internal: fix type error in unit test

* Remove FieldOrderPicker2

* internal: fix typeerror in create model modal

* Lint

* Lint

* Add published_table_id to report_card and associated hydrations (#65661)

* Add published_table_id to report_card and use that in published_as_model table hydration

* Simplify hydration

* Add table :published_models hydration

* Fix whitespace

* Wrong migration file

* Add serde fk for published_table_id

* Kondo when-let

* Kondo temps

* Add DataModelV1 and use it in Admin panel

* internal: fix type error in create metrics modal

* internal: fix typings of schema and table nodes in Tree component

* Remove duplicated stuff from DataModelV1

* internal: fix type error in bulk selection helpers

* internal: fix types in bulk selection typings

* fix merge conflict

* Fix effect re-running forever
- Fixes GDGT-1230

* Productionizing edit tables endpoint (#65535)

* Data source and validation (#65788)

* fix sybntax error

* internal: fix typing in bulk selection

* internal: remove unused SubstituteModelModal

* Integrate table.published_models and table.published_as_model

* Alignment

* cleanup merge conflicts

* fix: remove problematic hardcoded height in icon

* Do not stick table name and description header
- Closes GDGT-1212

* cleanup

* Remove unused ActiveInput

* Order of forms

* Fix button size

* Padding

* Adjust styling of toast button
- Closes GDGT-1227

* Fix updating the data_source of a table

See notes

* fix an issue with checkboxes

* internal: use semantic ocean color

* Do not allow to manually set metabase transform as a table source

* Fix typo

* Remove redundant sorts

* Curious if we are seeing missing migrations post restore

* fix unable to start e2e tests due to table id of sample database mismatch  it changes because we add new columns to the tables and the order of tables we sync changes because of that 💀

* fix serdes tests and remove stray migrations

* Revert "fix unable to start e2e tests due to table id of sample database mismatch  it changes because we add new columns to the tables and the order of tables we sync changes because of that 💀"

This reverts commit e17557d4da.

* fix failed ot sync sample dbs

* transform also count as table dependecies

* fix kondo

* test fixes :green:

* fix a bunch of more tests

* index report_card.published_table_id

* reverse index

* Revert "Curious if we are seeing missing migrations post restore"

This reverts commit b6f2dae154.

* Fix migration indentation

* move files to enterprise folder

* Fix migration indentation for real

* move again

* Add config require

* Fix types

* Revert "Fix types"

This reverts commit 982354df9a.

* typecheck

* Carry lib collection rename through to test

* Change orphan-only => to filter by tables without owners (#65839)

* Change orphan-only => to filter by tables without owners
Add new unused-only => to filter by tables without and dependants

* Use orphan-only in frontend

---------

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

* Override _bin collation on metabase_table.name for maria/mysql term LIKE

* add base tests

---------

Co-authored-by: Dan Stone <wotbrew@gmail.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
Co-authored-by: Uladzimir Havenchyk <uladzimir@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@gmail.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
2025-11-14 20:09:28 +07:00
Aleksandr Lesnenko
57e2001582 e2e specs 2025-11-13 22:14:06 -05:00
Aleksandr Lesnenko
064d02c783 hide empty library collections 2025-11-13 21:59:55 -05:00
Aleksandr Lesnenko
6dec92324d fix logic of showing collection menu in collection header 2025-11-13 21:59:55 -05:00
Alexander Polyankin
1b31c1bd83 EE plugin 2025-11-13 21:09:47 -05:00
Alexander Polyankin
1140b38691 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 20:57:00 -05:00
Alexander Polyankin
d5af13c899 EE plugin 2025-11-13 20:56:43 -05:00
Aleksandr Lesnenko
ed86b03694 collapsible collections 2025-11-13 20:42:12 -05:00
Aleksandr Lesnenko
536136e872 show the library separately at the top 2025-11-13 19:56:44 -05:00
Aleksandr Lesnenko
2da2f0d2bd top library 2025-11-13 18:39:43 -05:00
Aleksandr Lesnenko
ac249dd6d1 collapse our analytics if there is library 2025-11-13 18:20:16 -05:00
Aleksandr Lesnenko
1c926fa58e fix snippets part 2 2025-11-13 17:49:31 -05:00
Aleksandr Lesnenko
d50e05e24a fix snippets part 1 2025-11-13 17:49:31 -05:00
Alexander Polyankin
48f58c1264 Rename Models collection to Data 2025-11-13 15:51:48 -05:00
Alexander Polyankin
144958321f Rename Models collection to Data 2025-11-13 15:50:55 -05:00
Alexander Polyankin
ae12b79fe0 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 15:49:42 -05:00
Alexander Polyankin
cee4ea61ba Rename Models collection to Data 2025-11-13 15:49:33 -05:00
Nathan Voxland
299915e7b4 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 14:39:41 -06:00
Alexander Polyankin
bfa2b9a309 Sidebar for modeling 2025-11-13 15:39:17 -05:00
Nathan Voxland
94c347610d Lint fix 2025-11-13 14:38:19 -06:00
Alexander Polyankin
80613c6763 Sidebar for modeling 2025-11-13 15:17:45 -05:00
Alexander Polyankin
f9c754db95 Sidebar for modeling 2025-11-13 15:16:32 -05:00
Alexander Polyankin
432101b087 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 15:09:34 -05:00
Alexander Polyankin
4e6d12d40f Sidebar for modeling 2025-11-13 15:09:17 -05:00
Nathan Voxland
0f4a5d4a6f Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 14:03:30 -06:00
Nathan Voxland
457c7ff2a9 Moved create back to POSt /api/ee/library 2025-11-13 14:03:14 -06:00
Alexander Polyankin
b25821fada Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 14:58:49 -05:00
Alexander Polyankin
f5197df496 Update name inputs 2025-11-13 14:58:39 -05:00
Nathan Voxland
a2f33d6705 Added GET /api/ee/library
Moved create to POSt /api/ee/library/create
2025-11-13 13:56:54 -06:00
Alexander Polyankin
d0378bc8b5 Update name inputs 2025-11-13 14:55:45 -05:00
Alexander Polyankin
294348e98f moving 2025-11-13 12:00:24 -05:00
Alexander Polyankin
fb3de16e0d moving 2025-11-13 11:44:59 -05:00
Alexander Polyankin
34826093f7 moving 2025-11-13 11:39:02 -05:00
Alexander Polyankin
8308adc3d8 moving 2025-11-13 11:37:56 -05:00
Alexander Polyankin
4a5ffa6cbd moving 2025-11-13 11:31:23 -05:00
Alexander Polyankin
2488ef3254 moving 2025-11-13 11:26:19 -05:00
Alexander Polyankin
67105b8fbc moving 2025-11-13 11:21:16 -05:00
Alexander Polyankin
77ce47f815 moving 2025-11-13 11:17:27 -05:00
Alexander Polyankin
12d5530708 moving 2025-11-13 11:05:38 -05:00
Alexander Polyankin
00c65498e1 Move library checks 2025-11-13 11:00:08 -05:00
Alexander Polyankin
c692dd9286 Move library checks 2025-11-13 10:45:54 -05:00
Alexander Polyankin
0ab71025fd Move library checks 2025-11-13 10:43:02 -05:00
Alexander Polyankin
569cd4f208 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-13 10:39:34 -05:00
Alexander Polyankin
a476852bfc Move library checks 2025-11-13 10:39:23 -05:00
Nathan Voxland
17d07d3594 Kondo fix 2025-11-13 09:32:26 -06:00
Nathan Voxland
cb24c615b6 Fixed tests post-rename 2025-11-13 01:11:37 -06:00
Aleksandr Lesnenko
b60895ac03 drop stupid logic 2025-11-12 23:23:33 -05:00
Alexander Polyankin
87b24e9d09 Disable library collections 2025-11-12 22:18:55 -05:00
Alexander Polyankin
c976599707 Disable library collections 2025-11-12 22:10:22 -05:00
Alexander Polyankin
c36c5bc8ab Add View 2025-11-12 21:54:21 -05:00
Alexander Polyankin
0e1fb566aa Add View 2025-11-12 21:43:38 -05:00
Alexander Polyankin
112ab0194b cleanup 2025-11-12 21:35:31 -05:00
Alexander Polyankin
9a01f495a5 Rename :semantic-layer to :library 2025-11-12 21:27:20 -05:00
Alexander Polyankin
b62ea9face Rename :semantic-layer to :library 2025-11-12 21:18:00 -05:00
Aleksandr Lesnenko
bfc1b11ca4 UXW-2269 Hide collections under Our Analytics in Modeling tab if they don't contain any metrics/models 2025-11-12 21:06:58 -05:00
Aleksandr Lesnenko
4b9ff89204 Merge remote-tracking branch 'origin/master' into workbench2 2025-11-12 20:41:14 -05:00
Aleksandr Lesnenko
b1de0951be check last collection selected validity 2025-11-12 20:34:26 -05:00
Aleksandr Lesnenko
ceb99fa4a7 do not disable if descendants can place the entityType 2025-11-12 20:02:53 -05:00
Aleksandr Lesnenko
dccbeb029a restrict content creation in semantic layer folders 2025-11-12 17:07:37 -05:00
Nathan Voxland
200a887ad2 - Don't allow updates to Library properties
- CI fixes
2025-11-12 14:36:53 -06:00
Alexander Polyankin
b6f39af5b5 OSS 2025-11-12 15:28:53 -05:00
Alexander Polyankin
1447e2f857 OSS 2025-11-12 15:21:26 -05:00
Alexander Polyankin
b49f3e334d OSS 2025-11-12 15:16:16 -05:00
Alexander Polyankin
47dd45c7d7 OSS 2025-11-12 15:00:11 -05:00
Alexander Polyankin
8dff56d5f2 OSS 2025-11-12 14:56:12 -05:00
Alexander Polyankin
376a62e2e4 OSS 2025-11-12 14:53:32 -05:00
Alexander Polyankin
7c46a67a04 OSS 2025-11-12 14:42:13 -05:00
Alexander Polyankin
ec7e9149e9 OSS 2025-11-12 14:40:01 -05:00
Alexander Polyankin
b897ea4dd0 Merge 2025-11-12 14:06:59 -05:00
Alexander Polyankin
ef24c2549e Merge remote-tracking branch 'origin/master' into workbench2
# Conflicts:
#	enterprise/frontend/src/metabase-enterprise/dependencies/index.ts
#	enterprise/frontend/src/metabase-enterprise/feature_level_permissions/index.ts
#	enterprise/frontend/src/metabase-enterprise/metabot/index.tsx
#	enterprise/frontend/src/metabase-enterprise/moderation/index.ts
#	enterprise/frontend/src/metabase-enterprise/plugins.js
#	enterprise/frontend/src/metabase-enterprise/snippets/index.js
#	enterprise/frontend/src/metabase-enterprise/transforms-python/index.tsx
#	enterprise/frontend/src/metabase-enterprise/transforms/index.tsx
2025-11-12 14:05:25 -05:00
Nathan Voxland
f8fce0ca36 Setup library permissions 2025-11-12 11:54:18 -06:00
Nathan Voxland
d4c3bc4970 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-12 11:09:24 -06:00
Alexander Polyankin
6e94dedd76 Comment out the root collection check 2025-11-12 12:08:31 -05:00
Nathan Voxland
476126dd40 Merge remote-tracking branch 'origin/workbench2' into workbench2
# Conflicts:
#	enterprise/backend/src/metabase_enterprise/semantic_layer/validation.clj
2025-11-12 11:07:43 -06:00
Nathan Voxland
8242eb6a52 - Fixed library creation
- Removed include_editable_semantic_layer API flag
2025-11-12 11:07:15 -06:00
Alexander Polyankin
f2a8fdbd54 Comment out the root collection check 2025-11-12 11:57:15 -05:00
Alexander Polyankin
f6574f89ee Comment out the root collection check 2025-11-12 11:41:11 -05:00
Alexander Polyankin
bf8f73cc5f Add default collection id 2025-11-12 11:38:12 -05:00
Nathan Voxland
889b245d5e Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-12 09:59:18 -06:00
Nathan Voxland
702f1a372a Removed content_type, added semantic-layer-models and semantic-layer-metrics types 2025-11-12 09:59:05 -06:00
Alexander Polyankin
fc7906b0ae Add default collection id 2025-11-12 10:44:28 -05:00
Alexander Polyankin
3ba5cc052d Add default collection id 2025-11-12 10:38:25 -05:00
Alexander Polyankin
66da27bf7c Add default collection id 2025-11-12 10:30:23 -05:00
Alexander Polyankin
6ed201fe0a Add default collection id 2025-11-12 10:30:04 -05:00
Alexander Polyankin
74ad8b021d Add default collection id 2025-11-12 10:20:50 -05:00
Alexander Polyankin
1846ab4a96 Add default collection id 2025-11-12 10:08:21 -05:00
Alexander Polyankin
1ad201e041 Simplify nav 2025-11-12 09:58:45 -05:00
Alexander Polyankin
01e2a54c56 Simplify nav 2025-11-12 09:40:28 -05:00
Alexander Polyankin
3761e533b1 Simplify nav 2025-11-12 09:31:25 -05:00
Alexander Polyankin
8cdfd21952 Simplify nav 2025-11-12 09:15:50 -05:00
Alexander Polyankin
8514ef6271 Simplify nav 2025-11-12 08:43:49 -05:00
Alexander Polyankin
fbdedca6ea Simplify nav 2025-11-12 08:38:25 -05:00
Alexander Polyankin
e424da2c28 Simplify nav 2025-11-11 21:29:55 -05:00
Alexander Polyankin
efdb994c9b Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-11 21:29:28 -05:00
Alexander Polyankin
47bb22096a Simplify nav 2025-11-11 21:29:07 -05:00
Nathan Voxland
25b38c7554 Added include_editable_semantic_layer to GET /api/card/:id 2025-11-11 16:34:20 -06:00
Nathan Voxland
796661fd54 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-11 15:32:59 -06:00
Nathan Voxland
515ff64d6a Improved collection serdes 2025-11-11 15:32:48 -06:00
Alexander Polyankin
834f591437 Simplify nav 2025-11-11 16:08:33 -05:00
Alexander Polyankin
013ff8273c Simplify nav 2025-11-11 16:03:46 -05:00
Alexander Polyankin
171276b439 Simplify nav 2025-11-11 15:56:52 -05:00
Alexander Polyankin
f6db06b4af Simplify nav 2025-11-11 15:43:15 -05:00
Nathan Voxland
98f1ab357a Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-11 14:28:01 -06:00
Nathan Voxland
facc0654df Made allowed_content be a list
Made allowed_content use dataset not model
2025-11-11 14:27:50 -06:00
Alexander Polyankin
5630234dac Simplify nav 2025-11-11 15:01:50 -05:00
Alexander Polyankin
26ddab4335 Simplify nav 2025-11-11 15:01:37 -05:00
Alexander Polyankin
af00324ff8 Simplify nav 2025-11-11 14:52:38 -05:00
Alexander Polyankin
cb1d7e7805 Simplify nav 2025-11-11 14:50:28 -05:00
Alexander Polyankin
8d08814128 Simplify nav 2025-11-11 14:34:06 -05:00
Alexander Polyankin
dc84037858 Simplify nav 2025-11-11 14:24:09 -05:00
Alexander Polyankin
952c1b5e84 Simplify nav 2025-11-11 14:19:59 -05:00
Alexander Polyankin
346adeda32 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-11 14:17:27 -05:00
Alexander Polyankin
861d3db6aa Simplify nav 2025-11-11 14:16:52 -05:00
Nathan Voxland
ce5a6d6bda Merge branch 'library' into workbench2 2025-11-11 13:12:53 -06:00
Nathan Voxland
c6d47e6f01 Renamed to semantic layer 2025-11-11 13:10:51 -06:00
Alexander Polyankin
4aebf50623 Simplify nav 2025-11-11 13:44:27 -05:00
Alexander Polyankin
cd56bbcc41 Simplify nav 2025-11-11 13:36:52 -05:00
Alexander Polyankin
f35c128378 Simplify nav 2025-11-11 13:36:11 -05:00
Alexander Polyankin
4e4fcefdcf Simplify nav 2025-11-11 13:20:44 -05:00
Alexander Polyankin
88005eb443 Simplify nav 2025-11-11 13:20:12 -05:00
Alexander Polyankin
e740851b38 Simplify nav 2025-11-11 13:17:42 -05:00
Alexander Polyankin
6e5f4a4248 Simplify nav 2025-11-11 13:14:38 -05:00
Alexander Polyankin
7b651d7cf0 Simplify nav 2025-11-11 13:05:55 -05:00
Alexander Polyankin
24e3438e3f Simplify nav 2025-11-11 12:41:56 -05:00
Alexander Polyankin
420a82da88 Simplify nav 2025-11-11 12:31:59 -05:00
Alexander Polyankin
a144ba57a2 Simplify nav 2025-11-11 12:25:29 -05:00
Alexander Polyankin
c4ea11f71e Simplify nav 2025-11-11 12:24:13 -05:00
Nathan Voxland
0949ad4c14 Fixed CI failures 2025-11-11 10:30:07 -06:00
Alexander Polyankin
1874944538 Simplify nav 2025-11-11 10:41:23 -05:00
Alexander Polyankin
535672c8a8 Nav changes 2025-11-11 09:39:24 -05:00
Nathan Voxland
da3ee83c9a Fixed CI failures 2025-11-11 00:32:09 -06:00
Nathan Voxland
0a164e5554 Fixed hooks 2025-11-10 23:17:00 -06:00
Alexander Polyankin
0863ff7107 Merge master 2025-11-10 22:44:25 -05:00
Alexander Polyankin
a9da08972e Merge master 2025-11-10 22:36:25 -05:00
Alexander Polyankin
9cf8fdee58 Merge master 2025-11-10 22:31:47 -05:00
Alexander Polyankin
fd74bf9962 Merge master 2025-11-10 22:26:07 -05:00
Alexander Polyankin
3c8dd544c0 Merge master 2025-11-10 22:11:50 -05:00
Alexander Polyankin
7b04a5828c Data Studio: models, metrics, snippets, glossary (#65395) 2025-11-10 22:07:57 -05:00
Nathan Voxland
76bd5760bf Added checks in update operations 2025-11-10 17:16:53 -06:00
Nathan Voxland
6452043e7f Fixed library module requirements 2025-11-10 16:35:38 -06:00
Nathan Voxland
4d37013c2e Merge remote-tracking branch 'origin/master' into library 2025-11-10 16:24:38 -06:00
Nathan Voxland
ae4e9d5577 Only allow inserting allowed content 2025-11-10 16:15:07 -06:00
Aleksandr Lesnenko
45ca2337ba update transforms sidebar 2025-11-07 21:05:52 -05:00
Aleksandr Lesnenko
5198680d05 review 2025-11-07 16:53:37 -05:00
Nathan Voxland
87196224e4 Added /api/ee/library/create API 2025-11-07 10:44:27 -06:00
Kamil Mielnik
258e4f2c31 Merge branch 'master' into workbench2 2025-11-06 23:23:41 -08:00
Aleksandr Lesnenko
2205142c1d Merge branch 'master' into workbench2 2025-11-04 12:16:58 -05:00
Aleksandr Lesnenko
a6e689c827 update a path 2025-11-04 12:15:19 -05:00
Aleksandr Lesnenko
5282d3a312 fix metabot data-studio integration 2025-11-04 10:56:20 -05:00
Alexander Polyankin
768038b21f . 2025-11-03 23:39:18 -05:00
Alexander Polyankin
64210dc82f . 2025-11-03 23:32:57 -05:00
Alexander Polyankin
23343b6ed3 . 2025-11-03 23:26:09 -05:00
Alexander Polyankin
8f66b4ef34 . 2025-11-03 23:17:58 -05:00
Alexander Polyankin
f63677e971 . 2025-11-03 22:57:07 -05:00
Alexander Polyankin
4163c1641b . 2025-11-03 22:35:48 -05:00
Alexander Polyankin
5c174399c6 Fix paths 2025-11-03 22:33:08 -05:00
Alexander Polyankin
fb10250deb Fix paths 2025-11-03 22:23:06 -05:00
Alexander Polyankin
a58f30b34c Fix paths 2025-11-03 22:14:32 -05:00
Alexander Polyankin
710f25794a Fix paths 2025-11-03 22:09:53 -05:00
Alexander Polyankin
6cba76168a Fix paths 2025-11-03 22:02:57 -05:00
Alexander Polyankin
fc82ab1b9a Fix paths 2025-11-03 21:57:43 -05:00
Alexander Polyankin
3cf5565030 Fix paths 2025-11-03 21:57:25 -05:00
Alexander Polyankin
3c7e12b8c6 Fix paths 2025-11-03 21:51:45 -05:00
Alexander Polyankin
f448ab6de5 Fix paths 2025-11-03 21:42:00 -05:00
Alexander Polyankin
fa59881ddf Fix paths 2025-11-03 21:38:52 -05:00
Aleksandr Lesnenko
986c3289ea lint 2025-11-03 20:40:00 -05:00
Aleksandr Lesnenko
f5e258f051 specs 2025-11-03 20:24:10 -05:00
Aleksandr Lesnenko
99da7a96e5 specs 2025-11-03 20:24:10 -05:00
Alexander Polyankin
c6a32ea431 Fix paths 2025-11-03 20:21:59 -05:00
Alexander Polyankin
2a90485d4a Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-03 20:18:15 -05:00
Alexander Polyankin
c9f7e8f66b Fix paths 2025-11-03 20:17:57 -05:00
Aleksandr Lesnenko
21eaffb153 specs 2025-11-03 20:10:12 -05:00
Aleksandr Lesnenko
f760b2d78d polish, fixing bugs caught by e2e specs, update e2e specs 2025-11-03 20:01:29 -05:00
Aleksandr Lesnenko
1f1142860e UXW-2211 transforms inner nav width 2025-11-03 18:02:27 -05:00
Aleksandr Lesnenko
3c3e7776ab move Metabot to data studio 2025-11-03 18:00:22 -05:00
Aleksandr Lesnenko
af982a3ca4 replace transforms urls 2025-11-03 17:59:54 -05:00
Aleksandr Lesnenko
178a9ce965 replace datamodel path to point to data studio 2025-11-03 17:27:50 -05:00
Aleksandr Lesnenko
a7c893f7c0 types 2025-11-03 17:21:47 -05:00
Aleksandr Lesnenko
203ae9c55b some ui polish 2025-11-03 15:00:25 -05:00
Alexander Polyankin
cc3105ef0f Fix paths 2025-11-03 14:58:04 -05:00
Alexander Polyankin
48819b05a7 Fix icon 2025-11-03 13:41:40 -05:00
Alexander Polyankin
5ab21902d1 Fix icon 2025-11-03 13:38:51 -05:00
Alexander Polyankin
88f0851fd4 Fix icon 2025-11-03 12:23:21 -05:00
Alexander Polyankin
b64803ab91 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-11-03 11:35:13 -05:00
Alexander Polyankin
c700965477 Flicker 2025-11-03 11:35:01 -05:00
Alexander Polyankin
6ade875f8b Flicker 2025-11-03 11:31:17 -05:00
Aleksandr Lesnenko
1c7e5f448c layout fixes, python shared lib stub link -- need to use update with correct icon 2025-11-03 11:27:12 -05:00
Alexander Polyankin
8fdc854aea Flicker 2025-11-03 10:43:27 -05:00
Alexander Polyankin
f3c35d4184 Merge branch 'master' into workbench2 2025-11-03 10:37:14 -05:00
Alexander Polyankin
47384a7965 Flicker 2025-11-03 10:33:49 -05:00
Alexander Polyankin
35594ed38e Flicker 2025-11-03 10:31:16 -05:00
Alexander Polyankin
5cb92f6592 Flicker 2025-11-03 10:27:07 -05:00
Alexander Polyankin
31d5471b5b Flicker 2025-11-03 10:23:30 -05:00
Alexander Polyankin
0242840402 Flicker 2025-11-03 10:10:11 -05:00
Alexander Polyankin
8866f2d446 Permissions 2025-11-03 10:07:59 -05:00
Alexander Polyankin
a6f4ba70d6 Permissions 2025-11-03 09:43:21 -05:00
Alexander Polyankin
800496c30b Permissions 2025-11-03 09:41:56 -05:00
Alexander Polyankin
49d04555c9 Permissions 2025-11-03 09:39:18 -05:00
Alexander Polyankin
ab719e84f6 Empty states 2025-11-03 09:34:02 -05:00
Alexander Polyankin
126fefa0f1 Empty states 2025-11-03 09:29:15 -05:00
Alexander Polyankin
9bb252a52b Empty states 2025-11-03 09:18:55 -05:00
Alexander Polyankin
47b4c2473a Empty states 2025-11-03 09:17:11 -05:00
Alexander Polyankin
8a34280084 Empty states 2025-11-03 09:14:53 -05:00
Alexander Polyankin
dd301f5587 port runs 2025-11-03 09:10:54 -05:00
Alexander Polyankin
3748a5a0c4 port runs 2025-11-03 09:04:36 -05:00
Alexander Polyankin
4cefbd1011 port runs 2025-11-03 09:03:00 -05:00
Alexander Polyankin
84eeb9d35b port runs 2025-11-03 09:00:00 -05:00
Alexander Polyankin
22935320ca port runs 2025-10-31 22:53:30 -04:00
Alexander Polyankin
6bb12036ba port runs 2025-10-31 22:37:01 -04:00
Alexander Polyankin
5214db1b34 port runs 2025-10-31 22:36:14 -04:00
Alexander Polyankin
65007662b6 port runs 2025-10-31 22:36:03 -04:00
Alexander Polyankin
fc8e625fd1 port runs 2025-10-31 22:26:17 -04:00
Alexander Polyankin
7f880449e6 port runs 2025-10-31 22:18:30 -04:00
Alexander Polyankin
4126a6fb3c port runs 2025-10-31 22:01:07 -04:00
Alexander Polyankin
97b0caa82b port runs 2025-10-31 21:52:35 -04:00
Alexander Polyankin
a848c57956 Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-10-31 21:42:00 -04:00
Alexander Polyankin
2b06075990 port runs 2025-10-31 21:41:49 -04:00
Alexander Polyankin
8fdf387887 port runs 2025-10-31 21:17:21 -04:00
Alexander Polyankin
1c5c3da666 port runs 2025-10-31 21:12:37 -04:00
Aleksandr Lesnenko
bfb3848543 new transform 2025-10-31 20:04:49 -04:00
Alexander Polyankin
1a14bfa131 port runs 2025-10-31 19:05:08 -04:00
Alexander Polyankin
96acca5e92 Merge remote-tracking branch 'origin/master' into workbench2 2025-10-31 19:02:15 -04:00
Alexander Polyankin
bb5fa428b5 port runs 2025-10-31 19:01:47 -04:00
Alexander Polyankin
7d11c2f799 port runs 2025-10-31 19:00:23 -04:00
Alexander Polyankin
e7e029a58c port runs 2025-10-31 18:56:16 -04:00
Alexander Polyankin
1d10123201 port runs 2025-10-31 18:50:02 -04:00
Alexander Polyankin
a77842e92b port runs 2025-10-31 18:49:22 -04:00
Alexander Polyankin
9b0d149418 port runs 2025-10-31 18:42:06 -04:00
Alexander Polyankin
dc7ae946c6 port runs 2025-10-31 18:39:17 -04:00
Alexander Polyankin
ddf5a9d460 port runs 2025-10-31 18:37:52 -04:00
Alexander Polyankin
cc6ce2834f Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-10-31 18:37:06 -04:00
Alexander Polyankin
82a7b2f5e4 port runs 2025-10-31 18:36:55 -04:00
Aleksandr Lesnenko
7e4daf2721 jobs list 2025-10-31 17:41:21 -04:00
Aleksandr Lesnenko
ac8c6e6e0c tree view, search, sorting 2025-10-31 17:07:23 -04:00
Aleksandr Lesnenko
2e94a0f579 show transforms list in the sidebar 2025-10-31 15:23:15 -04:00
Alexander Polyankin
d746234cb6 port runs 2025-10-31 14:01:48 -04:00
Alexander Polyankin
eecc699fbd port runs 2025-10-31 14:00:22 -04:00
Alexander Polyankin
72de9e73ce port runs 2025-10-31 13:59:10 -04:00
Alexander Polyankin
0abe4483fc port runs 2025-10-31 13:44:37 -04:00
Alexander Polyankin
4292db5bcf Merge remote-tracking branch 'origin/workbench2' into workbench2 2025-10-31 13:29:38 -04:00
Alexander Polyankin
c6027c28c6 port runs 2025-10-31 13:29:18 -04:00
Aleksandr Lesnenko
43cca10568 transforms inner nav 2025-10-31 13:28:55 -04:00
Alexander Polyankin
bac077c21d port jobs 2025-10-31 13:26:27 -04:00
Alexander Polyankin
1895291a04 Bench 2025-10-31 13:19:00 -04:00
Alexander Polyankin
fa7ba42783 Bench 2025-10-31 12:59:30 -04:00
Alexander Polyankin
9f39d20e07 Bench 2025-10-31 12:57:43 -04:00
Alexander Polyankin
0df67d497a Bench 2025-10-31 12:53:07 -04:00
Alexander Polyankin
876e3e3c27 Bench 2025-10-31 12:50:38 -04:00
Alexander Polyankin
9db2c15514 Bench 2025-10-31 12:46:20 -04:00
Alexander Polyankin
7ecdf0c5dd Bench 2025-10-31 12:37:06 -04:00
Alexander Polyankin
4f0a8c14b4 Bench 2025-10-31 12:34:27 -04:00
Alexander Polyankin
92d0358840 Bench 2025-10-31 12:27:32 -04:00
Alexander Polyankin
7ce95abb57 Bench 2025-10-31 12:20:01 -04:00
Alexander Polyankin
db118fbe12 Bench 2025-10-31 12:07:23 -04:00
Alexander Polyankin
06e2499210 Bench 2025-10-31 12:06:36 -04:00
Alexander Polyankin
1fd9f81f3b Bench 2025-10-31 11:53:06 -04:00
Alexander Polyankin
7e2a230c90 Bench 2025-10-31 11:52:03 -04:00
Alexander Polyankin
b508b28048 Bench 2025-10-31 11:43:24 -04:00
Alexander Polyankin
400d8426c0 Bench 2025-10-31 10:58:07 -04:00
Alexander Polyankin
7c437e28e6 Bench 2025-10-31 10:54:55 -04:00
Alexander Polyankin
657ab9590b Bench 2025-10-31 10:51:11 -04:00
Alexander Polyankin
8b68bbcc75 Bench 2025-10-31 10:42:02 -04:00
Alexander Polyankin
fe4ee32e2f Bench 2025-10-31 10:24:31 -04:00
Alexander Polyankin
99b089d83d Bench 2025-10-31 10:23:19 -04:00
Alexander Polyankin
a0da007219 Empty 2025-10-31 09:41:38 -04:00
1010 changed files with 33645 additions and 6937 deletions

View File

@@ -237,6 +237,7 @@
:api #{metabase.bookmarks.api}
:uses #{api
app-db
collections
config
premium-features
queries
@@ -1530,6 +1531,7 @@
api
app-db
audit-app
config
driver
lib
lib-be
@@ -1554,6 +1556,7 @@
lib
models
parameters
premium-features
query-processor
request
sync
@@ -1613,7 +1616,6 @@
warehouses
enterprise/advanced-permissions}}
xrays
{:team "Querying"
:api #{metabase.xrays.api
@@ -1840,6 +1842,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
@@ -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

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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

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");
}
@@ -238,7 +260,7 @@ function getTableSortOrderInput() {
}
function getTableSyncOptionsButton() {
return getTableSection().button(/Sync options/);
return getTableSection().button(/Sync/);
}
function getTableSectionField(name: string) {
@@ -253,6 +275,10 @@ function getTableSectionSortableFields() {
return getTableSection().findAllByRole("listitem");
}
function getTableSectionVisibilityTypeInput() {
return getTableSection().findByLabelText("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

@@ -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);
}
@@ -407,6 +419,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,70 @@
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,
}),
),
),
),
);
},
);
}

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

@@ -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 on disk").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", "No one").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 layer 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", "Unknown").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,55 @@
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");
});
});
});

View File

@@ -0,0 +1,413 @@
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.skip("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");
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,280 @@
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.skip("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");
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

@@ -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")

View File

@@ -1,8 +1,7 @@
import {
NativeEditor,
entityPickerModal,
entityPickerModalTab,
interceptIfNotPreviouslyDefined,
miniPicker,
modal,
openQuestionActions,
popover,
@@ -98,8 +97,8 @@ export function selectFromDropdown(option, clickOpts) {
export function startQuestionFromModel(modelName) {
cy.findByTestId("app-bar").findByText("New").click();
popover().findByText("Question").should("be.visible").click();
entityPickerModal().within(() => {
entityPickerModalTab("Collections").click();
miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(modelName).click();
});
}

View File

@@ -362,9 +362,10 @@ describe("scenarios > models", () => {
it("transforms the data picker", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText("Orders").should("exist");
cy.findByText("Orders Model").should("exist");
cy.findByText("Orders, Count").should("exist");
@@ -373,11 +374,13 @@ describe("scenarios > models", () => {
);
cy.findByText("Products").should("exist");
H.entityPickerModalTab("Tables").click();
cy.findByText("Orders").should("exist");
cy.findByText("People").should("exist");
cy.findByText("Products").should("exist");
cy.findByText("Reviews").should("exist");
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, "Orders").should("exist");
H.entityPickerModalItem(1, "People").should("exist");
H.entityPickerModalItem(1, "Products").should("exist");
H.entityPickerModalItem(1, "Reviews").should("exist");
cy.findByText("Orders, Count").should("not.exist");
cy.findByPlaceholderText("Search this database or everywhere…").type(
@@ -422,20 +425,23 @@ describe("scenarios > models", () => {
cy.intercept(`/api/database/${SAMPLE_DB_ID}/schema/PUBLIC`).as("schema");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText("Orders").click();
});
cy.icon("join_left_outer").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalTab("Data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Orders").should("exist");
cy.findByText("People").should("exist");
cy.findByText("Products").should("exist");
cy.findByText("Reviews").should("exist");
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, "Orders").should("exist");
H.entityPickerModalItem(1, "People").should("exist");
H.entityPickerModalItem(1, "Products").should("exist");
H.entityPickerModalItem(1, "Reviews").should("exist");
cy.findByText("Products").click();
H.entityPickerModalItem(1, "Products").click();
});
H.getNotebookStep("filter")
@@ -479,9 +485,8 @@ describe("scenarios > models", () => {
it("should not display models if nested queries are disabled", () => {
H.mockSessionProperty("enable-nested-queries", false);
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.findAllByRole("tab").should("not.exist");
cy.findByText("Orders").should("exist");
cy.findByText("People").should("exist");
cy.findByText("Products").should("exist");

View File

@@ -94,8 +94,8 @@ describe("issue 19737", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Question").should("be.visible").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText(personalCollectionName).click();
cy.findByText(modelName);
});
@@ -119,8 +119,8 @@ describe("issue 19737", () => {
cy.findByText("Question").should("be.visible").click();
// Open question picker (this is crucial) so the collection list are loaded.
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText("First collection").click();
cy.findByText(modelName);
});
@@ -143,8 +143,8 @@ describe("issue 19737", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Question").should("be.visible").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText("First collection").should("not.exist");
H.entityPickerModalLevel(1).should("exist");
H.entityPickerModalLevel(2).should("not.exist");
@@ -152,54 +152,6 @@ describe("issue 19737", () => {
});
});
// this is only testable in OSS because EE always has models from auditv2
describe("issue 19776", { tags: "@OSS" }, () => {
const modelName = "Orders Model";
function openEllipsisMenuFor(item) {
cy.findByText(item).closest("tr").find(".Icon-ellipsis").click();
}
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("should reflect archived model in the data picker without refreshing (metabase#19776)", () => {
cy.visit("/");
cy.findByTestId("app-bar").button("New").click();
H.popover().findByText("Question").click();
H.entityPickerModalTab("Collections").click(); // now you see it
H.entityPickerModal()
.button(/Filter/)
.click();
H.popover().findByText("Models").should("exist");
H.entityPickerModal().findByLabelText("Close").click();
// navigate without a page load
cy.findByTestId("sidebar-toggle").click();
H.navigationSidebar().findByText("Our analytics").click();
// archive the only model
cy.findByTestId("collection-table").within(() => {
openEllipsisMenuFor(modelName);
});
H.popover().contains("Move to trash").click();
cy.findByTestId("undo-list").findByText("Trashed model");
cy.findByTestId("app-bar").button("New").click();
H.popover().findByText("Question").click();
H.entityPickerModalTab("Collections").click(); // now you don't
H.entityPickerModal()
.button(/Filter/)
.click();
H.popover().findByText("Models").should("not.exist");
});
});
describe("issue 20042", () => {
beforeEach(() => {
cy.intercept("POST", `/api/card/${ORDERS_QUESTION_ID}/query`).as("query");
@@ -803,7 +755,6 @@ describe("issue 25537", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
cy.intercept("GET", "/api/collection/*/items?*").as("getCollectionContent");
});
it("should be able to pick a saved model when using a non-english locale (metabase#25537)", () => {
@@ -811,9 +762,8 @@ describe("issue 25537", () => {
H.createQuestion(questionDetails);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Sammlungen").click();
cy.wait("@getCollectionContent");
H.miniPicker().within(() => {
cy.findByText("Unsere Analysen").click();
cy.findByText(questionDetails.name).should("exist");
});
});
@@ -842,8 +792,8 @@ describe("issue 26091", () => {
cy.visit("/");
startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.saveQuestion("New model", undefined, {
@@ -853,8 +803,8 @@ describe("issue 26091", () => {
turnIntoModel();
startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("New model").should("be.visible");
cy.findByText("Old model").should("be.visible");
cy.findByText("Orders Model").should("be.visible");
@@ -921,8 +871,8 @@ describe("issue 28971", () => {
it("should be able to filter a newly created model (metabase#28971)", () => {
H.startNewModel();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cy.findByTestId("run-button").click();

View File

@@ -398,13 +398,13 @@ describe("issue 41785, issue 46756", () => {
it("does not break the question when removing column with the same mapping as another column (metabase#41785) (metabase#46756)", () => {
// it's important to create the model through UI to reproduce this issue
H.startNewModel();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.popover().findByText("ID").click();
@@ -465,8 +465,8 @@ describe("issue 40635", () => {
it("correctly displays question's and nested model's column names (metabase#40635)", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -475,8 +475,8 @@ describe("issue 40635", () => {
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
@@ -490,8 +490,8 @@ describe("issue 40635", () => {
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
@@ -672,8 +672,8 @@ describe("issue 25113", () => {
H.createQuestion(questionDetails, { visitQuestion: true });
H.openNotebook();
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("People Model").click();
});
H.popover().findByText("ID").click();
@@ -879,8 +879,8 @@ describe("issue 33844", () => {
cy.findByTestId("new-model-options")
.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();
});
cy.findByTestId("run-button").click();
@@ -1070,11 +1070,11 @@ describe("issue 34517", () => {
cy.location("pathname").should("eq", "/model/query");
// wait for the model editor to be fully loaded
H.entityPickerModal().should("exist");
H.miniPicker().should("exist");
cy.reload();
// wait for the model editor to be fully loaded
H.entityPickerModal().should("exist");
H.miniPicker().should("exist");
cy.location("pathname").should("eq", "/model/query");
});
});
@@ -1188,7 +1188,6 @@ describe("issue 34514", () => {
H.restore();
cy.signInAsAdmin();
cy.intercept("POST", "/api/dataset").as("dataset");
cy.intercept("GET", "/api/database/*/schema/*").as("fetchTables");
cy.intercept("GET", "/api/database/*").as("fetchDatabase");
// It's important to navigate via UI so that there are
@@ -1200,9 +1199,8 @@ describe("issue 34514", () => {
});
it("should not make network request with invalid query (metabase#34514)", () => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.wait("@fetchTables");
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -1215,9 +1213,8 @@ describe("issue 34514", () => {
});
it("should allow browser history navigation between tabs (metabase#34514, metabase#45787)", () => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.wait("@fetchTables");
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -1253,13 +1250,12 @@ describe("issue 34514", () => {
}
function assertBackToEmptyState() {
H.entityPickerModal().should("be.visible");
H.entityPickerModal().button("Close").click();
H.miniPicker().should("be.visible");
cy.findByTestId("editor-tabs-columns").should("be.disabled");
cy.button("Save").should("be.disabled");
H.getNotebookStep("data")
.findByText("Pick your starting data")
.findByPlaceholderText("Search for tables and more...")
.should("be.visible");
H.tableInteractive().should("not.exist");
cy.findByTestId("query-visualization-root").within(() => {
@@ -1329,13 +1325,13 @@ describe("issue 47988", () => {
H.createQuestion(model1Details);
H.createQuestion(model2Details);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("M1").click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("M2").click();
});
H.visualize();
@@ -1696,8 +1692,8 @@ describe("issue 57359", () => {
H.openOrdersTable({ mode: "notebook" });
cy.wrap([1, 2]).each(() => {
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
});
@@ -1783,7 +1779,8 @@ describe("Issue 30712", () => {
H.startNewModel();
H.entityPickerModal().findByText("Orders").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Orders").click();
H.join();
H.joinTable("Products");
@@ -1961,13 +1958,13 @@ describe("issue 50915", () => {
cy.log("create a model via the UI");
cy.visit("/model/new");
H.main().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.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("People").click();
});
cy.findByTestId("dataset-edit-bar").button("Save").click();

View File

@@ -139,7 +139,7 @@ describe("Add data modal", () => {
cy.findByText(/Getting Started/i).should("not.exist");
cy.log("Adding data from the 'Data' section should work");
cy.findByRole("tab", { name: /^Data/i })
cy.findByRole("section", { name: "Data" })
.findByLabelText("Add data")
.click();
});
@@ -245,7 +245,7 @@ describe("Add data modal", () => {
cy.findByLabelText("Navigation bar").contains("Exit admin").click();
H.navigationSidebar()
.findByRole("tab", { name: /^Data/i })
.findByRole("section", { name: "Data" })
.findByLabelText("Add data")
.should("be.visible")
.click();
@@ -379,7 +379,7 @@ describe("Add data modal", () => {
it("should be hidden for non-admins without upload permissions", () => {
cy.signInAsNormalUser();
cy.visit("/");
cy.findByRole("tab", { name: /^Data/i }).within(() => {
cy.findByRole("section", { name: "Data" }).within(() => {
cy.findByLabelText("Add data").should("not.exist");
});
});
@@ -390,7 +390,7 @@ const addDataModal = () => cy.findByRole("dialog", { name: "Add data" });
const openAddDataModalFromSidebar = () =>
H.navigationSidebar()
.findByRole("tab", { name: /^Data/i })
.findByRole("section", { name: "Data" })
.findByLabelText("Add data")
.should("be.visible")
.click();

View File

@@ -645,11 +645,11 @@ describe("shortcuts", { tags: ["@actions"] }, () => {
cy.realPress("o");
H.openNavigationSidebar();
H.navigationSidebar()
.findByRole("tab", { name: /bookmarks/i })
.findByRole("section", { name: "Bookmarks" })
.should("contain.text", "Test Dashboard");
cy.realPress("o");
H.navigationSidebar()
.findByRole("tab", { name: /bookmarks/i })
.findByRole("section", { name: "Bookmarks" })
.should("not.exist");
cy.realPress("e");

View File

@@ -141,7 +141,7 @@ describe("scenarios > browse", () => {
.findByLabelText("Create a new metric")
.should("be.visible")
.click();
cy.findByTestId("entity-picker-modal").should("be.visible");
H.miniPicker().should("be.visible");
H.expectNoBadSnowplowEvents();
H.expectUnstructuredSnowplowEvent({

View File

@@ -87,9 +87,10 @@ describe("scenarios > reference > databases", () => {
it("should sort databases in new UI based question data selection popover", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByTestId("item-picker-level-0").within(() => {
H.entityPickerModalItem(0, "Databases").click();
cy.findByTestId("item-picker-level-1").within(() => {
cy.get("[data-index='0']").should("contain.text", "a");
cy.get("[data-index='1']").should("contain.text", "b");
cy.get("[data-index='2']").should("contain.text", "c");

View File

@@ -153,7 +153,7 @@ describe("user > settings", () => {
});
});
it("Should show correct translations when a user logs in with a locale that is different from the site locale", () => {
it("should show correct translations when a user logs in with a locale that is different from the site locale", () => {
cy.intercept("GET", "/api/user/current").as("getUser");
cy.request("PUT", `/api/user/${NORMAL_USER_ID}`, { locale: "fr" });
cy.signOut();
@@ -165,7 +165,7 @@ describe("user > settings", () => {
// should be redirected to new question page
cy.wait("@getUser");
H.entityPickerModalTab("Collections").click();
H.miniPicker().findByText("Parcourir tout").click();
H.entityPickerModal().findByText("Orders Model").click();
cy.findByTestId("step-summarize-0-0")
.findByText("Summarize")

View File

@@ -226,7 +226,7 @@ function addBookmarkTo(name) {
H.popover().findByTextEnsureVisible("Bookmark").click();
H.navigationSidebar()
.findByRole("tab", { name: /^Bookmark/ })
.findByRole("section", { name: "Bookmarks" })
.should("contain", name);
}
@@ -236,7 +236,7 @@ function removeBookmarkFrom(name) {
H.popover().findByTextEnsureVisible("Remove from bookmarks").click();
H.navigationSidebar()
.findByRole("tab", { name: /^Bookmark/ })
.findByRole("section", { name: "Bookmarks" })
.should("not.exist");
}

View File

@@ -53,8 +53,9 @@ describe("scenarios > question > bookmarks", () => {
// when we assert on the next toast (when we turn the model back to the question).
H.undoToastList().icon("close").click();
H.openNavigationSidebar();
H.navigationSidebar().within(() => {
cy.findByLabelText(/Bookmarks/)
cy.findByRole("section", { name: "Bookmarks" })
.icon("model")
.should("exist");
});
@@ -67,7 +68,7 @@ describe("scenarios > question > bookmarks", () => {
H.openNavigationSidebar();
cy.log("Should not find bookmark");
H.navigationSidebar().within(() => {
cy.findByLabelText(/Bookmarks/)
cy.findByRole("section", { name: "Bookmarks" })
.icon("model")
.should("not.exist");
});

View File

@@ -38,8 +38,9 @@ describe("scenarios > organization > entity picker", () => {
describe("tables", () => {
it("should select a table from local search results", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
enterSearchText({
text: "prod",
placeholder: "Search this database or everywhere…",
@@ -56,8 +57,9 @@ describe("scenarios > organization > entity picker", () => {
it("should select a table from global search results", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
enterSearchText({
text: "prod",
placeholder: "Search this database or everywhere…",
@@ -77,8 +79,9 @@ describe("scenarios > organization > entity picker", () => {
H.startNewQuestion();
cy.log("create a recent item");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("Products").click();
});
@@ -86,9 +89,11 @@ describe("scenarios > organization > entity picker", () => {
"should search globally for recents and locally for tables by default",
);
H.getNotebookStep("data").findByText("Products").click();
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.log("local -> global transition without changing search text");
H.entityPickerModalTab("Tables").click();
H.entityPickerModalTab("Data").click();
enterSearchText({
text: "Orders",
placeholder: "Search this database or everywhere…",
@@ -103,7 +108,7 @@ describe("scenarios > organization > entity picker", () => {
});
cy.log("global -> local transition without changing search text");
H.entityPickerModalTab("Tables").click();
H.entityPickerModalTab("Data").click();
existingSearchTab().click();
localSearchTab("Sample Database").should("be.checked");
assertSearchResults({
@@ -135,8 +140,9 @@ describe("scenarios > organization > entity picker", () => {
it("should search for tables in the only database", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
enterSearchText({
text: "prod",
placeholder: "Search this database or everywhere…",
@@ -159,10 +165,11 @@ describe("scenarios > organization > entity picker", () => {
});
cy.signInAsNormalUser();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
cy.log("real table name should give no results");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
enterSearchText({
text: "Orders",
placeholder: "Search this database or everywhere…",
@@ -203,8 +210,9 @@ describe("scenarios > organization > entity picker", () => {
cy.log("first database - pre-selected");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
enterSearchText({
text: "prod",
placeholder: "Search this database or everywhere…",
@@ -219,7 +227,7 @@ describe("scenarios > organization > entity picker", () => {
cy.log("second database");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalTab("Data").click();
cy.findByText("Writable Postgres12").click();
enterSearchText({
text: "s",
@@ -236,7 +244,7 @@ describe("scenarios > organization > entity picker", () => {
cy.log("first database - manually selected");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalTab("Data").click();
cy.findByText("Sample Database").click();
enterSearchText({
text: "prod",
@@ -263,8 +271,9 @@ describe("scenarios > organization > entity picker", () => {
cy.log("first schema");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("Writable Postgres12").click();
enterSearchText({
text: "s",
@@ -280,7 +289,8 @@ describe("scenarios > organization > entity picker", () => {
cy.log("second schema");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalTab("Data").click();
cy.findByText("Writable Postgres12").click();
cy.findByText("Wild").click();
enterSearchText({
text: "s",
@@ -302,8 +312,9 @@ describe("scenarios > organization > entity picker", () => {
H.restore("mysql-8");
cy.signInAsAdmin();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("QA MySQL8").click();
enterSearchText({
text: "orders",
@@ -321,7 +332,7 @@ describe("scenarios > organization > entity picker", () => {
});
describe("cards", () => {
const tabs = ["Collections"];
const tabs = ["Data"];
it("should select a card from local search results", () => {
cy.signInAsAdmin();
@@ -330,28 +341,25 @@ describe("scenarios > organization > entity picker", () => {
const testCases = [
{
tab: "Collections",
cardName: "Root question 1",
sourceName: "Root question 1",
},
{
tab: "Collections",
cardName: "Root model 2",
sourceName: "Root model 2",
},
{
tab: "Collections",
cardName: "Root metric 1",
sourceName: "Orders",
},
];
testCases.forEach(({ tab, cardName, sourceName }) => {
testCases.forEach(({ cardName, sourceName }) => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tab).click();
enterSearchText({
text: cardName,
placeholder: "Search this collection or everywhere…",
placeholder: /Search/,
});
cy.findByText(cardName).click();
});
@@ -361,8 +369,9 @@ describe("scenarios > organization > entity picker", () => {
cy.log("scope search in a dashboard");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalTab("Data").click();
H.entityPickerModalItem(1, "Orders in a dashboard").click();
enterSearchText({
text: "Orders",
@@ -383,28 +392,28 @@ describe("scenarios > organization > entity picker", () => {
const testCases = [
{
tab: "Collections",
cardName: "Regular question 1",
sourceName: "Regular question 1",
},
{
tab: "Collections",
cardName: "Regular model 2",
sourceName: "Regular model 2",
},
{
tab: "Collections",
cardName: "Regular metric 1",
sourceName: "Orders",
},
];
testCases.forEach(({ tab, cardName, sourceName }) => {
testCases.forEach(({ cardName, sourceName }, i) => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tab).click();
if (i > 0) {
H.entityPickerModalTab("Data").click();
}
enterSearchText({
text: cardName,
placeholder: "Search this collection or everywhere…",
placeholder: /Search/,
});
selectGlobalSearchTab();
cy.findByText(cardName).click();
@@ -415,8 +424,9 @@ describe("scenarios > organization > entity picker", () => {
cy.log("should find dashboard questions in global search");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalTab("Data").click();
enterSearchText({
text: "Dashboard question 1",
placeholder: "Search this collection or everywhere…",
@@ -435,6 +445,7 @@ describe("scenarios > organization > entity picker", () => {
createTestCards();
cy.signInAsNormalUser();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
testCardSearchForNormalUser({ tabs });
});
@@ -449,6 +460,7 @@ describe("scenarios > organization > entity picker", () => {
cy.signIn("nocollection");
H.startNewQuestion();
H.miniPickerBrowseAll().click();
testCardSearchForInaccessibleRootCollection({
tabs,
isRootSelected: true,
@@ -459,7 +471,8 @@ describe("scenarios > organization > entity picker", () => {
cy.signInAsAdmin();
createTestCards();
H.startNewQuestion();
testCardSearchForAllPersonalCollections({ tabs });
H.miniPickerBrowseAll().click();
testCardSearchForAllPersonalCollections();
});
});
});
@@ -545,7 +558,7 @@ describe("scenarios > organization > entity picker", () => {
cy.signInAsAdmin();
createTestCards();
selectQuestionFromDashboard();
testCardSearchForAllPersonalCollections({ tabs });
testCardSearchForAllPersonalCollections();
});
});
@@ -1083,7 +1096,9 @@ describe("scenarios > organization > entity picker", () => {
);
H.newButton("Question").click();
H.entityPickerModalLevel(2).findByText("People").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalLevel(3).findByText("People").click();
H.queryBuilderHeader().findByRole("button", { name: "Save" }).click();
H.modal()
@@ -1320,7 +1335,7 @@ function enterSearchText({
placeholder,
}: {
text: string;
placeholder: string;
placeholder: string | RegExp;
}) {
cy.findByPlaceholderText(placeholder).clear().type(text);
}
@@ -1378,7 +1393,9 @@ function testCardSearchForNormalUser({ tabs }: { tabs: string[] }) {
tabs.forEach((tab) => {
cy.log("root collection - automatically selected");
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tab).click();
if (tabs.length > 1) {
H.entityPickerModalTab(tab).click();
}
enterSearchText({
text: "2",
placeholder: "Search this collection or everywhere…",
@@ -1478,7 +1495,6 @@ function testCardSearchForInaccessibleRootCollection({
if (isRootSelected) {
cy.log("inaccessible root collection - automatically selected");
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tab).click();
enterSearchText({
text: "1",
placeholder: "Search this collection or everywhere…",
@@ -1569,20 +1585,17 @@ function testCardSearchForInaccessibleRootCollection({
});
}
function testCardSearchForAllPersonalCollections({ tabs }: { tabs: string[] }) {
tabs.forEach((tab) => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab(tab).click();
cy.findByText("All personal collections").click();
enterSearchText({
text: "root",
placeholder: "Search…",
});
globalSearchTab().should("not.exist");
localSearchTab("All personal collections").should("not.exist");
assertSearchResults({
foundItems: ["Root question 1", "Root model 1", "Root metric 1"],
});
function testCardSearchForAllPersonalCollections() {
H.entityPickerModal().within(() => {
cy.findByText("All personal collections").click();
enterSearchText({
text: "root",
placeholder: "Search…",
});
globalSearchTab().should("not.exist");
localSearchTab("All personal collections").should("not.exist");
assertSearchResults({
foundItems: ["Root question 1", "Root model 1", "Root metric 1"],
});
});
}

View File

@@ -31,7 +31,7 @@ export const createSimpleQuestion = (name: string) =>
export const verifyBookmarksOrder = (expectedOrder: string[]) => {
navigationSidebar()
.findByLabelText(/Bookmarks/)
.findByRole("section", { name: "Bookmarks" })
.within(() => {
cy.get("li")
.should("have.length", expectedOrder.length)
@@ -51,7 +51,7 @@ export const moveBookmark = (
) => {
moveDnDKitElement(
navigationSidebar()
.findByLabelText(/Bookmarks/)
.findByRole("section", { name: "Bookmarks" })
.findByText(name),
{ vertical: verticalDistance },
);

View File

@@ -962,8 +962,8 @@ describe("admin > permissions > sandboxes (tested via the API)", () => {
createJoinedQuestion("14766_joined");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("14766_joined").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage

View File

@@ -129,9 +129,10 @@ describe("issue 9027", () => {
cy.signInAsAdmin();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText("Orders").should("exist");
H.entityPickerModalItem(0, "Our analytics").click();
H.entityPickerModalItem(1, "Orders").should("exist");
cy.button("Close").click();
});
@@ -159,8 +160,9 @@ describe("issue 9027", () => {
function goToSavedQuestionPickerAndAssertQuestion(questionName, exists = true) {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText(questionName).should(exists ? "exist" : "not.exist");
cy.button("Close").click();
});
@@ -589,8 +591,8 @@ describe("issue 17514", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
@@ -955,6 +957,7 @@ describe("issue 19341", () => {
it("should correctly disable nested queries (metabase#19341)", () => {
// Test "Saved Questions" table is hidden in QB data selector
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByText("Orders").should("exist");
@@ -981,6 +984,7 @@ describe("issue 19341", () => {
});
cy.icon("join_left_outer").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().findAllByRole("tab").should("not.exist");
// Test "Explore results" button is hidden for native questions
@@ -1007,8 +1011,9 @@ describe("issue 19742", () => {
cy.findByText("New").click();
H.popover().findByText("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("Orders").should("exist");
cy.button("Close").click();
});
@@ -1027,8 +1032,9 @@ describe("issue 19742", () => {
cy.findByText("New").click();
H.popover().findByText("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("Orders").should("not.exist");
cy.findByText("Products").should("exist");
@@ -1182,8 +1188,8 @@ describe("issue 20627", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText(newTableName).click();
});

View File

@@ -153,61 +153,6 @@ describe("issue 25016", () => {
});
});
// this is only testable in OSS because EE always has models from auditv2
describe("issue 25144", { tags: "@OSS" }, () => {
beforeEach(() => {
H.restore("setup");
cy.signInAsAdmin();
cy.intercept("POST", "/api/card").as("createCard");
cy.intercept("PUT", "/api/card/*").as("updateCard");
});
it("should show Saved Questions tab after creating the first question (metabase#25144)", () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
cy.findByText("Collections").should("not.exist");
H.entityPickerModalItem(2, "Orders").click();
});
H.saveQuestion("Orders question");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should("be.visible").click();
H.entityPickerModalItem(1, "Orders question").should("be.visible");
});
});
it("should show Models tab after creation the first model (metabase#24878)", () => {
cy.visit("/model/new");
cy.findByTestId("new-model-options")
.findByText(/use the notebook/i)
.click();
H.entityPickerModal().within(() => {
H.entityPickerModalItem(2, "Orders").click();
});
cy.findByTestId("dataset-edit-bar").button("Save").click();
H.modal().within(() => {
cy.findByLabelText("Name").clear().type("Orders model");
cy.button("Save").click();
});
cy.wait("@createCard");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should("be.visible").click();
H.entityPickerModalItem(1, "Orders model").should("be.visible");
});
});
});
describe("issue 27104", () => {
const questionDetails = {
dataset_query: {
@@ -528,48 +473,6 @@ describe("issue 30610", () => {
});
});
describe("issue 36669", () => {
beforeEach(() => {
H.restore();
cy.signInAsNormalUser();
cy.intercept("GET", "/api/search*").as("search");
});
it("should be able to change question data source to raw data after selecting saved question (metabase#36669)", () => {
const questionDetails = {
name: "Orders 36669",
query: {
"source-table": ORDERS_ID,
limit: 5,
},
};
H.createQuestion(questionDetails).then(() => {
H.startNewQuestion();
});
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByPlaceholderText("Search this collection or everywhere…").type(
"Orders 36669",
);
cy.wait("@search");
cy.findByText("Everywhere").click();
cy.findByRole("tabpanel").findByText("Orders 36669").click();
});
H.getNotebookStep("data").findByText("Orders 36669").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.log("verify Tables are listed");
cy.findByRole("tabpanel").should("contain", "Orders");
});
});
});
describe("issue 35290", () => {
beforeEach(() => {
H.restore();
@@ -641,9 +544,8 @@ describe("issue 43216", () => {
cy.log("Create target question");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.waitForLoaderToBeRemoved();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Source question").click();
});
H.saveQuestion("Target question");
@@ -690,8 +592,9 @@ function removeSourceColumns() {
function createAdHocQuestion(questionName) {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText("Our analytics").click();
cy.findByText(questionName).click();
});
cy.findByTestId("fields-picker").click();

View File

@@ -263,7 +263,8 @@ describe("issue 38354", { tags: "@external" }, () => {
it("should be possible to change source database (metabase#38354)", () => {
H.openNotebook();
H.getNotebookStep("data").findByTestId("data-step-cell").click();
H.entityPickerModal().within(() => {
H.miniPicker().within(() => {
H.miniPickerHeader().click();
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -681,62 +682,6 @@ describe("issue 42244", () => {
});
});
describe("issue 42957", () => {
beforeEach(() => {
H.restore();
cy.signInAsAdmin();
});
it("does not show collections that contain models from different tabs (metabase#42957)", () => {
H.createQuestion({
name: "Model",
type: "model",
query: {
"source-table": ORDERS_ID,
},
});
H.createCollection({ name: "Collection without models" }).then(
({ body: collection }) => {
cy.wrap(collection.id).as("collectionId");
},
);
cy.get("@collectionId").then((collectionId) => {
H.createQuestion({
name: "Question",
type: "question",
query: {
"source-table": ORDERS_ID,
},
collection_id: collectionId,
});
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
// wait for data to load
H.entityPickerModalLevel(1).should("contain", "Orders, Count");
// open filter
cy.findByRole("button", { name: /Filter/ }).click();
});
H.popover().findByLabelText("Saved questions").click();
H.entityPickerModal().within(() => {
// close filter
cy.findByRole("button", { name: /Filter/ }).click();
H.entityPickerModalLevel(1).should(
"not.contain",
"Collection without models",
);
});
});
});
describe("issue 40064", () => {
beforeEach(() => {
H.restore();
@@ -890,13 +835,13 @@ describe("issue 32020", () => {
H.startNewQuestion();
cy.log("create joined question manually");
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(question1Details.name).click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(question2Details.name).click();
});
H.popover().findByText("ID").click();
@@ -944,8 +889,8 @@ describe("issue 44071", () => {
it("should be able to save questions based on another questions without collection access (metabase#44071)", () => {
cy.visit("/");
H.newButton("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
cy.findByText(/Personal Collection/).click();
cy.findByText(questionDetails.name).click();
});
@@ -1425,21 +1370,22 @@ describe("issue 19894", () => {
H.startNewQuestion();
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(1, "Q1").click();
H.miniPicker().should("exist");
cy.realType("Q1");
H.miniPicker().findByText("Q1").click();
cy.button("Join data").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(1, "Q2").click();
cy.realType("Q2");
H.miniPicker().findByText("Q2").click();
H.popover().findByText("Category").click();
H.popover().findByText("Category").click();
cy.button("Join data").click();
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(1, "Q3").click();
cy.realType("Q3");
H.miniPicker().findByText("Q3").click();
H.popover().findByText("Category").should("be.visible");
H.popover().findByText("Count").should("be.visible");
@@ -1600,18 +1546,24 @@ describe("issue 44974", { tags: "@external" }, () => {
},
};
H.createQuestion(questionDetails, {
// Visit question to put it in recents
visitQuestion: true,
});
H.createQuestion(questionDetails);
H.openOrdersTable({ mode: "notebook" });
H.join();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders Model").should("be.visible");
cy.findByText(questionDetails.name).should("not.exist");
});
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Recents").should("not.exist");
H.entityPickerModalTab("Collections").click();
cy.findAllByRole("tab").should("not.exist");
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText("Orders Model").should("be.visible");
cy.findByText(questionDetails.name).should("not.exist");
cy.button("Close").click();
});
});
});
@@ -2668,7 +2620,8 @@ describe("issue 23449", () => {
H.navigationSidebar().findByLabelText("Browse models").click();
cy.findByLabelText("Create a new model").click();
cy.findByRole("link", { name: /Use the notebook editor/ }).click();
H.entityPickerModal().findByText("Reviews").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Reviews").click();
cy.log("Remove Body column");
cy.findByLabelText("Pick columns").click();
@@ -2948,8 +2901,8 @@ describe("Issue 33835", { tags: "@external" }, () => {
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "orders" });
H.startNewQuestion();
H.entityPickerModal().findByText("Writable Postgres12").click();
H.entityPickerModal().findByText("Orders").click();
H.miniPicker().findByText("Writable Postgres12").click();
H.miniPicker().findByText("Orders").click();
H.getNotebookStep("summarize")
.findByText("Pick a function or metric")

View File

@@ -43,8 +43,8 @@ describe("issue 48754", () => {
cy.findByText("Category").click();
});
H.getNotebookStep("summarize").button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Q1").click();
});
H.popover()
@@ -563,8 +563,8 @@ describe("issue 53404", () => {
H.visitQuestion(ORDERS_QUESTION_ID);
H.openNotebook();
H.getNotebookStep("data").button("Join data").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders").click();
});
H.popover().findByText("ID").click();
@@ -667,8 +667,8 @@ describe("issue 46845", () => {
cy.log("add a self-join");
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.popover().findByText("Product ID").click();
@@ -793,8 +793,8 @@ describe("issue 58829", () => {
cy.log("verify it can be joined");
H.openProductsTable({ mode: "notebook" });
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("M1").click();
});
H.popover().findByText("ID").click();
@@ -906,8 +906,8 @@ describe("issue 54920", () => {
cy.log("create a derived question and verify that it can be run");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Base question").click();
});
H.getNotebookStep("filter")
@@ -948,8 +948,8 @@ describe("issue 55631", () => {
cy.signInAsAdmin();
H.startNewQuestion();
H.entityPickerModal().within(() => {
cy.findByText("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
cy.intercept("POST", "/api/card").as("cardCreate");
@@ -1016,13 +1016,13 @@ describe("issue 39033", () => {
H.createNativeQuestion(question1Details);
H.createNativeQuestion(question2Details);
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(question1Name).click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(question2Name).click();
});
H.popover().findByText("PRODUCT_ID").click();
@@ -1061,13 +1061,13 @@ describe("issue 56416", () => {
it("should be able to use a column from a joined question with a long name (metabase#56416)", () => {
cy.log("create the joined question");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});
H.getNotebookStep("summarize")
@@ -1090,13 +1090,13 @@ describe("issue 56416", () => {
cy.log("create the main question");
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText(joinedQuestionName).click();
});
H.popover().findByText("Product ID").click();

View File

@@ -295,8 +295,8 @@ describe("scenarios > question > multiple column breakouts", () => {
bucket2Name: string;
}) {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText(tableName).click();
});
H.getNotebookStep("summarize")

View File

@@ -384,8 +384,8 @@ describe("scenarios > question > nested", () => {
cy.findByText("New").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Question").should("be.visible").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("15725").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage

View File

@@ -27,8 +27,7 @@ describe("scenarios > question > new", () => {
}
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample3").should("be.visible");
});
});
@@ -39,12 +38,10 @@ describe("scenarios > question > new", () => {
);
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.tabsShouldBe("Tables", ["Tables", "Collections"]);
H.entityPickerModalTab("Search").should("not.exist");
H.entityPickerModalTab("Collections").click();
cy.get("[role='tab']:contains('Search')").should("not.exist");
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByPlaceholderText("Search this collection or everywhere…")
.type(" ")
@@ -57,6 +54,7 @@ describe("scenarios > question > new", () => {
// should not trigger search for an empty string
cy.get("@searchQuery").should("have.been.calledOnce");
cy.get("[role='tab']:contains('Search')").should("be.visible");
cy.findAllByTestId("result-item").should("not.exist");
cy.findByText("Everywhere").click();
cy.findAllByTestId("result-item").should("have.length.at.least", 4);
@@ -76,8 +74,7 @@ describe("scenarios > question > new", () => {
cy.findByPlaceholderText("Search this collection or everywhere…")
.clear()
.blur();
H.entityPickerModalTab("Search").should("not.exist");
H.tabsShouldBe("Collections", ["Tables", "Collections"]);
cy.get("[role='tab']:contains('Search')").should("not.exist");
cy.findByText("Orders, Count").click();
});
@@ -94,17 +91,19 @@ describe("scenarios > question > new", () => {
H.notebookButton().should("be.visible");
cy.findByTestId("data-step-cell").contains("Orders, Count").click();
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
// It is now possible to choose another saved question
H.entityPickerModalTab("Collections").should(
H.entityPickerModalItem(0, "Our analytics").should(
"have.attr",
"aria-selected",
"data-active",
"true",
);
cy.findByText("Orders").should("exist");
cy.findByText("Orders, Count").should("exist");
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(0, "Databases").click();
cy.findByText("Products").click();
});
cy.findByTestId("data-step-cell").contains("Products");
@@ -128,8 +127,8 @@ describe("scenarios > question > new", () => {
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
// Note: collection name's first letter is capitalized
cy.findByText(/foo:bar/i).click();
cy.findByText("Orders");
@@ -143,10 +142,9 @@ describe("scenarios > question > new", () => {
});
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
assertDataPickerEntitySelected(0, "Our analytics");
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText("First collection").should("exist");
cy.findByText("Second collection").should("not.exist");
cy.findByText("Third collection").should("not.exist");
@@ -169,15 +167,18 @@ describe("scenarios > question > new", () => {
cy.signOut();
cy.signIn("nocollection");
H.startNewQuestion();
H.entityPickerModal().findByText("Orders").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, "Orders").click();
H.visualize();
H.saveQuestion("Personal question");
cy.signOut();
cy.signInAsAdmin();
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(0, "Our analytics").click();
cy.findByText("All personal collections").click();
cy.findByText(H.getPersonalCollectionName(USERS.nocollection)).click();
cy.findByText("Personal question").click();
@@ -252,9 +253,10 @@ describe("scenarios > question > new", () => {
H.popover().findByText("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Orders").click();
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, "Orders").click();
});
cy.log(
@@ -262,7 +264,8 @@ describe("scenarios > question > new", () => {
);
cy.findByRole("button", { name: /Orders/ }).click();
H.shouldDisplayTabs(["Recents", "Tables", "Collections"]);
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModalTab("Recents").click();
cy.findByRole("dialog", { name: "Pick your starting data" })
.findByRole("button", { name: /Orders/ })
@@ -360,8 +363,8 @@ describe("scenarios > question > new", () => {
cy.findByLabelText("Navigation bar").findByText("New").click();
H.popover().findByText("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Orders").click();
});
cy.findByTestId("qb-header").findByText("Save").click();
@@ -411,8 +414,9 @@ describe("scenarios > question > new", () => {
cy.findByLabelText("Navigation bar").findByText("New").click();
H.popover().findByText("Question").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Orders").click();
});
cy.findByTestId("qb-header").findByText("Save").click();
@@ -510,8 +514,8 @@ describe("scenarios > question > new", () => {
});
it("should hide public collections when selecting a dashboard for a question in a personal collection", () => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -543,8 +547,8 @@ describe("scenarios > question > new", () => {
});
it("should show all collections when selecting a dashboard for a question in a public collection", () => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -579,8 +583,8 @@ describe("scenarios > question > new", () => {
describe("creating a new dashboard", () => {
beforeEach(() => {
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -605,6 +609,7 @@ describe("scenarios > question > new", () => {
});
it("when selecting a collection", () => {
// H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Dashboards").click();
H.entityPickerModalItem(1, "Collection in root collection").click();
@@ -712,11 +717,10 @@ describe(
it("can create a question from the sample database", () => {
cy.visit("/question/new");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should("be.visible");
H.entityPickerModalTab("Tables").click();
H.entityPickerModalItem(2, "Products").click();
H.entityPickerModalLevel(0).findByText("Databases").click();
H.entityPickerModalItem(3, "Products").click();
});
// strange: we get different behavior when we go to question/new
@@ -730,8 +734,8 @@ describe(
it("can create a question from a saved question", () => {
cy.visit("/question/new");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(1, "Orders").click();
});
@@ -754,20 +758,19 @@ describe(
cy.visit("/question/notebook");
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.tabsShouldBe("Tables", ["Tables", "Collections"]);
H.entityPickerModalTab("Collections").should("be.visible");
H.entityPickerModalTab("Tables").should("be.visible");
H.entityPickerModalTab("Collections").click();
H.entityPickerModalItem(1, "Orders Model").click();
});
cy.wait("@recents");
cy.button(/Orders Model/).click();
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.tabsShouldBe("Collections", ["Recents", "Tables", "Collections"]);
H.tabsShouldBe("Collections", ["Recents", "Data"]);
H.entityPickerModalTab("Recents").click();
cy.findByTestId("result-item").should("contain.text", "Orders Model");
});

View File

@@ -25,63 +25,35 @@ describe("scenarios > notebook > data source", () => {
cy.visit("/");
cy.findByTestId("app-bar").findByText("New").click();
H.popover().findByTextEnsureVisible("Question").click();
cy.findByTestId("data-step-cell").should(
"have.text",
"Pick your starting data",
cy.findByPlaceholderText("Search for tables and more...").should(
"exist",
);
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
cy.log("Should not have Recents tab");
cy.findAllByRole("tab").should("have.length", 0);
cy.findAllByRole("tab").should("not.exist");
H.entityPickerModalLevel(0).should("not.exist");
H.entityPickerModalLevel(1).should("not.exist");
H.entityPickerModalLevel(2)
.get("[data-index]")
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalLevel(0)
.findByText("Sample Database")
.should("not.exist");
H.entityPickerModalLevel(3)
.find("[data-index]")
.should("have.length", 8);
assertDataPickerEntityNotSelected(2, "Accounts");
assertDataPickerEntityNotSelected(2, "Analytic Events");
assertDataPickerEntityNotSelected(2, "Feedback");
assertDataPickerEntityNotSelected(2, "Invoices");
assertDataPickerEntityNotSelected(2, "Orders");
assertDataPickerEntityNotSelected(2, "People");
assertDataPickerEntityNotSelected(2, "Products");
assertDataPickerEntityNotSelected(2, "Reviews");
assertDataPickerEntityNotSelected(3, "Accounts");
assertDataPickerEntityNotSelected(3, "Analytic Events");
assertDataPickerEntityNotSelected(3, "Feedback");
assertDataPickerEntityNotSelected(3, "Invoices");
assertDataPickerEntityNotSelected(3, "Orders");
assertDataPickerEntityNotSelected(3, "People");
assertDataPickerEntityNotSelected(3, "Products");
assertDataPickerEntityNotSelected(3, "Reviews");
});
},
);
it("should not show saved questions if only models exist (metabase#25142)", () => {
H.createQuestion({
name: "GUI Model",
query: { "source-table": REVIEWS_ID, limit: 1 },
display: "table",
type: "model",
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
cy.findAllByRole("tab").should("have.length", 2);
H.entityPickerModalTab("Tables").should("exist");
H.entityPickerModalTab("Collections").should("exist");
});
});
it("should not show models if only saved questions exist", () => {
H.createQuestion({
name: "GUI Question",
query: { "source-table": REVIEWS_ID, limit: 1 },
display: "table",
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.shouldDisplayTabs(["Tables", "Collections"]);
});
});
it("should include dashboard questions when computing which tabs to show (metabase#56887)", () => {
it("should include dashboard questions (metabase#56887)", () => {
const QUESTION_NAME = "Find me";
H.createDashboard({
@@ -101,10 +73,7 @@ describe("scenarios > notebook > data source", () => {
H.startNewQuestion();
H.entityPickerModal().should("exist");
H.tabsShouldBe("Tables", ["Tables", "Collections"]);
H.entityPickerModalTab("Collections").click();
H.miniPickerBrowseAll().click();
H.entityPickerModalItem(1, "Test Dashboard").click();
H.entityPickerModalItem(2, QUESTION_NAME).should("exist");
});
@@ -120,13 +89,15 @@ describe("scenarios > notebook > data source", () => {
H.openReviewsTable();
H.openNotebook();
cy.findByTestId("data-step-cell").should("have.text", "Reviews").click();
H.miniPickerHeader().should("contain", "Sample Database");
H.miniPicker().findByText("Reviews");
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.tabsShouldBe("Tables", ["Tables", "Collections"]);
// should not show databases step if there's only 1 database
H.entityPickerModalLevel(0).should("not.exist");
// should not show schema step if there's only 1 schema
H.entityPickerModalLevel(1).should("not.exist");
assertDataPickerEntitySelected(2, "Reviews");
H.entityPickerModalLevel(2).should("not.exist");
assertDataPickerEntitySelected(3, "Reviews");
});
});
@@ -134,13 +105,15 @@ describe("scenarios > notebook > data source", () => {
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
H.openNotebook();
cy.findByTestId("data-step-cell").should("have.text", "Orders").click();
H.miniPickerHeader().should("contain", "Sample Database");
H.miniPicker().findByText("Orders");
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.tabsShouldBe("Tables", ["Tables", "Collections"]);
// should not show databases step if there's only 1 database
H.entityPickerModalLevel(0).should("not.exist");
// should not show schema step if there's only 1 schema
H.entityPickerModalLevel(1).should("not.exist");
assertDataPickerEntitySelected(2, "Orders");
H.entityPickerModalLevel(2).should("not.exist");
assertDataPickerEntitySelected(3, "Orders");
});
});
@@ -167,8 +140,7 @@ describe("scenarios > notebook > data source", () => {
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(dbName).click();
cy.findByText(schemaName).click();
cy.findByText(tableName).click();
@@ -178,28 +150,23 @@ describe("scenarios > notebook > data source", () => {
H.openNotebook();
cy.findByTestId("data-step-cell").should("contain", tableName).click();
H.miniPickerHeader().should("contain", schemaName);
H.miniPicker().findByText(tableName).should("exist");
cy.realType("a"); // start typing to expose mini-picker "Browse all" option
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
assertDataPickerEntitySelected(0, dbName);
assertDataPickerEntitySelected(1, schemaName);
assertDataPickerEntitySelected(2, tableName);
H.entityPickerModalTab("Recents").click();
cy.contains("button", "Animals")
.should("exist")
.and("contain.text", tableName)
.and("have.attr", "aria-selected", "true");
H.entityPickerModalTab("Tables").click();
cy.findByText(dbName).click();
cy.findByText(schemaName).click();
assertDataPickerEntitySelected(1, dbName);
assertDataPickerEntitySelected(2, schemaName);
assertDataPickerEntitySelected(3, tableName);
cy.findByText(tableName).click();
});
cy.log("select a table from the second schema");
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Public").click();
H.miniPicker().within(() => {
cy.findByText(dbName).click();
cy.findByText("public").click();
cy.findByText("Many Data Types").click();
});
H.popover().findByText("Name").click();
@@ -207,8 +174,8 @@ describe("scenarios > notebook > data source", () => {
cy.log("select a table from the third schema");
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(dbName).click();
cy.findByText("Domestic").click();
cy.findByText("Animals").click();
});
@@ -221,17 +188,9 @@ describe("scenarios > notebook > data source", () => {
cy.visit(`/model/${ORDERS_MODEL_ID}/query`);
cy.findByTestId("data-step-cell").should("have.text", "Orders").click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").should(
"have.attr",
"aria-selected",
"true",
);
// should not show databases step if there's only 1 database
H.entityPickerModalLevel(0).should("not.exist");
// should not show schema step if there's only 1 schema
H.entityPickerModalLevel(1).should("not.exist");
assertDataPickerEntitySelected(2, "Orders");
H.miniPicker().within(() => {
H.miniPickerHeader().should("contain", "Sample Database");
cy.findByText("Orders").should("exist");
});
});
});
@@ -257,14 +216,7 @@ describe("scenarios > notebook > data source", () => {
.should("have.text", modelDetails.name)
.click();
H.entityPickerModal().within(() => {
H.shouldDisplayTabs(["Tables", "Collections"]);
assertDataPickerEntitySelected(0, "Our analytics");
assertDataPickerEntitySelected(1, "First collection");
assertDataPickerEntitySelected(2, "Second collection");
assertDataPickerEntitySelected(3, checkNotNull(modelDetails.name));
H.miniPicker().within(() => {
cy.findByText(checkNotNull(modelDetails.name))
.should("exist")
.and("contain.text", checkNotNull(modelDetails.name));
@@ -276,12 +228,12 @@ describe("scenarios > notebook > data source", () => {
H.openNotebook();
openDataSelector();
H.miniPickerHeader().should("contain", "Our analytics");
H.miniPicker().findByText("Orders Model").should("exist");
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should(
"have.attr",
"aria-selected",
"true",
);
assertDataPickerEntitySelected(0, "Our analytics");
assertDataPickerEntitySelected(1, "Orders Model");
@@ -291,12 +243,14 @@ describe("scenarios > notebook > data source", () => {
moveToCollection("First collection");
openDataSelector();
H.miniPickerHeader().should("contain", "First collection");
H.miniPicker().findByText("Orders Model").should("exist");
H.miniPickerHeader().click();
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should(
"have.attr",
"aria-selected",
"true",
);
assertDataPickerEntitySelected(0, "Our analytics");
assertDataPickerEntitySelected(1, "First collection");
assertDataPickerEntitySelected(2, "Orders Model");
@@ -326,12 +280,12 @@ describe("scenarios > notebook > data source", () => {
H.visitQuestion("@nestedQuestionId");
H.openNotebook();
openDataSelector();
H.miniPickerHeader().should("contain", "Our analytics");
H.miniPicker().findByText(sourceQuestionName).should("exist");
H.miniPickerHeader().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should(
"have.attr",
"aria-selected",
"true",
);
assertDataPickerEntitySelected(0, "Our analytics");
assertDataPickerEntitySelected(1, sourceQuestionName);
@@ -348,12 +302,12 @@ describe("scenarios > notebook > data source", () => {
H.openNotebook();
openDataSelector();
H.miniPickerHeader().should("contain", "First collection");
H.miniPicker().findByText(sourceQuestionName).should("exist");
H.miniPickerHeader().click().click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").should(
"have.attr",
"aria-selected",
"true",
);
assertDataPickerEntitySelected(0, "Our analytics");
assertDataPickerEntitySelected(1, "First collection");
assertDataPickerEntitySelected(2, sourceQuestionName);
@@ -362,28 +316,6 @@ describe("scenarios > notebook > data source", () => {
});
});
describe("scenarios > notebook > data source", { tags: "@OSS" }, () => {
beforeEach(() => {
H.restore("setup");
cy.signInAsAdmin();
});
it("should not show saved questions if only models exist (metabase#25142)", () => {
H.createQuestion({
name: "GUI Model",
query: { "source-table": REVIEWS_ID, limit: 1 },
display: "table",
type: "model",
});
H.startNewQuestion();
H.entityPickerModal().within(() => {
cy.findAllByRole("tab").should("have.length", 2);
H.entityPickerModalTab("Tables").should("be.visible");
H.entityPickerModalTab("Collections").should("be.visible");
});
});
});
describe("issue 34350", { tags: "@external" }, () => {
beforeEach(() => {
H.restore("postgres-12");
@@ -393,7 +325,8 @@ describe("issue 34350", { tags: "@external" }, () => {
it("works after changing question's source table to a one from a different database (metabase#34350)", () => {
H.openOrdersTable({ mode: "notebook" });
openDataSelector();
H.entityPickerModal().within(() => {
H.miniPicker().within(() => {
H.miniPickerHeader().click();
cy.findByText("QA Postgres12").click();
cy.findByText("Orders").click();
});
@@ -428,15 +361,16 @@ describe("issue 28106", () => {
H.startNewQuestion();
cy.wait(["@getRootCollection", "@getTree"]);
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("Databases").click();
cy.findByText("Writable Postgres12").click();
H.entityPickerModalLevel(1)
H.entityPickerModalLevel(2)
.findByTestId("scroll-container")
.as("schemasList");
H.entityPickerModalLevel(2).should("contain", "Animals");
H.entityPickerModalLevel(3).should("contain", "Animals");
cy.get("@schemasList").scrollTo("bottom");
@@ -459,8 +393,7 @@ describe("issue 28106", () => {
);
});
// Needs to be OSS because EE will always have models due to instance analytics
describe("issue 32252", { tags: "@OSS" }, () => {
describe("issue 32252", () => {
beforeEach(() => {
H.restore("setup");
cy.signInAsAdmin();
@@ -486,28 +419,26 @@ describe("issue 32252", { tags: "@OSS" }, () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByText("Recents").should("not.exist");
cy.findByText("Collections").should("be.visible");
cy.button("Close").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("My collection").click();
cy.findByText("My question").should("exist");
});
cy.findByTestId("sidebar-toggle").click();
H.navigationSidebar().findByText("Our analytics").click();
cy.button("Actions").click();
cy.findAllByRole("button", { name: "Actions" }).eq(0).click();
H.popover().findByText("Move to trash").click();
cy.findByTestId("toast-undo")
.findByText("Trashed collection")
.should("be.visible");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByText("Recents").should("not.exist");
cy.findByText("Collections").should("not.exist");
cy.findByText("Orders").should("be.visible");
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("My collection").should("not.exist");
cy.findByText("My question").should("not.exist");
});
});
@@ -515,17 +446,18 @@ describe("issue 32252", { tags: "@OSS" }, () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByText("Recents").should("not.exist");
cy.findByText("Collections").should("be.visible");
cy.button("Close").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("My collection").click();
cy.findByText("My question").should("exist");
});
cy.findByTestId("sidebar-toggle").click();
H.navigationSidebar().findByText("Our analytics").click();
cy.findByTestId("collection-entry-name").click();
cy.findAllByTestId("collection-entry-name")
.findByText("My collection")
.click();
cy.button("Actions").click();
H.popover().findByText("Move to trash").click();
cy.findByTestId("toast-undo")
@@ -533,11 +465,10 @@ describe("issue 32252", { tags: "@OSS" }, () => {
.should("be.visible");
H.newButton("Question").click();
H.entityPickerModal().within(() => {
cy.findByTestId("loading-indicator").should("not.exist");
cy.findByText("Recents").should("not.exist");
cy.findByText("Collections").should("not.exist");
cy.findByText("Orders").should("be.visible");
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("My collection").should("not.exist");
cy.findByText("My question").should("not.exist");
});
});
});

View File

@@ -52,10 +52,8 @@ describe("scenarios > notebook > link to data source", () => {
cy.log("Normal click on the data source still opens the entity picker");
H.getNotebookStep("data").findByText("Reviews").click();
cy.findByTestId("entity-picker-modal").within(() => {
cy.findByText("Pick your starting data").should("be.visible");
cy.findByLabelText("Close").click();
});
H.miniPicker().should("exist");
H.miniPicker().findByText("Reviews").click(); // close miniPicker
cy.log("Meta/Ctrl click on the fields picker behaves as a regular click");
H.getNotebookStep("data").findByTestId("fields-picker").click(clickConfig);
@@ -427,12 +425,11 @@ describe("scenarios > notebook > link to data source", () => {
"Even if user opens the notebook link directly, they should not see the source question. We open the entity picker instead",
);
cy.visit(`/question/${nestedQuestion.id}/notebook`);
cy.findByTestId("entity-picker-modal").within(() => {
cy.findByText("Pick your starting data").should("be.visible");
cy.findByLabelText("Close").click();
});
H.getNotebookStep("data").should("contain", "Pick your starting data");
H.getNotebookStep("data")
.findByPlaceholderText("Search for tables and more...")
.should("exist");
H.miniPicker().should("be.visible");
cy.log(
"The same should be true for a user that additionally doesn't have write query permissions",
@@ -517,8 +514,8 @@ describe("scenarios > notebook > link to data source", () => {
H.openProductsTable({ 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("Orders").click();
});

View File

@@ -28,9 +28,9 @@ describe("scenarios > question > notebook > native query preview sidebar", () =>
cy.findByTestId("app-bar").findByLabelText("New").click();
H.popover().findByTextEnsureVisible("Question").click();
cy.findByTestId("data-step-cell").should(
"have.text",
"Pick your starting data",
H.miniPickerBrowseAll().click();
cy.findByPlaceholderText("Search for tables and more...").should(
"be.visible",
);
H.entityPickerModal().button("Close").click();
@@ -328,8 +328,7 @@ describe(
it("should work for both simple and nested questions based on previously converted GUI query", () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText(MONGO_DB_NAME).click();
cy.findByText("Products").click();
});

View File

@@ -234,9 +234,9 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
cy.signInAsAdmin();
cy.viewport(1280, 720);
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();
});
});
@@ -467,8 +467,8 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
H.openNotebook();
H.join();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Products model").click();
});
@@ -477,8 +477,8 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
it('should not show "median" aggregation option for databases that do not support "percentile-aggregations" driver feature', () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -580,12 +580,14 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
H.getNotebookStep("data")
.as("dataStep")
.within(() => {
cy.findByText("Pick your starting data").should("exist");
cy.findByPlaceholderText("Search for tables and more...").should(
"exist",
);
cy.icon("play").should("not.be.visible");
});
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
@@ -900,7 +902,8 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
it("should not leave the UI in broken state after adding an aggregation (metabase#48358)", () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().findByText("Products").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Products").click();
H.addSummaryField({ metric: "Sum of ...", field: "Price" });
H.addSummaryGroupingField({ field: "Created At" });
H.addSummaryGroupingField({ field: "Category" });
@@ -976,7 +979,7 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
.findByText("Use the notebook editor")
.click();
H.entityPickerModal().should("be.visible");
H.miniPicker().should("be.visible");
// Cypress can emulate the browser's back button with cy.go('back'), but
// this does not trigger a confirmation modal, so we need to perform a
@@ -997,7 +1000,8 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
it("shows all available columns and groups in the breakout picker (metabase#46832)", () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().findByText("Orders").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Orders").click();
H.join();
H.joinTable("Reviews", "Product ID", "Product ID");
H.addSummaryField({ metric: "Count of rows" });
@@ -1047,7 +1051,8 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
it("should allow using aggregation functions inside expressions in aggregation (metabase#52611)", () => {
cy.visit("/");
H.newButton("Question").click();
H.entityPickerModal().findByText("Orders").click();
H.miniPicker().findByText("Sample Database").click();
H.miniPicker().findByText("Orders").click();
H.addSummaryField({ metric: "Custom Expression" });
H.enterCustomColumnDetails({
formula: "case(Sum([Total]) > 10, Sum([Total]), Sum([Subtotal]))",

View File

@@ -427,8 +427,8 @@ describe("scenarios > question > offset", () => {
const breakoutName = "Created At";
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
addCustomAggregation({
@@ -471,8 +471,8 @@ describe("scenarios > question > offset", () => {
];
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Orders").click();
});
addCustomAggregation({

View File

@@ -374,8 +374,8 @@ describe("scenarios > question > saved", () => {
.findByText("Use the notebook editor")
.click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
H.miniPicker().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Products").click();
});

View File

@@ -52,8 +52,8 @@ describe("scenarios > question > download", () => {
testCases.forEach((fileType) => {
it(`downloads ${fileType} file`, () => {
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.findByText("Orders, Count").click();
});

View File

@@ -242,9 +242,10 @@ describe("#39152 sharing an unsaved question", () => {
it("should ask the user to save the question before creating a public link", () => {
H.startNewQuestion();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText("People").click();
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, "People").click();
});
H.visualize();

View File

@@ -245,8 +245,8 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
});
// Build a new question off that grouping by City
H.startNewQuestion();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Collections").click();
H.miniPicker().within(() => {
cy.findByText("Our analytics").click();
cy.contains("CA People").click();
});

View File

@@ -15,9 +15,10 @@ describe("scenarios > visualizations > table", () => {
function joinTable(table) {
cy.findByText("Join data").click();
H.miniPickerBrowseAll().click();
H.entityPickerModal().within(() => {
H.entityPickerModalTab("Tables").click();
cy.findByText(table).click();
H.entityPickerModalItem(0, "Databases").click();
H.entityPickerModalItem(1, table).click();
});
}

View File

@@ -18,6 +18,7 @@
[metabase-enterprise.comments.api]
[metabase-enterprise.content-translation.routes]
[metabase-enterprise.content-verification.api.routes]
[metabase-enterprise.data-studio.api]
[metabase-enterprise.database-replication.api :as database-replication.api]
[metabase-enterprise.database-routing.api]
[metabase-enterprise.dependencies.api]
@@ -25,6 +26,7 @@
[metabase-enterprise.email.api]
[metabase-enterprise.embedding-hub.api]
[metabase-enterprise.gsheets.api :as gsheets.api]
[metabase-enterprise.library.api]
[metabase-enterprise.llm.api]
[metabase-enterprise.metabot-v3.api]
[metabase-enterprise.metabot-v3.tools.api]
@@ -57,6 +59,7 @@
:ai-entity-analysis (deferred-tru "AI Entity Analysis")
:collection-cleanup (deferred-tru "Collection Cleanup")
:content-translation (deferred-tru "Content translation")
:data-studio (deferred-tru "Data Studio")
:dependencies (deferred-tru "Dependency Tracking")
:documents (deferred-tru "Documents")
:embedding (deferred-tru "Embedding")
@@ -107,6 +110,7 @@
"/content-translation" (premium-handler metabase-enterprise.content-translation.routes/routes :content-translation)
"/cloud-add-ons" metabase-enterprise.cloud-add-ons.api/routes
"/comment" (premium-handler metabase-enterprise.comments.api/routes :documents)
"/data-studio" (premium-handler metabase-enterprise.data-studio.api/routes :data-studio)
"/database-replication" (-> database-replication.api/routes ;; database-replication requires all these features.
(premium-handler :attached-dwh)
(premium-handler :etl-connections)
@@ -120,6 +124,7 @@
"/gsheets" (-> gsheets.api/routes ;; gsheets requires both features.
(premium-handler :attached-dwh)
(premium-handler :etl-connections))
"/library" (premium-handler metabase-enterprise.library.api/routes :data-studio)
"/logs" (premium-handler 'metabase-enterprise.advanced-config.api.logs :audit-app)
"/metabot-tools" metabase-enterprise.metabot-v3.tools.api/routes
"/metabot-v3" (premium-handler metabase-enterprise.metabot-v3.api/routes :metabot-v3)

View File

@@ -0,0 +1,10 @@
(ns metabase-enterprise.data-studio.api
"`/api/ee/data-studio/` routes"
(:require
[metabase-enterprise.data-studio.api.table]
[metabase.api.util.handlers :as handlers]))
(def ^{:arglists '([request respond raise])} routes
"`/api/ee/data-studio` routes."
(handlers/route-map-handler
{"/table" metabase-enterprise.data-studio.api.table/routes}))

View File

@@ -0,0 +1,212 @@
(ns metabase-enterprise.data-studio.api.table
"/api/ee/data-studio/table endpoints for bulk table operations."
(:require
[clojure.string :as str]
[metabase.api.common :as api]
[metabase.api.macros :as api.macros]
[metabase.api.routes.common :refer [+auth]]
[metabase.collections.core :as collections]
[metabase.database-routing.core :as database-routing]
[metabase.driver.settings :as driver.settings]
[metabase.driver.util :as driver.u]
[metabase.events.core :as events]
[metabase.lib-be.core :as lib-be]
[metabase.lib.core :as lib]
[metabase.lib.metadata :as lib.metadata]
[metabase.queries.core :as queries]
[metabase.request.core :as request]
[metabase.sync.core :as sync]
[metabase.util :as u]
[metabase.util.jvm :as u.jvm]
[metabase.util.log :as log]
[metabase.util.malli :as mu]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]
[metabase.util.quick-task :as quick-task]
[metabase.warehouse-schema.models.table :as table]
[toucan2.core :as t2]
[toucan2.realize :as t2.realize]))
(set! *warn-on-reflection* true)
(mr/def ::table-selectors
[:map
;; disjunctive filters (e.g. db_id IN $database_ids OR id IN $table_ids)
[:database_ids {:optional true} [:sequential ms/PositiveInt]]
[:schema_ids {:optional true} [:sequential :string]]
[:table_ids {:optional true} [:sequential ms/PositiveInt]]])
(mu/defn ^:private table-selectors->filter
[{:keys [database_ids table_ids schema_ids]}]
(let [schema-expr (fn [s]
(let [[schema-db-id schema-name] (str/split s #"\:")]
[:and [:= :db_id (parse-long schema-db-id)] [:= :schema schema-name]]))]
(cond-> [:or false]
(seq database_ids) (conj [:in :db_id (sort database_ids)])
(seq table_ids) (conj [:in :id (sort table_ids)])
(seq schema_ids) (conj (into [:or] (map schema-expr) (sort schema_ids))))))
(mr/def ::data-layers
(into [:enum {:decode/string keyword}] table/data-layers))
(mr/def ::data-sources
(into [:enum {:decode/string keyword}] table/data-sources))
(mr/def ::data-authorities
(into [:enum {:decode/string keyword}] table/writable-data-authority-types))
(defn- sync-unhidden-tables
"Function to call on newly unhidden tables. Starts a thread to sync all tables. Groups tables by database to
efficiently sync tables from different databases."
[newly-unhidden]
(when (seq newly-unhidden)
(u.jvm/in-virtual-thread*
(fn []
(doseq [[db-id tables] (group-by :db_id newly-unhidden)]
(let [database (t2/select-one :model/Database db-id)]
;; it's okay to allow testing H2 connections during sync. We only want to disallow you from testing them for the
;; purposes of creating a new H2 database.
(if (binding [driver.settings/*allow-testing-h2-connections* true]
(driver.u/can-connect-with-details? (:engine database) (:details database)))
(doseq [table tables]
(log/info (u/format-color :green "Table '%s' is now visible. Resyncing." (:name table)))
(sync/sync-table! table))
(log/warn (u/format-color :red "Cannot connect to database '%s' in order to sync unhidden tables"
(:name database))))))))))
(defn- maybe-sync-unhidden-tables!
[existing-tables {:keys [data_layer] :as body}]
;; sync any tables that are changed from copper to something else
(sync-unhidden-tables (when (and (contains? body :data_layer) (not= :copper data_layer))
(filter #(= :copper (:data_layer %)) existing-tables))))
(api.macros/defendpoint :post "/edit"
"Bulk updating tables."
[_route-params
_query-params
body
:- [:merge
::table-selectors
[:map {:closed true}
[:data_authority {:optional true} [:maybe ::data-authorities]]
[:data_source {:optional true} [:maybe ::data-sources]]
[:data_layer {:optional true} [:maybe ::data-layers]]
[:entity_type {:optional true} [:maybe :string]]
[:owner_email {:optional true} [:maybe :string]]
[:owner_user_id {:optional true} [:maybe :int]]]]]
(api/check-superuser)
(let [where (table-selectors->filter (select-keys body [:database_ids :schema_ids :table_ids]))
set-ks [:data_authority
:data_source
:data_layer
:owner_email
:owner_user_id
:entity_type]
existing-tables (t2/select :model/Table {:where where})
table-ids (set (map :id existing-tables))
set-map (select-keys body set-ks)]
(when (seq set-map)
(t2/update! :model/Table [:in table-ids] set-map)
(maybe-sync-unhidden-tables! existing-tables set-map))
{}))
(defn- table->published-model
[{:keys [id name db_id] :as _table} creator-id collection-id]
{:name (format "Model based on %s" name)
:description (format "Base model for table %s " name)
:dataset_query (let [mp (lib-be/application-database-metadata-provider db_id)]
(lib/query mp (lib.metadata/table mp id)))
:type :model
:display :table
:visualization_settings {}
:creator_id creator-id
:collection_id collection-id
:published_table_id id})
(api.macros/defendpoint :post "/publish-model"
"Create a model for each of selected tables"
[_route-params
_query-params
{:keys [target_collection_id]
:as body}
:- [:merge
::table-selectors
[:map
[:target_collection_id [:maybe [:or pos-int? [:= "library"]]]]]]]
(api/check-superuser)
(let [target-collection (cond
(= "library" target_collection_id) (api/check-403 (collections/remote-synced-collection))
(nil? target_collection_id) nil
:else (api/check-404 (t2/select-one :model/Collection target_collection_id)))
where (table-selectors->filter (select-keys body [:database_ids :schema_ids :table_ids]))
created-models (t2/with-transaction [_conn]
(into []
(comp
(map t2.realize/realize)
(partition-all 20)
(mapcat (fn [batch]
(mapv (fn [table]
(queries/create-card! (table->published-model table api/*current-user-id* (:id target-collection)) @api/*current-user*))
batch))))
(t2/reducible-select :model/Table :active true {:where where})))]
{:created_count (count created-models)
:models created-models
:target_collection target-collection}))
(defn- sync-schema-async!
[table user-id]
(events/publish-event! :event/table-manual-sync {:object table :user-id user-id})
(quick-task/submit-task! #(database-routing/with-database-routing-off (sync/sync-table! table))))
(api.macros/defendpoint :post "/sync-schema"
"Batch version of /table/:id/sync_schema. Takes an abstract table selection as /table/edit does.
- Currently checks policy before returning (so you might receive a 4xx on e.g. AuthZ policy failure)
- The sync itself is however, asyncronous. This call may return before all tables synced."
[_
_
body :- ::table-selectors]
(api/check-superuser)
(let [tables (t2/select :model/Table {:where (table-selectors->filter body), :order-by [[:id]]})
db-ids (sort (set (map :db_id tables)))]
(doseq [database (t2/select :model/Database :id [:in db-ids])]
(try
(binding [driver.settings/*allow-testing-h2-connections* true]
(driver.u/can-connect-with-details? (:engine database) (:details database) :throw-exceptions))
nil
(catch Throwable e
(log/warn (u/format-color :red "Cannot connect to database '%s' in order to sync tables" (:name database)))
(throw (ex-info (ex-message e) {:status-code 422})))))
(doseq [table tables]
(sync-schema-async! table api/*current-user-id*))
{:status :ok}))
(api.macros/defendpoint :post "/rescan-values"
"Batch version of /table/:id/rescan_values. Takes an abstract table selection as /table/edit does."
[_
_
body :- ::table-selectors]
(api/check-superuser)
(let [tables (t2/select :model/Table {:where (table-selectors->filter body), :order-by [[:id]]})]
;; same permission skip as the single-table api, see comment in /:id/rescan_values
(doseq [table tables]
(events/publish-event! :event/table-manual-scan {:object table :user-id api/*current-user-id*})
(request/as-admin
(quick-task/submit-task! #(sync/update-field-values-for-table! table))))
{:status :ok}))
(api.macros/defendpoint :post "/discard-values"
"Batch version of /table/:id/discard_values. Takes an abstract table selection as /table/edit does."
[_
_
body :- ::table-selectors]
(api/check-superuser)
(let [tables (t2/select :model/Table {:where (table-selectors->filter body), :order-by [[:id]]})]
(let [field-ids-to-delete-q {:select [:id]
:from [(t2/table-name :model/Field)]
:where [:in :table_id (map :id tables)]}]
(t2/delete! (t2/table-name :model/FieldValues) :field_id [:in field-ids-to-delete-q]))
{:status :ok}))
(def ^{:arglists '([request respond raise])} routes
"`/api/ee/data-studio/table` routes."
(api.macros/ns-handler *ns* +auth))

View File

@@ -3,6 +3,7 @@
[clojure.string :as str]
[metabase-enterprise.documents.prose-mirror :as prose-mirror]
[metabase.api.common :as api]
[metabase.collections.models.collection :as collection]
[metabase.models.interface :as mi]
[metabase.models.serialization :as serdes]
[metabase.public-sharing.core :as public-sharing]
@@ -228,3 +229,11 @@
(:attrs %)))
:when (contains? model->serdes-model model)]
{[(model->serdes-model model) link-id] {"Document" id}}))))))
(t2/define-before-insert :model/Document [model]
(collection/check-allowed-content :model/Document (:collection_id model))
model)
(t2/define-before-update :model/Document [model]
(collection/check-allowed-content :model/Document (:collection_id (t2/changes model)))
model)

View File

@@ -0,0 +1,84 @@
(ns metabase-enterprise.library.api
(:require
[metabase.api.common :as api]
[metabase.api.macros :as api.macros]
[metabase.api.routes.common :refer [+auth]]
[metabase.collections.core :as collections]
[metabase.collections.models.collection :as collection]
[toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(api.macros/defendpoint :post "/"
"Creates the Library if it doesn't exist. Returns the created collection.
Requires superuser permissions."
[_route
_query
_body]
(api/check-superuser)
(api/check-400 (not (collections/library-collection)) "Library already exists")
(collections/create-library-collection!))
(defn- add-here-and-below [collection]
(let [descendent-ids (map :id (collection/descendants-flat collection))
below (t2/select-fn-set :type [:model/Card :type] :collection_id [:in descendent-ids])]
;; This function is only used on the root Library which cannot have items directly in it
;; So can assume :here is only collection, and all descendants are :below
(assoc collection :here #{:collection}
:below (cond-> below
(contains? below :model)
(-> (disj :model) (conj :dataset))
true sort))))
(api.macros/defendpoint :get "/"
"Get the Library. If no library exists, it doesn't fail but returns an empty response."
[_route
_query
_body]
(if-let [library (collections/library-collection)]
(-> (api/read-check library)
(t2/hydrate
:can_write
:effective_children)
(add-here-and-below))
{:message "Library does not exist"}))
(defn- select-collections
[]
(t2/select :model/Collection
{:where [:and
[:in :type [collection/library-collection-type
collection/library-models-collection-type
collection/library-metrics-collection-type]]
(collection/visible-collection-filter-clause
:id
{:include-archived-items :exclude
:include-trash-collection? false
:permission-level :read
:archive-operation-id nil})]
:order-by [[:%lower.name :asc]]}))
(api.macros/defendpoint :get "/tree"
"This matches /api/collection/tree but only returns the library collection."
[_route-params
_query]
(let [collections (-> (select-collections)
(t2/hydrate :can_write))
collection-type-ids (reduce (fn [acc {collection-id :collection_id, card-type :type, :as _card}]
(update acc (case (keyword card-type)
:model :dataset
:metric :metric
:card) conj collection-id))
{:dataset #{}
:metric #{}
:card #{}}
(t2/reducible-query {:select-distinct [:collection_id :type]
:from [:report_card]
:where [:= :archived false]}))]
(collection/collections->tree collection-type-ids collections)))
(def ^{:arglists '([request respond raise])} routes
"`/api/ee/library` routes."
(api.macros/ns-handler *ns* +auth))

View File

@@ -0,0 +1,11 @@
(ns metabase-enterprise.library.core
(:require
[metabase-enterprise.library.validation]
[potemkin :as p]))
(comment
metabase-enterprise.library.validation/keep-me)
(p/import-vars
[metabase-enterprise.library.validation
check-allowed-content])

View File

@@ -0,0 +1,36 @@
(ns metabase-enterprise.library.validation
(:require
[clojure.set :as set]
[metabase.collections.models.collection :as collection]
[metabase.premium-features.core :refer [defenterprise]]
[toucan2.core :as t2]))
(defenterprise check-allowed-content
"Check if the collection's content matches the allowed content.
Throws an exception if it does not"
:feature :data-studio
[content-type collection-id]
(when collection-id
(when-let [collection-type (t2/select-one-fn :type [:model/Collection :type] :id collection-id)]
(when (and (= collection-type collection/library-collection-type) (not (contains? #{collection/library-models-collection-type
collection/library-metrics-collection-type}
content-type)))
(throw (ex-info "Cannot add anything to the Library collection" {})))
(when (and (= collection-type collection/library-models-collection-type) (not (= :model content-type)))
(throw (ex-info "Can only add models to the 'Models' collection" {})))
(when (and (= collection-type collection/library-metrics-collection-type) (not (= :metric content-type)))
(throw (ex-info "Can only add metrics to the 'Metrics' collection" {})))))
true)
(defenterprise check-library-update
"Checks that a collection of type `:data-studio` only contains allowed content."
metabase-enterprise.library.validation
:feature :data-studio
[collection]
(when (and (contains? #{collection/library-collection-type
collection/library-models-collection-type
collection/library-metrics-collection-type} (:type collection))
(seq (set/intersection (set (keys (t2/changes collection)))
#{:name :description :archived :location :personal_owner_id :slug :namespace :type :authority_level :is_sample})))
(throw (ex-info "Cannot update properties on a Library collection" {})))
true)

View File

@@ -472,7 +472,7 @@
(defn- search-filters
"Generate WHERE conditions based on search context filters."
[{:keys [archived? verified models created-at created-by last-edited-at last-edited-by table-db-id ids display-type]}]
[{:keys [archived? verified models created-at created-by last-edited-at last-edited-by table-db-id ids display-type collection]}]
(let [conditions (filter some?
[(when (some? archived?)
[:= :archived archived?])
@@ -486,6 +486,8 @@
[:in :last_editor_id last-edited-by])
(when table-db-id
[:= :database_id table-db-id])
(when collection
[:= :collection_id collection])
(when (seq ids)
[:in :model_id (map str ids)])
(when (seq display-type)
@@ -754,6 +756,22 @@
docs)))))
(defn- filter-by-collection-id
"Filter documents by collection and all descendant collections."
[docs collection-id]
(let [collection-ids (keep :collection_id docs)
collections-map (when (seq collection-ids)
(->> (t2/select [:collection :id :location]
:id [:in collection-ids])
(into {} (map (juxt :id identity)))))]
(filterv (fn [doc]
(let [doc-collection-id (:collection_id doc)]
(or (= doc-collection-id collection-id)
(when doc-collection-id
(when-let [collection (get collections-map doc-collection-id)]
(str/starts-with? (:location collection) (str "/" collection-id "/")))))))
docs)))
(defn- apply-collection-filter
"Apply personal collection filtering with logging."
[search-context docs]
@@ -771,6 +789,23 @@
(analytics/inc! :metabase-search/semantic-collection-filter-ms time-ms)
filtered-docs))))
(defn- apply-collection-id-filter
"Apply collection ID filtering with logging."
[search-context docs]
(let [collection-id (:collection search-context)]
(if (nil? collection-id)
docs
(let [timer (u/start-timer)
filtered-docs (filter-by-collection-id docs collection-id)
time-ms (u/since-ms timer)]
(log/debug "Collection filter" {:collection-id collection-id
:before (count docs)
:after (count filtered-docs)
:dropped (- (count docs) (count filtered-docs))
:time_ms time-ms})
(analytics/inc! :metabase-search/semantic-collection-id-filter-ms time-ms)
filtered-docs))))
(defn- reducible-search-query
"Extracted so can be redefd in tests."
[db query]
@@ -803,6 +838,7 @@
filtered-results (->> raw-results
filter-read-permitted
(apply-collection-filter search-context)
(apply-collection-id-filter search-context)
(mapv search/collapse-id))
filter-time-ms (u/since-ms filter-timer)

View File

@@ -59,7 +59,7 @@
run-id driver transform-details
(fn [_cancel-chan] (driver/run-transform! driver transform-details opts))))
(transforms.instrumentation/with-stage-timing [run-id [:import :table-sync]]
(transforms.util/sync-target! target database run-id)
(transforms.util/sync-target! target database)
;; This event must be published only after the sync is complete - the new table needs to be in AppDB.
(events/publish-event! :event/transform-run-complete {:object transform-details}))))
(catch Throwable t

View File

@@ -128,15 +128,10 @@
(defn sync-target!
"Sync target of a transform"
([transform-id run-id]
(let [{:keys [source target]} (t2/select-one :model/Transform transform-id)
db (get-in source [:query :database])
database (t2/select-one :model/Database db)]
(sync-target! target database run-id)))
([target database _run-id]
;; sync the new table (note that even a failed sync status means that the execution succeeded)
(log/info "Syncing target" (pr-str target) "for transform")
(activate-table-and-mark-computed! database target)))
[target database]
;; sync the new table (note that even a failed sync status means that the execution succeeded)
(log/info "Syncing target" (pr-str target) "for transform")
(activate-table-and-mark-computed! database target))
(defn target-table-exists?
"Test if the target table of a transform already exists."
@@ -160,16 +155,20 @@
([database target {:keys [create?]}]
(when-let [table (or (target-table (:id database) target)
(when create?
(sync/create-table! database (select-keys target [:schema :name]))))]
(sync/create-table! database (select-keys target [:schema :name :data_source :data_authority]))))]
(sync/sync-table! table)
table)))
(defn activate-table-and-mark-computed!
"Activate table for `target` in `database` in the app db."
[database target]
(when-let [table (sync-table! database target {:create? true})]
(when (or (not (:active table)) (not (= (:data_authority table) :computed)))
(t2/update! :model/Table (:id table) {:active true, :data_authority :computed}))))
(when-let [table (sync-table! database (assoc target
:data_authority :computed
:data_source :metabase-transform)
{:create? true})]
(when-not (:active table)
(t2/update! :model/Table (:id table) {:active true}))
table))
(defn deactivate-table!
"Deactivate table for `target` in `database` in the app db."

View File

@@ -341,7 +341,7 @@
result (transforms.instrumentation/with-stage-timing [run-id [:computation :python-execution]]
(transforms.util/run-cancelable-transform! run-id driver transform-details run-fn :ex-message-fn ex-message-fn))]
(transforms.instrumentation/with-stage-timing [run-id [:import :table-sync]]
(transforms.util/sync-target! target db run-id))
(transforms.util/sync-target! target db))
{:run_id run-id
:result result}))
(catch Throwable t

View File

@@ -19,6 +19,7 @@
:dashboard-subscription-filters
:disable-password-login
:database-auth-providers
:data-studio
:development-mode
:documents
:email-allow-list
@@ -68,6 +69,7 @@
:dashboard_subscription_filters true
:disable_password_login true
:database_auth_providers true
:data_studio true
:development_mode true
:documents true
:email_allow_list true

View File

@@ -0,0 +1,396 @@
(ns ^:mb/driver-tests metabase-enterprise.data-studio.api.table-test
"Tests for /api/ee/data-studio/table endpoints."
(:require
[clojure.test :refer :all]
[metabase-enterprise.data-studio.api.table :as api.table]
[metabase.sync.core :as sync]
[metabase.test :as mt]
[metabase.util :as u]
[toucan2.core :as t2])
(:import (java.util.concurrent CountDownLatch TimeUnit)))
(set! *warn-on-reflection* true)
(deftest publish-model-test
(mt/with-premium-features #{:data-studio}
(testing "POST /api/ee/data-studio/table/publish-model"
(testing "creates models for selected tables"
(mt/with-model-cleanup [:model/Card]
(mt/with-temp [:model/Collection {collection-id :id} {}]
(let [response (mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-model"
{:table_ids [(mt/id :users) (mt/id :venues)]
:target_collection_id collection-id})]
(is (= 2 (:created_count response)))
(is (= 2 (count (:models response))))
(testing "models have correct attributes"
(doseq [model (:models response)]
(is (= "model" (:type model)))
(is (= collection-id (:collection_id model)))
(is (int? (:published_table_id model)))))
(testing "the query should works"
(is (some? (mt/process-query (-> response :models first :dataset_query)))))
(testing "models are persisted in database"
(is (= 2 (t2/count :model/Card :collection_id collection-id :type :model)))))))))))
;: TODO (Ngoc 31/10/2025): test publish model to library mark the library as dirty
(deftest published-as-model-test
(mt/with-premium-features #{:data-studio}
(mt/with-model-cleanup [:model/Card]
(mt/with-temp [:model/Collection {collection-id :id} {}]
(let [{:keys [models]} (mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-model"
{:table_ids [(mt/id :venues)]
:target_collection_id collection-id})
[model] models
venues-schema (t2/select-one-fn :schema [:model/Table :schema] (mt/id :venues))
schema-url (format "database/%d/schema/%s" (mt/id) venues-schema)
list-tables-via-table-api (fn [] (->> (mt/user-http-request :crowberto :get 200 "table" :db-id (mt/id))
(filter #(= (mt/id) (:db_id %)))))
list-tables-via-schema-api #(mt/user-http-request :crowberto :get 200 schema-url)]
(testing "list tables"
(let [list-response (list-tables-via-table-api)
published-as-model (u/index-by :id :published_as_model list-response)]
(is (true? (published-as-model (mt/id :venues))))
(is (false? (published-as-model (mt/id :users))))))
(testing "list tables via schema"
(let [list-response (list-tables-via-schema-api)
published-as-model (u/index-by :id :published_as_model list-response)]
(is (true? (published-as-model (mt/id :venues))))
(is (false? (published-as-model (mt/id :users))))))
(testing "archived tables are not returned"
(t2/update! :model/Card (:id model) {:archived true, :archived_directly false})
(is (not-any? :published_as_model (list-tables-via-table-api)))))))))
(deftest published-models-perm-test
(mt/with-premium-features #{:data-studio}
;; what I think would be reasonable permissions behaviour:
;; > if no permissions on model or its collection, model is filtered out
;; what I have done:
;; > if not super-user we never give you the published models
(mt/with-model-cleanup [:model/Card]
(mt/with-temp [:model/Collection {collection1 :id} {}
:model/Collection {collection2 :id} {}]
(let [publish-venues #(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-model"
{:table_ids [(mt/id :venues)]
:target_collection_id %})
{[{model1 :id}] :models} (publish-venues collection1)
{[{model2 :id}] :models} (publish-venues collection2)
query-meta-url #(format "table/%d/query_metadata" %)
list-published-models #(:published_models (mt/user-http-request %1 :get 200 (query-meta-url %2)))]
(testing "admin sees both models"
(is (= #{model1 model2} (set (map :id (list-published-models :crowberto (mt/id :venues)))))))
(testing "no published models"
(is (nil? (list-published-models :crowberto (mt/id :users)))))
(testing "non admin does not see published models"
(is (nil? (list-published-models :rasta (mt/id :venues))))
(is (nil? (list-published-models :rasta (mt/id :users))))))))))
(deftest bulk-edit-visibility-sync-test
(mt/with-premium-features #{:data-studio}
(testing "POST /api/ee/data-studio/table/edit visibility field synchronization"
(mt/with-temp [:model/Database {db-id :id} {}
:model/Table {table-1-id :id} {:db_id db-id}
:model/Table {table-2-id :id} {:db_id db-id}]
(testing "updating data_layer syncs to visibility_type for all tables"
;; Update two tables to gold, which should sync to nil visibility_type
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [table-1-id table-2-id]
:data_layer "gold"})
(is (= :gold (t2/select-one-fn :data_layer :model/Table :id table-1-id)))
(is (= nil (t2/select-one-fn :visibility_type :model/Table :id table-1-id)))
(is (= :gold (t2/select-one-fn :data_layer :model/Table :id table-2-id)))
(is (= nil (t2/select-one-fn :visibility_type :model/Table :id table-2-id))))
(testing "updating data_layer to copper syncs to hidden visibility_type"
;; Update one table back to copper, which should sync to :hidden
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [table-1-id]
:data_layer "copper"})
(is (= :copper (t2/select-one-fn :data_layer :model/Table :id table-1-id)))
(is (= :hidden (t2/select-one-fn :visibility_type :model/Table :id table-1-id))))
(testing "cannot update both visibility_type and data_layer at once"
(mt/user-http-request :crowberto :post 400 "ee/data-studio/table/edit"
{:table_ids [table-1-id]
:visibility_type "hidden"
:data_layer "copper"}))))))
(deftest requests-data-studio-feature-flag-test
(mt/with-premium-features #{}
(is (= "Data Studio is a paid feature not currently available to your instance. Please upgrade to use it. Learn more at metabase.com/upgrade/"
(:message (mt/user-http-request :crowberto :post 402 "ee/data-studio/table/edit"
{:table_ids [(mt/id :users)]
:data_layer "gold"}))))))
(deftest ^:parallel non-admins-cant-trigger-bulk-sync-test
(mt/with-premium-features #{:data-studio}
(testing "Non-admins should not be allowed to trigger sync"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :post 403 "ee/data-studio/table/sync-schema" {:database_ids [(mt/id)]}))))))
(deftest trigger-bulk-metadata-sync-for-table-test
(mt/with-premium-features #{:data-studio}
;; lot more to test here but will wait for firmer ground
(testing "Can we trigger a metadata sync for a filtered set of tables"
(let [tables (atom [])
latch (CountDownLatch. 4)]
(mt/with-temp [:model/Database {d1 :id} {:engine "h2", :details (:details (mt/db))}
:model/Database {d2 :id} {:engine "h2", :details (:details (mt/db))}
:model/Table {t1 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {t2 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {_ :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t4 :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t5 :id} {:db_id d2, :schema "FOO"}]
(with-redefs [sync/sync-table! (fn [table]
(swap! tables conj table)
(.countDown latch)
nil)]
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/sync-schema" {:database_ids [d1],
:schema_ids [(format "%d:FOO" d2)]
:table_ids [t4]}))
(testing "sync called?"
(is (true? (.await latch 4 TimeUnit/SECONDS)))
(is (= [t1 t2 t4 t5] (map :id @tables)))))))))
(deftest ^:parallel non-admins-cant-trigger-bulk-rescan-values-test
(mt/with-premium-features #{:data-studio}
(testing "Non-admins should not be allowed to trigger rescan values"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :post 403 "ee/data-studio/table/rescan-values" {:database_ids [(mt/id)]}))))))
(deftest trigger-rescan-values-for-tables-test
(mt/with-premium-features #{:data-studio}
;; lot more to test here but will wait for firmer ground
(testing "Can we trigger a field values sync for a filtered set of tables"
(let [tables (atom [])
latch (CountDownLatch. 4)]
(mt/with-temp [:model/Database {d1 :id} {:engine "h2", :details (:details (mt/db))}
:model/Database {d2 :id} {:engine "h2", :details (:details (mt/db))}
:model/Table {t1 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {t2 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {_ :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t4 :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t5 :id} {:db_id d2, :schema "FOO"}]
(with-redefs [sync/update-field-values-for-table! (fn [table]
(swap! tables conj table)
(.countDown latch)
nil)]
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/rescan-values" {:database_ids [d1],
:schema_ids [(format "%d:FOO" d2)]
:table_ids [t4]}))
(testing "rescanned?"
(is (true? (.await latch 4 TimeUnit/SECONDS)))
(is (= [t1 t2 t4 t5] (map :id @tables)))))))))
(deftest ^:parallel non-admins-cant-trigger-bulk-discard-values-test
(mt/with-premium-features #{:data-studio}
(testing "Non-admins should not be allowed to trigger discard values"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :post 403 "ee/data-studio/table/discard-values" {:database_ids [(mt/id)]}))))))
(deftest ^:parallel bulk-discard-values-test
(mt/with-premium-features #{:data-studio}
(testing "POST /api/ee/data-studio/table/discard-values"
(mt/with-temp
[:model/Database {d1 :id} {:engine "h2", :details (:details (mt/db))}
:model/Database {d2 :id} {:engine "h2", :details (:details (mt/db))}
:model/Table {t1 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {t2 :id} {:db_id d1, :schema "PUBLIC"}
:model/Table {t3 :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t4 :id} {:db_id d2, :schema "PUBLIC"}
:model/Table {t5 :id} {:db_id d2, :schema "FOO"}
:model/Field {f1 :id} {:table_id t1}
:model/FieldValues {v1 :id} {:field_id f1, :values ["T1"]}
:model/Field {f2 :id} {:table_id t2}
:model/FieldValues {v2 :id} {:field_id f2, :values ["T2"]}
:model/Field {f3 :id} {:table_id t3}
:model/FieldValues {v3 :id} {:field_id f3, :values ["T3"]}
:model/Field {f4 :id} {:table_id t4}
:model/FieldValues {v4 :id} {:field_id f4, :values ["T4-1"]}
:model/Field {f5 :id} {:table_id t4}
:model/FieldValues {v5 :id} {:field_id f5, :values ["T4-2"]}
:model/Field {f6 :id} {:table_id t5}
:model/FieldValues {v6 :id} {:field_id f6, :values ["T5-1"]}
:model/Field {f7 :id} {:table_id t5}
:model/FieldValues {v7 :id} {:field_id f7, :values ["T5-2"]}]
(let [url "ee/data-studio/table/discard-values"
remaining-field-values-q {:select [:fv.id]
:from [[(t2/table-name :model/FieldValues) :fv]
[(t2/table-name :model/Field) :f]]
:where [:and [:= :fv.field_id :f.id]
[:in :f.table_id [t1 t2 t3 t4 t5]]]
:order-by [[:fv.id :asc]]}
get-field-values #(mapv :id (t2/query remaining-field-values-q))]
(testing "Non-admin toucans should not be allowed to discard values"
(is (= "You don't have permissions to do that." (mt/user-http-request :rasta :post 403 url {:table_ids [t1]})))
(testing "FieldValues should still exist"
(is (= [v1 v2 v3 v4 v5 v6 v7] (get-field-values)))))
(testing "Admins should be able to successfully delete them"
(is (= {:status "ok"} (mt/user-http-request :crowberto :post 200 url {:database_ids [d1],
:schema_ids [(format "%d:FOO" d2)]
:table_ids [t4]})))
(testing "Selected FieldValues should be gone"
(is (= [v3] (get-field-values))))))))))
(deftest ^:parallel table-selectors->filter-test
(testing "table-selectors->filter function generates correct WHERE clauses"
(let [selectors->table-ids (fn [selectors]
(let [where (#'api.table/table-selectors->filter selectors)]
(t2/select-pks-set :model/Table {:where where})))]
(mt/with-temp [:model/Database {db-1 :id} {}
:model/Database {db-2 :id} {}
:model/Table {table-1 :id} {:db_id db-1}
:model/Table {table-2 :id} {:db_id db-1, :schema "schema-a"}
:model/Table {table-3 :id} {:db_id db-2, :schema "schema-a"}
:model/Table {table-4 :id} {:db_id db-2, :schema "schema-b"}
:model/Table {table-5 :id} {:db_id db-2}]
(testing "filter by database_ids"
(is (= #{table-1 table-2}
(selectors->table-ids {:database_ids [db-1]}))))
(testing "filter by table_ids"
(is (= #{table-3 table-4}
(selectors->table-ids {:table_ids [table-3 table-4]}))))
(testing "filter by schema_ids"
(is (= #{table-2}
(selectors->table-ids {:schema_ids [(format "%d:schema-a" db-1)]}))))
(testing "filter by multiple schema_ids"
(is (= #{table-3 table-4}
(selectors->table-ids {:schema_ids [(format "%d:schema-a" db-2)
(format "%d:schema-b" db-2)]}))))
(testing "combine database_ids and table_ids (OR logic)"
(is (= #{table-1 table-2 table-3}
(selectors->table-ids {:database_ids [db-1]
:table_ids [table-3]}))))
(testing "combine all selectors (OR logic)"
(is (= #{table-1 table-2 table-4 table-5}
(selectors->table-ids {:database_ids [db-1]
:table_ids [table-5]
:schema_ids [(format "%d:schema-b" db-2)]}))))
(testing "empty selectors returns no tables"
(is (= nil (selectors->table-ids {}))))))))
(deftest trigger-sync-on-data-layer-change-from-copper-test
(mt/with-premium-features #{:data-studio}
(testing "Changing data_layer from copper to another value triggers sync"
(mt/with-temp [:model/Database {db-id :id} {}
:model/Table {copper-1 :id} {:db_id db-id, :data_layer :copper}
:model/Table {copper-2 :id} {:db_id db-id, :data_layer :copper}
:model/Table {copper-3 :id} {:db_id db-id, :data_layer :copper}
:model/Table {copper-4 :id} {:db_id db-id, :data_layer :copper}
:model/Table {gold-1 :id} {:db_id db-id, :data_layer :gold}
:model/Table {gold-2 :id} {:db_id db-id, :data_layer :gold}]
(let [synced-ids (atom #{})]
(mt/with-dynamic-fn-redefs [api.table/sync-unhidden-tables (fn [tables] (reset! synced-ids (set (map :id tables))))]
(testing "Changing from copper to gold triggers sync"
(reset! synced-ids #{})
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [copper-1 copper-2]
:data_layer "gold"})
(is (= #{copper-1 copper-2} @synced-ids)))
(testing "Changing from copper to silver triggers sync"
(reset! synced-ids #{})
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [copper-3]
:data_layer "silver"})
(is (= #{copper-3} @synced-ids)))
(testing "Changing from copper to bronze triggers sync"
(reset! synced-ids #{})
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [copper-4]
:data_layer "bronze"})
(is (= #{copper-4} @synced-ids)))
(testing "Not changing from copper does not trigger sync"
(reset! synced-ids #{})
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [gold-1]
:data_layer "silver"})
(is (= #{} @synced-ids)))
(testing "Changing to copper does not trigger sync"
(reset! synced-ids #{})
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [gold-2]
:data_layer "copper"})
(is (= #{} @synced-ids)))))))))
(deftest bulk-edit-test
(mt/with-premium-features #{:data-studio}
(testing "can edit a bunch of things at once"
(mt/with-temp [:model/Database {clojure :id} {}
:model/Database {jvm :id} {}
:model/Table {vars :id} {:db_id clojure}
:model/Table {namespaces :id} {:db_id clojure}
:model/Table {beans :id} {:db_id jvm}
:model/Table {classes :id} {:db_id jvm}
:model/Table {gc :id} {:db_id jvm, :schema "jre"}
:model/Table {jit :id} {:db_id jvm, :schema "jre"}]
(testing "only admin can edit"
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :post 403 "ee/data-studio/table/edit"
{:database_ids [clojure jvm]
:data_layer "copper"}))))
(testing "simple happy path updating with db ids"
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:database_ids [clojure jvm]
:data_layer "copper"
:data_authority "authoritative"
:data_source "ingested"})
(is (= #{:copper} (t2/select-fn-set :data_layer :model/Table :db_id [:in [clojure jvm]])))
(is (= #{:authoritative} (t2/select-fn-set :data_authority :model/Table :db_id [:in [clojure jvm]])))
(is (= #{:ingested} (t2/select-fn-set :data_source :model/Table :db_id [:in [clojure jvm]]))))
(testing "updating with all selectors"
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:database_ids [clojure]
:table_ids [classes]
:schema_ids [(format "%d:jre" jvm)]
:data_layer "silver"})
(is (= {vars :silver
namespaces :silver
beans :copper
classes :silver
gc :silver
jit :silver}
(t2/select-pk->fn :data_layer :model/Table :db_id [:in [clojure jvm]]))))
(testing "can update owner_email"
(is (= #{nil} (t2/select-fn-set :owner_email :model/Table :db_id [:in [clojure jvm]])))
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:database_ids [clojure]
:owner_email "clojure-owner@example.com"})
(is (= {vars "clojure-owner@example.com"
namespaces "clojure-owner@example.com"
beans nil
classes nil
gc nil
jit nil}
(t2/select-pk->fn :owner_email :model/Table :db_id [:in [clojure jvm]]))))
(testing "can update owner_user_id"
(is (= #{nil} (t2/select-fn-set :owner_user_id :model/Table :db_id [:in [clojure jvm]])))
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/edit"
{:table_ids [beans classes]
:owner_user_id (mt/user->id :rasta)})
(is (= {vars nil
namespaces nil
beans (mt/user->id :rasta)
classes (mt/user->id :rasta)
gc nil
jit nil}
(t2/select-pk->fn :owner_user_id :model/Table :db_id [:in [clojure jvm]]))))))))

View File

@@ -0,0 +1,37 @@
(ns metabase-enterprise.library.api-test
(:require
[clojure.test :refer :all]
[metabase.collections.models.collection :as collection]
[metabase.collections.test-helpers :refer [without-library]]
[metabase.test :as mt]
[toucan2.core :as t2]))
(deftest get-library-test
(mt/with-premium-features #{:data-studio}
(mt/with-discard-model-updates! [:model/Collection]
(without-library
(testing "When there is no library, returns a message but still 200"
(let [response (mt/user-http-request :crowberto :get 200 "ee/library")]
(is (= {:message "Library does not exist"} response))))
(let [_ (collection/create-library-collection!)
models-id (t2/select-one-pk :model/Collection :type collection/library-models-collection-type)
metrics-id (t2/select-one-pk :model/Collection :type collection/library-metrics-collection-type)
response (mt/user-http-request :crowberto :get 200 "ee/library")]
(testing "When library exists, but no content"
(is (= "Library" (:name response)))
(is (= ["collection"] (:here response)))
(is (= [] (:below response))))
(testing "With content in the library"
(mt/with-temp [:model/Card _ {:name "Card in Models"
:collection_id models-id
:type :model}
:model/Card _ {:name "Card in Metrics"
:collection_id metrics-id
:type :metric}
:model/Card _ {:name "Other Card in Metrics"
:collection_id metrics-id
:type :metric}]
(let [response (mt/user-http-request :crowberto :get 200 "ee/library")]
(is (= "Library" (:name response)))
(is (= ["dataset" "metric"] (:below response)))
(is (= ["collection"] (:here response)))))))))))

View File

@@ -0,0 +1,63 @@
(ns metabase-enterprise.library.validation-test
(:require
[clojure.test :refer :all]
[metabase.collections.models.collection :as collection]
[metabase.test :as mt]
[toucan2.core :as t2]))
(deftest allowed-content-doesnt-block-regular-collections
(mt/with-premium-features #{:data-studio}
(mt/with-temp [:model/Collection regular-collection {:name "Regular Collection" :type nil}]
(testing "Regular collections can add anything"
(is (some? (t2/insert! :model/Collection (merge (mt/with-temp-defaults :model/Collection) {:location (str "/" (:id regular-collection) "/")}))))
(is (some? (t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :model, :collection_id (:id regular-collection)}))))
(is (some? (t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :metric, :collection_id (:id regular-collection)}))))
(is (some? (t2/insert! :model/Dashboard (merge (mt/with-temp-defaults :model/Dashboard) {:collection_id (:id regular-collection)}))))))))
(deftest library-completely-locked-down
(mt/with-premium-features #{:data-studio}
(mt/with-temp [:model/Collection no-allowed-content {:name "Test No Content" :type collection/library-collection-type}]
(testing "Cannot add anything to library collections"
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Cannot add anything to the Library collection"
(t2/insert! :model/Collection (merge (mt/with-temp-defaults :model/Collection) {:location (str "/" (:id no-allowed-content) "/")}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Cannot add anything to the Library collection"
(t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :model, :collection_id (:id no-allowed-content)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Cannot add anything to the Library collection"
(t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :metric :collection_id (:id no-allowed-content)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Cannot add anything to the Library collection"
(t2/insert! :model/Dashboard (merge (mt/with-temp-defaults :model/Dashboard) {:collection_id (:id no-allowed-content)}))))))))
(deftest check-allowed-content-model
(mt/with-premium-features #{:data-studio}
(mt/with-temp [:model/Collection allow-models {:name "Test Base Library" :type collection/library-models-collection-type}]
(testing "Can only add allowed content types"
(is (some? (t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :model, :collection_id (:id allow-models)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add models to the 'Models' collection"
(t2/insert! :model/Collection (merge (mt/with-temp-defaults :model/Collection) {:location (str "/" (:id allow-models) "/")}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add models to the 'Models' collection"
(t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :metric :collection_id (:id allow-models)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add models to the 'Models' collection"
(t2/insert! :model/Dashboard (merge (mt/with-temp-defaults :model/Dashboard) {:collection_id (:id allow-models)}))))))))
(deftest check-allowed-content-metric
(mt/with-premium-features #{:data-studio}
(mt/with-temp [:model/Collection allow-metrics {:name "Test Base Library" :type collection/library-metrics-collection-type}]
(testing "Can only add allowed content types"
(is (some? (t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :metric, :collection_id (:id allow-metrics)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add metrics to the 'Metrics' collection"
(t2/insert! :model/Collection (merge (mt/with-temp-defaults :model/Collection) {:location (str "/" (:id allow-metrics) "/")}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add metrics to the 'Metrics' collection"
(t2/insert! :model/Card (merge (mt/with-temp-defaults :model/Card) {:type :model :collection_id (:id allow-metrics)}))))
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Can only add metrics to the 'Metrics' collection"
(t2/insert! :model/Dashboard (merge (mt/with-temp-defaults :model/Dashboard) {:collection_id (:id allow-metrics)}))))))))
(deftest cannot-update-library-collections
(mt/with-premium-features #{:data-studio}
(mt/with-temp [:model/Collection library {:name "Test Library" :type collection/library-collection-type}
:model/Collection models {:name "Test Semantic Model Layer" :type collection/library-models-collection-type}
:model/Collection metrics {:name "Test Semantic Metrics Layer" :type collection/library-metrics-collection-type}]
(doseq [col [library models metrics]]
(testing (str "Checking type " (:type col))
(is (thrown-with-msg? clojure.lang.ExceptionInfo
#"Cannot update properties on a Library collection"
(t2/update! :model/Collection (:id col) {:name "New Name"}))))))))

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