Compare commits
629 Commits
python-run
...
workbench2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b4c699e7 | ||
|
|
c4f830074f | ||
|
|
bfecdec661 | ||
|
|
8cdba689d2 | ||
|
|
84c2b8374a | ||
|
|
84a6016781 | ||
|
|
36c3c77fa6 | ||
|
|
a469e93bb6 | ||
|
|
364ae147e6 | ||
|
|
283bc8e108 | ||
|
|
0779b24d00 | ||
|
|
12bfb92194 | ||
|
|
012a639d83 | ||
|
|
b6ec6e858d | ||
|
|
f434a3b5aa | ||
|
|
567618aceb | ||
|
|
3f1dfd226a | ||
|
|
00e7937b52 | ||
|
|
cacdf1322d | ||
|
|
f2533fb48e | ||
|
|
76c27e6be0 | ||
|
|
54bfe2df5b | ||
|
|
4cdaa1af9e | ||
|
|
37f6092efe | ||
|
|
76f4cbd3af | ||
|
|
a1b29a297d | ||
|
|
d0ca291b45 | ||
|
|
42d380cdd5 | ||
|
|
45fd51a855 | ||
|
|
dd39b05088 | ||
|
|
e5c7403b5c | ||
|
|
342d36d0c6 | ||
|
|
3f572e6b94 | ||
|
|
93e3ca13a0 | ||
|
|
3fa7413b35 | ||
|
|
7460ab0940 | ||
|
|
4beb5cca15 | ||
|
|
adcfb1222e | ||
|
|
716d7d2bdb | ||
|
|
29340508d7 | ||
|
|
35b580d74b | ||
|
|
3766730259 | ||
|
|
1c47368211 | ||
|
|
a9d1140b1a | ||
|
|
69de5d7b2a | ||
|
|
21cd7ae941 | ||
|
|
e5e68ca9c2 | ||
|
|
5bb40b9b35 | ||
|
|
74ad50b451 | ||
|
|
cae26d16d4 | ||
|
|
ed2e9ea0b6 | ||
|
|
3a4edea410 | ||
|
|
ec755156b0 | ||
|
|
b9b05a31b3 | ||
|
|
6c100d3d91 | ||
|
|
ea203da09e | ||
|
|
526e45a63b | ||
|
|
8d6639115f | ||
|
|
e1dccbf743 | ||
|
|
1fba5c95b1 | ||
|
|
69f5e949a6 | ||
|
|
fcc871f760 | ||
|
|
69fc1330c7 | ||
|
|
8048a10e81 | ||
|
|
042986670f | ||
|
|
8ff79dcdcb | ||
|
|
76315411c3 | ||
|
|
9ddfabe818 | ||
|
|
70299d9646 | ||
|
|
2416c7985e | ||
|
|
db4ae27e1a | ||
|
|
678d02ea98 | ||
|
|
6cf5078bdb | ||
|
|
4edf71026d | ||
|
|
fb8817222f | ||
|
|
aab2552107 | ||
|
|
14a8527136 | ||
|
|
7752bee446 | ||
|
|
692fd60f63 | ||
|
|
2e61a1446b | ||
|
|
3b4b235cc4 | ||
|
|
7b86886d46 | ||
|
|
8bccb26278 | ||
|
|
50569ee4c6 | ||
|
|
380b944284 | ||
|
|
eb62d57555 | ||
|
|
38008218af | ||
|
|
0a5b39049f | ||
|
|
e796112439 | ||
|
|
4893ef8c5c | ||
|
|
8848219d99 | ||
|
|
d00d877c08 | ||
|
|
f3bff106a0 | ||
|
|
1899ee9a35 | ||
|
|
f861aa583a | ||
|
|
9b9a4a6242 | ||
|
|
86c84566d0 | ||
|
|
d2dab60c23 | ||
|
|
1b6225d8b9 | ||
|
|
9c9faa0ac7 | ||
|
|
0dc4aebe0c | ||
|
|
4553f2da22 | ||
|
|
e76536256a | ||
|
|
35b129cf56 | ||
|
|
18f5f86209 | ||
|
|
bc5cb1c325 | ||
|
|
1cd7b283f5 | ||
|
|
86001f2203 | ||
|
|
8c4fac5813 | ||
|
|
a6039ccbcd | ||
|
|
be34af6a95 | ||
|
|
c3af2d357a | ||
|
|
3b8f854d75 | ||
|
|
21111813dd | ||
|
|
6abb3957ca | ||
|
|
c23e771219 | ||
|
|
6379de4077 | ||
|
|
1bb3fb5a47 | ||
|
|
bb57238a40 | ||
|
|
fd44a4a4ab | ||
|
|
3a7d694450 | ||
|
|
8c7ef1cb29 | ||
|
|
13b6c5954f | ||
|
|
2d423ab9eb | ||
|
|
5a776c67db | ||
|
|
511c8f4edc | ||
|
|
0795705b5d | ||
|
|
2cc624a9fb | ||
|
|
eb4af32f81 | ||
|
|
7d75cdd2a9 | ||
|
|
a19a1792c3 | ||
|
|
f1768f2c2f | ||
|
|
ce6ac1e6c0 | ||
|
|
9140001a09 | ||
|
|
1b075b6a17 | ||
|
|
4742b245a3 | ||
|
|
010fc0c706 | ||
|
|
a69e9dff99 | ||
|
|
b90fe0c82f | ||
|
|
99d21c6245 | ||
|
|
8f1fc35b57 | ||
|
|
8df3bf8590 | ||
|
|
54ca922f81 | ||
|
|
35d51cc0aa | ||
|
|
61e4b45a05 | ||
|
|
2b9a76c68e | ||
|
|
790879964d | ||
|
|
7313895370 | ||
|
|
01174fe314 | ||
|
|
9684013374 | ||
|
|
d51f1cb8b6 | ||
|
|
3ffc52dfa5 | ||
|
|
c53e1b1caa | ||
|
|
4abbcc05ba | ||
|
|
d74456fa8a | ||
|
|
1de902db18 | ||
|
|
728a23310b | ||
|
|
c5098ee299 | ||
|
|
6168e37bd2 | ||
|
|
2d67edd722 | ||
|
|
b35540ea65 | ||
|
|
b3e02d7b35 | ||
|
|
09f229e248 | ||
|
|
5e8b08aea0 | ||
|
|
1797f19eb4 | ||
|
|
8e2706d20a | ||
|
|
528c637866 | ||
|
|
a9317a3dab | ||
|
|
feb1d1d1ad | ||
|
|
fd85264a58 | ||
|
|
042424d082 | ||
|
|
8f75a93d32 | ||
|
|
e879b29173 | ||
|
|
c4057c58b6 | ||
|
|
f28d9d5af9 | ||
|
|
162045ca28 | ||
|
|
2507d4a74e | ||
|
|
0579457b07 | ||
|
|
8b38056c2b | ||
|
|
135377c55a | ||
|
|
6e2cb29d39 | ||
|
|
edc9329042 | ||
|
|
b85c85aae2 | ||
|
|
005e07ca7e | ||
|
|
8b54aeb677 | ||
|
|
8f804ee84e | ||
|
|
34fa895152 | ||
|
|
f75e99d852 | ||
|
|
87ddcd9cb6 | ||
|
|
0291de7cd4 | ||
|
|
e13a07e9ce | ||
|
|
beaecf8593 | ||
|
|
2420f26c27 | ||
|
|
752b2a4c14 | ||
|
|
43ee6826fc | ||
|
|
5c181eec70 | ||
|
|
701ea75b06 | ||
|
|
c6c36b777c | ||
|
|
dc8190f6d2 | ||
|
|
f94d3b160c | ||
|
|
eb469813d9 | ||
|
|
6565756360 | ||
|
|
c76e082b02 | ||
|
|
dc63b94eff | ||
|
|
78c8a0265a | ||
|
|
d53ff6e6a0 | ||
|
|
e0b8bd2acc | ||
|
|
a359db80df | ||
|
|
b4474e719d | ||
|
|
bcf0c0065c | ||
|
|
fa92970adc | ||
|
|
a14451ba73 | ||
|
|
c3a90bcf36 | ||
|
|
ba719e9cd0 | ||
|
|
8d515e92e3 | ||
|
|
9d3be84fae | ||
|
|
8a913474d6 | ||
|
|
4fa0c3b422 | ||
|
|
d883a13568 | ||
|
|
70c6b519bb | ||
|
|
818c44a1cd | ||
|
|
72a63f66da | ||
|
|
2d5adffd78 | ||
|
|
cc02de6a3a | ||
|
|
8401bce4c4 | ||
|
|
627bbbb1b7 | ||
|
|
c36649a144 | ||
|
|
0548a4da87 | ||
|
|
e263f1737f | ||
|
|
39f0ee723d | ||
|
|
b35d28c281 | ||
|
|
4b35d5afee | ||
|
|
12d44596b4 | ||
|
|
3118bf04f7 | ||
|
|
583c6590af | ||
|
|
64eb5336d3 | ||
|
|
beb106808c | ||
|
|
fcbd4ad3e7 | ||
|
|
9ce326e5cf | ||
|
|
905afa2968 | ||
|
|
e3d3455a19 | ||
|
|
3ce176b363 | ||
|
|
914c5cddd0 | ||
|
|
3da6139783 | ||
|
|
06827b7c33 | ||
|
|
ed528b774f | ||
|
|
5d6fd986f1 | ||
|
|
7d2d042837 | ||
|
|
6accc082bf | ||
|
|
aca2799153 | ||
|
|
814c49473b | ||
|
|
25e0d78408 | ||
|
|
2e12638d28 | ||
|
|
9997588d3e | ||
|
|
b21e5da510 | ||
|
|
a830ebbf36 | ||
|
|
2408bfb5e7 | ||
|
|
31c5636350 | ||
|
|
352419559b | ||
|
|
0fefbe02e1 | ||
|
|
a0d78a2826 | ||
|
|
6b5ea7220b | ||
|
|
2287799774 | ||
|
|
306d62eb1e | ||
|
|
99f8c03ca6 | ||
|
|
68c02c5278 | ||
|
|
9494b5dced | ||
|
|
c4157319ae | ||
|
|
6765a205a8 | ||
|
|
967b2dec8e | ||
|
|
e533d5d67b | ||
|
|
aab525fda4 | ||
|
|
ef2fdb4f87 | ||
|
|
2c9fe22dfe | ||
|
|
f4524f4f01 | ||
|
|
dc76695d0d | ||
|
|
6cb477cd2a | ||
|
|
e494ee1f8e | ||
|
|
b18b6d676c | ||
|
|
b5046f7745 | ||
|
|
ff8b3352b7 | ||
|
|
d5ecc533fe | ||
|
|
5e311d78d4 | ||
|
|
31b081dc7e | ||
|
|
d2dedc0702 | ||
|
|
32ef181154 | ||
|
|
644eabcfe4 | ||
|
|
2ec5edfb1b | ||
|
|
aafeba9544 | ||
|
|
dba6fe5808 | ||
|
|
cb193dc2ae | ||
|
|
b0f71c03ea | ||
|
|
3a5fe4339a | ||
|
|
1e26430772 | ||
|
|
75167a125d | ||
|
|
21999e0156 | ||
|
|
fc6ec01fb4 | ||
|
|
8b7792f7f7 | ||
|
|
0715e6877b | ||
|
|
4460b65e26 | ||
|
|
eb008a63f7 | ||
|
|
8d676bebd4 | ||
|
|
905270ad6e | ||
|
|
b919085277 | ||
|
|
f58d0ea87b | ||
|
|
3368696328 | ||
|
|
908ddb5a76 | ||
|
|
bff6043361 | ||
|
|
9787dc33bf | ||
|
|
b7e1f0fbbe | ||
|
|
0f02e68901 | ||
|
|
2da9deb077 | ||
|
|
dc410f1754 | ||
|
|
b7f1725579 | ||
|
|
29122a9d19 | ||
|
|
0221887631 | ||
|
|
3caa0c3ee7 | ||
|
|
f369233998 | ||
|
|
ca1b05af35 | ||
|
|
4083ef4bf3 | ||
|
|
0d2883785c | ||
|
|
6ed99ac363 | ||
|
|
9d4f79815d | ||
|
|
2df7fc960e | ||
|
|
ef069e67f1 | ||
|
|
56a4c43415 | ||
|
|
90ab235b7d | ||
|
|
665c22c1b2 | ||
|
|
63a852d94e | ||
|
|
ecf9534212 | ||
|
|
4130c16f0e | ||
|
|
c31a6b3712 | ||
|
|
15fad56d26 | ||
|
|
acb1d2e14a | ||
|
|
611707669e | ||
|
|
3b9a0fa2c2 | ||
|
|
51662399da | ||
|
|
3c105548de | ||
|
|
dc4b244ff6 | ||
|
|
e20e7f38b5 | ||
|
|
8b324dc75d | ||
|
|
d3d98b9dec | ||
|
|
7b17b208bb | ||
|
|
ed2ed6de4b | ||
|
|
d1c640dba4 | ||
|
|
09b7251b3e | ||
|
|
da986230cb | ||
|
|
088126f392 | ||
|
|
cad4359d51 | ||
|
|
8121169b7b | ||
|
|
2d48062e18 | ||
|
|
54e8d5fb8e | ||
|
|
5709b33da0 | ||
|
|
bd58b27031 | ||
|
|
8575acca08 | ||
|
|
5cf519a128 | ||
|
|
13021db8cd | ||
|
|
09dc306245 | ||
|
|
b38d1ae937 | ||
|
|
30b9f5dd3d | ||
|
|
5ae8b77d16 | ||
|
|
0affb1367b | ||
|
|
fe98098726 | ||
|
|
47b209b095 | ||
|
|
2017a5959f | ||
|
|
57e2001582 | ||
|
|
064d02c783 | ||
|
|
6dec92324d | ||
|
|
1b31c1bd83 | ||
|
|
1140b38691 | ||
|
|
d5af13c899 | ||
|
|
ed86b03694 | ||
|
|
536136e872 | ||
|
|
2da2f0d2bd | ||
|
|
ac249dd6d1 | ||
|
|
1c926fa58e | ||
|
|
d50e05e24a | ||
|
|
48f58c1264 | ||
|
|
144958321f | ||
|
|
ae12b79fe0 | ||
|
|
cee4ea61ba | ||
|
|
299915e7b4 | ||
|
|
bfa2b9a309 | ||
|
|
94c347610d | ||
|
|
80613c6763 | ||
|
|
f9c754db95 | ||
|
|
432101b087 | ||
|
|
4e6d12d40f | ||
|
|
0f4a5d4a6f | ||
|
|
457c7ff2a9 | ||
|
|
b25821fada | ||
|
|
f5197df496 | ||
|
|
a2f33d6705 | ||
|
|
d0378bc8b5 | ||
|
|
294348e98f | ||
|
|
fb3de16e0d | ||
|
|
34826093f7 | ||
|
|
8308adc3d8 | ||
|
|
4a5ffa6cbd | ||
|
|
2488ef3254 | ||
|
|
67105b8fbc | ||
|
|
77ce47f815 | ||
|
|
12d5530708 | ||
|
|
00c65498e1 | ||
|
|
c692dd9286 | ||
|
|
0ab71025fd | ||
|
|
569cd4f208 | ||
|
|
a476852bfc | ||
|
|
17d07d3594 | ||
|
|
cb24c615b6 | ||
|
|
b60895ac03 | ||
|
|
87b24e9d09 | ||
|
|
c976599707 | ||
|
|
c36c5bc8ab | ||
|
|
0e1fb566aa | ||
|
|
112ab0194b | ||
|
|
9a01f495a5 | ||
|
|
b62ea9face | ||
|
|
bfc1b11ca4 | ||
|
|
4b9ff89204 | ||
|
|
b1de0951be | ||
|
|
ceb99fa4a7 | ||
|
|
dccbeb029a | ||
|
|
200a887ad2 | ||
|
|
b6f39af5b5 | ||
|
|
1447e2f857 | ||
|
|
b49f3e334d | ||
|
|
47dd45c7d7 | ||
|
|
8dff56d5f2 | ||
|
|
376a62e2e4 | ||
|
|
7c46a67a04 | ||
|
|
ec7e9149e9 | ||
|
|
b897ea4dd0 | ||
|
|
ef24c2549e | ||
|
|
f8fce0ca36 | ||
|
|
d4c3bc4970 | ||
|
|
6e94dedd76 | ||
|
|
476126dd40 | ||
|
|
8242eb6a52 | ||
|
|
f2a8fdbd54 | ||
|
|
f6574f89ee | ||
|
|
bf8f73cc5f | ||
|
|
889b245d5e | ||
|
|
702f1a372a | ||
|
|
fc7906b0ae | ||
|
|
3ba5cc052d | ||
|
|
66da27bf7c | ||
|
|
6ed201fe0a | ||
|
|
74ad8b021d | ||
|
|
1846ab4a96 | ||
|
|
1ad201e041 | ||
|
|
01e2a54c56 | ||
|
|
3761e533b1 | ||
|
|
8cdfd21952 | ||
|
|
8514ef6271 | ||
|
|
fbdedca6ea | ||
|
|
e424da2c28 | ||
|
|
efdb994c9b | ||
|
|
47bb22096a | ||
|
|
25b38c7554 | ||
|
|
796661fd54 | ||
|
|
515ff64d6a | ||
|
|
834f591437 | ||
|
|
013ff8273c | ||
|
|
171276b439 | ||
|
|
f6db06b4af | ||
|
|
98f1ab357a | ||
|
|
facc0654df | ||
|
|
5630234dac | ||
|
|
26ddab4335 | ||
|
|
af00324ff8 | ||
|
|
cb1d7e7805 | ||
|
|
8d08814128 | ||
|
|
dc84037858 | ||
|
|
952c1b5e84 | ||
|
|
346adeda32 | ||
|
|
861d3db6aa | ||
|
|
ce5a6d6bda | ||
|
|
c6d47e6f01 | ||
|
|
4aebf50623 | ||
|
|
cd56bbcc41 | ||
|
|
f35c128378 | ||
|
|
4e4fcefdcf | ||
|
|
88005eb443 | ||
|
|
e740851b38 | ||
|
|
6e5f4a4248 | ||
|
|
7b651d7cf0 | ||
|
|
24e3438e3f | ||
|
|
420a82da88 | ||
|
|
a144ba57a2 | ||
|
|
c4ea11f71e | ||
|
|
0949ad4c14 | ||
|
|
1874944538 | ||
|
|
535672c8a8 | ||
|
|
da3ee83c9a | ||
|
|
0a164e5554 | ||
|
|
0863ff7107 | ||
|
|
a9da08972e | ||
|
|
9cf8fdee58 | ||
|
|
fd74bf9962 | ||
|
|
3c8dd544c0 | ||
|
|
7b04a5828c | ||
|
|
76bd5760bf | ||
|
|
6452043e7f | ||
|
|
4d37013c2e | ||
|
|
ae4e9d5577 | ||
|
|
45ca2337ba | ||
|
|
5198680d05 | ||
|
|
87196224e4 | ||
|
|
258e4f2c31 | ||
|
|
2205142c1d | ||
|
|
a6e689c827 | ||
|
|
5282d3a312 | ||
|
|
768038b21f | ||
|
|
64210dc82f | ||
|
|
23343b6ed3 | ||
|
|
8f66b4ef34 | ||
|
|
f63677e971 | ||
|
|
4163c1641b | ||
|
|
5c174399c6 | ||
|
|
fb10250deb | ||
|
|
a58f30b34c | ||
|
|
710f25794a | ||
|
|
6cba76168a | ||
|
|
fc82ab1b9a | ||
|
|
3cf5565030 | ||
|
|
3c7e12b8c6 | ||
|
|
f448ab6de5 | ||
|
|
fa59881ddf | ||
|
|
986c3289ea | ||
|
|
f5e258f051 | ||
|
|
99da7a96e5 | ||
|
|
c6a32ea431 | ||
|
|
2a90485d4a | ||
|
|
c9f7e8f66b | ||
|
|
21eaffb153 | ||
|
|
f760b2d78d | ||
|
|
1f1142860e | ||
|
|
3c3e7776ab | ||
|
|
af982a3ca4 | ||
|
|
178a9ce965 | ||
|
|
a7c893f7c0 | ||
|
|
203ae9c55b | ||
|
|
cc3105ef0f | ||
|
|
48819b05a7 | ||
|
|
5ab21902d1 | ||
|
|
88f0851fd4 | ||
|
|
b64803ab91 | ||
|
|
c700965477 | ||
|
|
6ade875f8b | ||
|
|
1c7e5f448c | ||
|
|
8fdc854aea | ||
|
|
f3c35d4184 | ||
|
|
47384a7965 | ||
|
|
35594ed38e | ||
|
|
5cb92f6592 | ||
|
|
31d5471b5b | ||
|
|
0242840402 | ||
|
|
8866f2d446 | ||
|
|
a6f4ba70d6 | ||
|
|
800496c30b | ||
|
|
49d04555c9 | ||
|
|
ab719e84f6 | ||
|
|
126fefa0f1 | ||
|
|
9bb252a52b | ||
|
|
47b4c2473a | ||
|
|
8a34280084 | ||
|
|
dd301f5587 | ||
|
|
3748a5a0c4 | ||
|
|
4cefbd1011 | ||
|
|
84eeb9d35b | ||
|
|
22935320ca | ||
|
|
6bb12036ba | ||
|
|
5214db1b34 | ||
|
|
65007662b6 | ||
|
|
fc8e625fd1 | ||
|
|
7f880449e6 | ||
|
|
4126a6fb3c | ||
|
|
97b0caa82b | ||
|
|
a848c57956 | ||
|
|
2b06075990 | ||
|
|
8fdf387887 | ||
|
|
1c5c3da666 | ||
|
|
bfb3848543 | ||
|
|
1a14bfa131 | ||
|
|
96acca5e92 | ||
|
|
bb5fa428b5 | ||
|
|
7d11c2f799 | ||
|
|
e7e029a58c | ||
|
|
1d10123201 | ||
|
|
a77842e92b | ||
|
|
9b0d149418 | ||
|
|
dc7ae946c6 | ||
|
|
ddf5a9d460 | ||
|
|
cc6ce2834f | ||
|
|
82a7b2f5e4 | ||
|
|
7e4daf2721 | ||
|
|
ac8c6e6e0c | ||
|
|
2e94a0f579 | ||
|
|
d746234cb6 | ||
|
|
eecc699fbd | ||
|
|
72de9e73ce | ||
|
|
0abe4483fc | ||
|
|
4292db5bcf | ||
|
|
c6027c28c6 | ||
|
|
43cca10568 | ||
|
|
bac077c21d | ||
|
|
1895291a04 | ||
|
|
fa7ba42783 | ||
|
|
9f39d20e07 | ||
|
|
0df67d497a | ||
|
|
876e3e3c27 | ||
|
|
9db2c15514 | ||
|
|
7ecdf0c5dd | ||
|
|
4f0a8c14b4 | ||
|
|
92d0358840 | ||
|
|
7ce95abb57 | ||
|
|
db118fbe12 | ||
|
|
06e2499210 | ||
|
|
1fd9f81f3b | ||
|
|
7e2a230c90 | ||
|
|
b508b28048 | ||
|
|
400d8426c0 | ||
|
|
7c437e28e6 | ||
|
|
657ab9590b | ||
|
|
8b68bbcc75 | ||
|
|
fe4ee32e2f | ||
|
|
99b089d83d | ||
|
|
a0da007219 |
@@ -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
|
||||
|
||||
@@ -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 |
15
e2e/support/helpers/api/createLibrary.ts
Normal file
15
e2e/support/helpers/api/createLibrary.ts
Normal 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"));
|
||||
};
|
||||
20
e2e/support/helpers/api/createSnippetFolder.ts
Normal file
20
e2e/support/helpers/api/createSnippetFolder.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
110
e2e/support/helpers/e2e-data-studio-helpers.ts
Normal file
110
e2e/support/helpers/e2e-data-studio-helpers.ts
Normal 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),
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
7
e2e/support/helpers/e2e-dependency-graph-helpers.ts
Normal file
7
e2e/support/helpers/e2e-dependency-graph-helpers.ts
Normal 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"),
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]");
|
||||
}
|
||||
|
||||
9
e2e/support/helpers/e2e-ui-select.ts
Normal file
9
e2e/support/helpers/e2e-ui-select.ts
Normal 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");
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
70
e2e/support/test-library-data.ts
Normal file
70
e2e/support/test-library-data.ts
Normal 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,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
55
e2e/test/scenarios/data-studio/modeling-library.cy.spec.ts
Normal file
55
e2e/test/scenarios/data-studio/modeling-library.cy.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
413
e2e/test/scenarios/data-studio/modeling-metrics.cy.spec.ts
Normal file
413
e2e/test/scenarios/data-studio/modeling-metrics.cy.spec.ts
Normal 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");
|
||||
}
|
||||
280
e2e/test/scenarios/data-studio/modeling-models.cy.spec.ts
Normal file
280
e2e/test/scenarios/data-studio/modeling-models.cy.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
301
e2e/test/scenarios/data-studio/snippets.cy.spec.ts
Normal file
301
e2e/test/scenarios/data-studio/snippets.cy.spec.ts
Normal 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");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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]))",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}))
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
84
enterprise/backend/src/metabase_enterprise/library/api.clj
Normal file
84
enterprise/backend/src/metabase_enterprise/library/api.clj
Normal 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))
|
||||
11
enterprise/backend/src/metabase_enterprise/library/core.clj
Normal file
11
enterprise/backend/src/metabase_enterprise/library/core.clj
Normal 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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]))))))))
|
||||
@@ -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)))))))))))
|
||||
@@ -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
Reference in New Issue
Block a user