Compare commits
407 Commits
debug-fail
...
representa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b584b35cd9 | ||
|
|
a36cda7ffd | ||
|
|
7f68894e80 | ||
|
|
151f143233 | ||
|
|
e440239b79 | ||
|
|
0f41ae3925 | ||
|
|
d4344c308e | ||
|
|
b933496d37 | ||
|
|
32d69c1a4f | ||
|
|
7096051905 | ||
|
|
e88075f7d4 | ||
|
|
13db04d7a0 | ||
|
|
3ae80a41da | ||
|
|
6414a9d73b | ||
|
|
19a79d96c1 | ||
|
|
637fa143ff | ||
|
|
8393eb2820 | ||
|
|
e67f25b401 | ||
|
|
39dfac795e | ||
|
|
9bef191295 | ||
|
|
520de22c5e | ||
|
|
67a99bb92d | ||
|
|
cbe07addba | ||
|
|
4efe26824e | ||
|
|
b7ff644e78 | ||
|
|
ccbda1251f | ||
|
|
a07c3b654e | ||
|
|
e80478b006 | ||
|
|
c8d87bdf7b | ||
|
|
e2ec2f6aff | ||
|
|
ce9ac84ecc | ||
|
|
66b5043940 | ||
|
|
2b607f547a | ||
|
|
0400e1bf07 | ||
|
|
df5abbd1b9 | ||
|
|
99fa786ec8 | ||
|
|
b561b5c8b9 | ||
|
|
f2e8e294f8 | ||
|
|
8257c072a9 | ||
|
|
08770c8e8f | ||
|
|
4ef178f794 | ||
|
|
89443cda94 | ||
|
|
99fd1809cb | ||
|
|
868fb6da0a | ||
|
|
8a56f6b3bb | ||
|
|
384d24a75b | ||
|
|
e94dd16252 | ||
|
|
561d578043 | ||
|
|
6f778c9269 | ||
|
|
9b93bfe279 | ||
|
|
dc89b2bbeb | ||
|
|
538ebb5f08 | ||
|
|
e4325d4e02 | ||
|
|
b453baf5ed | ||
|
|
773a588692 | ||
|
|
31f76f5856 | ||
|
|
81f10bdb3c | ||
|
|
3dcfbce301 | ||
|
|
ae23f598f0 | ||
|
|
5c10e2d7a3 | ||
|
|
53f51fa289 | ||
|
|
4280e1af7a | ||
|
|
0a2255c36f | ||
|
|
e1e8d53234 | ||
|
|
cca02fcf31 | ||
|
|
eb4a9f528e | ||
|
|
6e30ce468c | ||
|
|
96eac2d9da | ||
|
|
2d6624319a | ||
|
|
15bb72ee66 | ||
|
|
ce0232662d | ||
|
|
076267ed7c | ||
|
|
a7f3165348 | ||
|
|
1881164422 | ||
|
|
81ba716093 | ||
|
|
f51192d3db | ||
|
|
f90fbb06ce | ||
|
|
23754f9e95 | ||
|
|
76cf73dde5 | ||
|
|
74b8adabe6 | ||
|
|
7d7595996c | ||
|
|
8f23a5b725 | ||
|
|
05b294f47a | ||
|
|
cb37ce8108 | ||
|
|
ae65686d30 | ||
|
|
f26f6a18cd | ||
|
|
4b33138799 | ||
|
|
d3bad99de1 | ||
|
|
a576039ae0 | ||
|
|
619e2fa17e | ||
|
|
9116c09afe | ||
|
|
47b0340836 | ||
|
|
eda624973b | ||
|
|
0c0634dbc1 | ||
|
|
8a1139118f | ||
|
|
8bb448b8fc | ||
|
|
99e16ef825 | ||
|
|
8b44711794 | ||
|
|
c35be5ea97 | ||
|
|
04f1e58c3a | ||
|
|
20657a3b0b | ||
|
|
174a118a6c | ||
|
|
4d6693adae | ||
|
|
91d2191c93 | ||
|
|
86dcae0d73 | ||
|
|
ea48c27d72 | ||
|
|
13cb16df11 | ||
|
|
89e585ac23 | ||
|
|
7c8078bd09 | ||
|
|
8e37784337 | ||
|
|
b0443a4805 | ||
|
|
ddcc13edd1 | ||
|
|
38a5d7b604 | ||
|
|
938600d341 | ||
|
|
1469864d71 | ||
|
|
777980d11e | ||
|
|
0a63009b9e | ||
|
|
609a58ad6d | ||
|
|
e202541ac8 | ||
|
|
7e54e25766 | ||
|
|
9b903edd5e | ||
|
|
ea1cdf4917 | ||
|
|
c58e26c8e2 | ||
|
|
99d1bd8cdf | ||
|
|
57999716ce | ||
|
|
57f64aded5 | ||
|
|
8d204c712e | ||
|
|
2cbece56c3 | ||
|
|
c8c515462a | ||
|
|
4ed9e32676 | ||
|
|
3a02474632 | ||
|
|
20875923d7 | ||
|
|
2b2cd76b2f | ||
|
|
6a78c448cf | ||
|
|
81fa467312 | ||
|
|
7b604076bc | ||
|
|
8f8b2b762a | ||
|
|
db2d21ef20 | ||
|
|
c2a163701b | ||
|
|
2a9df86ff0 | ||
|
|
b2502af083 | ||
|
|
4cd66a1b6e | ||
|
|
eb9a0acb6f | ||
|
|
bc6f659963 | ||
|
|
93fe76a5c6 | ||
|
|
993672c878 | ||
|
|
2421a24f31 | ||
|
|
2688cf4cfb | ||
|
|
4ac312e62c | ||
|
|
32a67d3633 | ||
|
|
e343d5ebbe | ||
|
|
49020716b5 | ||
|
|
e406c0c35c | ||
|
|
3cd8c2f2cd | ||
|
|
59f53c16db | ||
|
|
cac18c4db2 | ||
|
|
dde1cd6b95 | ||
|
|
e4b0d120e5 | ||
|
|
56fac7fa11 | ||
|
|
6826b70279 | ||
|
|
dae088f934 | ||
|
|
983bf8b002 | ||
|
|
709a494ad4 | ||
|
|
29a5c1d804 | ||
|
|
d2a4f43bfa | ||
|
|
9f04d4525c | ||
|
|
6a98142d10 | ||
|
|
6e075b3fee | ||
|
|
f4f2fae807 | ||
|
|
4b1783d724 | ||
|
|
9bb863e36f | ||
|
|
18b558507c | ||
|
|
60af228ce8 | ||
|
|
4ebe289c9e | ||
|
|
b96378dd1d | ||
|
|
80a1ee7544 | ||
|
|
190b660861 | ||
|
|
69a0182908 | ||
|
|
096c943179 | ||
|
|
3250dbbe3b | ||
|
|
eb27ad00a2 | ||
|
|
e5d50e6e41 | ||
|
|
e876cc59ff | ||
|
|
04f22bce1c | ||
|
|
fe38c6dbfb | ||
|
|
ab8d8e1a85 | ||
|
|
38edca362b | ||
|
|
481d5c7dd7 | ||
|
|
bd407a7a23 | ||
|
|
81d235e1f9 | ||
|
|
a6e8e815e1 | ||
|
|
20bc080cbe | ||
|
|
9489190f9b | ||
|
|
481b92c289 | ||
|
|
adcc8e3e5c | ||
|
|
d9fff0a2e7 | ||
|
|
ebe192cc51 | ||
|
|
fbe0f1a83b | ||
|
|
5f100521aa | ||
|
|
dcc2e5b777 | ||
|
|
3152844182 | ||
|
|
6cd4d53aaa | ||
|
|
de157d98f1 | ||
|
|
64a914bb6c | ||
|
|
b392c931ef | ||
|
|
57dce27409 | ||
|
|
7bbf5f082e | ||
|
|
6a90502eaa | ||
|
|
be07b8fe0f | ||
|
|
802765a6b4 | ||
|
|
8e4d85d37b | ||
|
|
ae17a6f937 | ||
|
|
cafc5540c0 | ||
|
|
ccc6c717d2 | ||
|
|
944cefd0b5 | ||
|
|
8bd2e87737 | ||
|
|
9927a4abcb | ||
|
|
b087de7b13 | ||
|
|
3760109621 | ||
|
|
c44514f19e | ||
|
|
d197607055 | ||
|
|
5b4b835a45 | ||
|
|
9cb6173c3f | ||
|
|
04fb2f80b8 | ||
|
|
434e4c77b4 | ||
|
|
fa86a2f729 | ||
|
|
3e8cc7d241 | ||
|
|
9672377eff | ||
|
|
2d25ff6801 | ||
|
|
23c32a9679 | ||
|
|
192a3f8cab | ||
|
|
d48e2e167f | ||
|
|
0fc449bb3f | ||
|
|
0ccfcf9368 | ||
|
|
f920a0ad6b | ||
|
|
ad03eb5e71 | ||
|
|
968ab66658 | ||
|
|
531c0a2ec0 | ||
|
|
361b089ae6 | ||
|
|
f868deee6b | ||
|
|
e24dd51f77 | ||
|
|
a8c3bf86bf | ||
|
|
f77ee2c518 | ||
|
|
126768e0b4 | ||
|
|
a0088a70e2 | ||
|
|
64860019bc | ||
|
|
b899d8cd97 | ||
|
|
fc0f79e339 | ||
|
|
60050841a2 | ||
|
|
bc8d0d0423 | ||
|
|
75de8e0572 | ||
|
|
9b1198ce7b | ||
|
|
f1cb9c64fd | ||
|
|
80af50a637 | ||
|
|
0ac69e8f89 | ||
|
|
2b3a76cf5b | ||
|
|
a4acab9eff | ||
|
|
8dd09eaf20 | ||
|
|
085b060c4c | ||
|
|
fb34fc1654 | ||
|
|
ca9f37c05f | ||
|
|
2ebdcb6139 | ||
|
|
1300e4fe2d | ||
|
|
ee5c71754a | ||
|
|
e0790dda10 | ||
|
|
ca30b7d361 | ||
|
|
6697c05014 | ||
|
|
56e70074d0 | ||
|
|
7e0b5e85cd | ||
|
|
e7154969aa | ||
|
|
471e76be64 | ||
|
|
25908d5f96 | ||
|
|
42f5d6cc13 | ||
|
|
e3a9edcb7a | ||
|
|
cf1c863b2b | ||
|
|
f7db036c73 | ||
|
|
49cb9edfa7 | ||
|
|
0fd583ee05 | ||
|
|
a33fee2435 | ||
|
|
de107e5639 | ||
|
|
339ce9b24c | ||
|
|
07b6dcd9ea | ||
|
|
560a3b67f0 | ||
|
|
d8ae013339 | ||
|
|
ad06c09272 | ||
|
|
b0d38931d0 | ||
|
|
30f997054b | ||
|
|
bc73878e2a | ||
|
|
2bdaa1ca68 | ||
|
|
949633567d | ||
|
|
ee68edecae | ||
|
|
6b055d5133 | ||
|
|
c39e42692c | ||
|
|
34dc6533b2 | ||
|
|
46bf926ee3 | ||
|
|
54389ff03a | ||
|
|
13a76e983e | ||
|
|
34b84f3151 | ||
|
|
d65e9c8ec4 | ||
|
|
29263dad18 | ||
|
|
b70431b8f6 | ||
|
|
57c7b6e15c | ||
|
|
9b0a44d385 | ||
|
|
c0bec3f32e | ||
|
|
2510bae3c2 | ||
|
|
1dad0e5c1e | ||
|
|
a9be96bbd2 | ||
|
|
1625842d80 | ||
|
|
9af2a858cb | ||
|
|
942fcbd2d0 | ||
|
|
67566c475b | ||
|
|
e1bf8c6582 | ||
|
|
069d773c94 | ||
|
|
82029ecfbb | ||
|
|
7070fac7a6 | ||
|
|
2653b9d303 | ||
|
|
addf6fda01 | ||
|
|
4a2651851b | ||
|
|
2f80bab68f | ||
|
|
a136a66af7 | ||
|
|
aab38f089a | ||
|
|
8dd1b8c4cc | ||
|
|
d8e3b30a83 | ||
|
|
6b9a0c0277 | ||
|
|
774cc91a4e | ||
|
|
0d185aa777 | ||
|
|
4434205bc0 | ||
|
|
9fc8a2724d | ||
|
|
94a5098f74 | ||
|
|
5c788efbe1 | ||
|
|
dc9bbc95d5 | ||
|
|
29e408bf72 | ||
|
|
9398a2f092 | ||
|
|
bed66ab743 | ||
|
|
ed931e2404 | ||
|
|
afa3d6d70c | ||
|
|
d616d269f9 | ||
|
|
ea1ef9c283 | ||
|
|
639fcf2a3e | ||
|
|
89bf939860 | ||
|
|
c635152b92 | ||
|
|
7e8d562358 | ||
|
|
320aab30bc | ||
|
|
968cf8c336 | ||
|
|
f4f22cb733 | ||
|
|
48239abdd3 | ||
|
|
2c40e5f217 | ||
|
|
38550465eb | ||
|
|
8f73866c87 | ||
|
|
d1625d7384 | ||
|
|
e22c93ed4c | ||
|
|
a229d08410 | ||
|
|
35aec6c7cb | ||
|
|
58c4abdd10 | ||
|
|
08e9272440 | ||
|
|
5c9303e6e3 | ||
|
|
554c6388b3 | ||
|
|
f21d0de52e | ||
|
|
8dc61e4eb0 | ||
|
|
270fcf4299 | ||
|
|
77cfe40954 | ||
|
|
6c4f743f38 | ||
|
|
65a5a67624 | ||
|
|
abaac8c3f7 | ||
|
|
e1406025a9 | ||
|
|
3312f915ee | ||
|
|
3809c909f0 | ||
|
|
388f208bb2 | ||
|
|
55ef1e8392 | ||
|
|
576535eaf7 | ||
|
|
78e3b5d282 | ||
|
|
ff96dabfcf | ||
|
|
3dece4143a | ||
|
|
092674a6b8 | ||
|
|
4d46a3beb4 | ||
|
|
edccda93bf | ||
|
|
2d10da8267 | ||
|
|
64d22caf90 | ||
|
|
c09d983f2c | ||
|
|
503c35172d | ||
|
|
57906d9ddc | ||
|
|
0e2fdcdfef | ||
|
|
83f6814775 | ||
|
|
1bb21b1ad7 | ||
|
|
fb62d65095 | ||
|
|
ad1423767a | ||
|
|
ef661cc1d1 | ||
|
|
be445608ce | ||
|
|
075ee44a6c | ||
|
|
407e6dd824 | ||
|
|
5b86da8b8b | ||
|
|
39d9982ae4 | ||
|
|
530b985bc4 | ||
|
|
ab8afe34b1 | ||
|
|
26b4d85352 | ||
|
|
558a2f91b2 | ||
|
|
6b9381b859 | ||
|
|
0cfa725615 | ||
|
|
7d74867058 | ||
|
|
bf3a3e9e00 | ||
|
|
bcca5c2329 | ||
|
|
25c142a7aa | ||
|
|
2d53706de7 | ||
|
|
ae6f82e0ee | ||
|
|
219685e26f | ||
|
|
1f892946c9 | ||
|
|
cdacf75b02 |
@@ -14,6 +14,12 @@
|
||||
|
||||
- **Run a test:** `./bin/mage run-tests namespace/test-name`
|
||||
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
|
||||
- **Run all tests for a module:** `./bin/mage run-tests test/metabase/notification` Because the module lives in that directory.
|
||||
|
||||
Note: the `./bin/mage run-tests` command accepts multiple args, so you can pass
|
||||
`./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test`
|
||||
to run 3 tests, or
|
||||
`./bin/mage run-tests test/metabase/module1 test/metabase/module2` to run 2 modules.
|
||||
|
||||
## Code Readability
|
||||
|
||||
|
||||
@@ -752,7 +752,10 @@
|
||||
;; `metabase-enterprise.sandbox.query-processor*-test`
|
||||
;;
|
||||
{:pattern "^metabase(?:((?:-enterprise\\.sandbox)?\\.query-processor)|(?:\\.driver\\.sql(?!server)(?!ite))).*-test$"
|
||||
:name general-qp-and-driver-test-namespaces}]
|
||||
:name general-qp-and-driver-test-namespaces}
|
||||
|
||||
{:pattern "(?:^mage.*)"
|
||||
:name mage-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
@@ -947,4 +950,11 @@
|
||||
toucan2.tools.with-temp/with-temp {:message "Use lib.tu/mock-metadata-provider in QP tests. Exceptions: tests for code that doesn't use MPs or models not supported by MPs"}}
|
||||
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests
|
||||
{:level :warning}}}}}
|
||||
{:level :warning}}}
|
||||
|
||||
mage-namespaces
|
||||
{:linters
|
||||
{:discouraged-java-method
|
||||
{java.lang.System {exit
|
||||
{:level :error
|
||||
:message "Don't use System/exit directly. Instead throw an ex-info with {:babashka/exit <exit-code>} (and optionally :mage/error) let the mage runner handle exiting the process"}}}}}}}
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
:api #{metabase.bookmarks.api}
|
||||
:uses #{api
|
||||
app-db
|
||||
collections
|
||||
config
|
||||
premium-features
|
||||
queries
|
||||
@@ -679,6 +680,7 @@
|
||||
metabase.lib.join.util
|
||||
metabase.lib.metadata
|
||||
metabase.lib.metadata.cached-provider
|
||||
metabase.lib.metadata.column
|
||||
metabase.lib.metadata.invocation-tracker
|
||||
metabase.lib.metadata.protocols
|
||||
metabase.lib.metadata.result-metadata
|
||||
@@ -1530,6 +1532,7 @@
|
||||
api
|
||||
app-db
|
||||
audit-app
|
||||
config
|
||||
driver
|
||||
lib
|
||||
lib-be
|
||||
@@ -1554,6 +1557,7 @@
|
||||
lib
|
||||
models
|
||||
parameters
|
||||
premium-features
|
||||
query-processor
|
||||
request
|
||||
sync
|
||||
@@ -1613,7 +1617,6 @@
|
||||
warehouses
|
||||
enterprise/advanced-permissions}}
|
||||
|
||||
|
||||
xrays
|
||||
{:team "Querying"
|
||||
:api #{metabase.xrays.api
|
||||
@@ -1840,6 +1843,22 @@
|
||||
:api #{}
|
||||
:uses #{premium-features}}
|
||||
|
||||
enterprise/data-studio
|
||||
{:team "Gadget"
|
||||
:api #{metabase-enterprise.data-studio.api}
|
||||
:uses #{api
|
||||
collections
|
||||
database-routing
|
||||
driver
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
queries
|
||||
request
|
||||
sync
|
||||
util
|
||||
warehouse-schema}}
|
||||
|
||||
enterprise/database-replication
|
||||
{:team "Cloud"
|
||||
:api #{metabase-enterprise.database-replication.api
|
||||
@@ -1876,7 +1895,6 @@
|
||||
:uses #{analyze
|
||||
api
|
||||
collections
|
||||
config
|
||||
driver
|
||||
events
|
||||
graph
|
||||
@@ -1975,6 +1993,13 @@
|
||||
:uses #{premium-features
|
||||
settings}}
|
||||
|
||||
enterprise/library
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.library.api}
|
||||
:uses #{api
|
||||
collections
|
||||
premium-features}}
|
||||
|
||||
enterprise/llm
|
||||
{:team "Metabot"
|
||||
:api #{metabase-enterprise.llm.api
|
||||
@@ -2073,6 +2098,21 @@
|
||||
util
|
||||
enterprise/serialization}}
|
||||
|
||||
enterprise/representations
|
||||
{:team "Querying"
|
||||
:api #{metabase-enterprise.representations.core
|
||||
metabase-enterprise.representations.api
|
||||
metabase-enterprise.representations.init}
|
||||
:uses #{api
|
||||
collections
|
||||
collections-rest
|
||||
config
|
||||
driver
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
util}}
|
||||
|
||||
enterprise/sandbox
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.sandbox.api.routes
|
||||
@@ -2150,6 +2190,7 @@
|
||||
app-db
|
||||
appearance
|
||||
collections
|
||||
config
|
||||
logger
|
||||
models
|
||||
plugins
|
||||
@@ -2250,6 +2291,8 @@
|
||||
app-db
|
||||
config
|
||||
driver
|
||||
lib
|
||||
lib-be
|
||||
query-processor
|
||||
settings
|
||||
util
|
||||
|
||||
@@ -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",
|
||||
|
||||
36
.github/file-paths.yaml
vendored
36
.github/file-paths.yaml
vendored
@@ -11,9 +11,18 @@ shared_specs: &shared_specs
|
||||
- "test/**/*.cljc"
|
||||
- "test/**/*.cljs"
|
||||
|
||||
frontend_ci: &frontend_ci
|
||||
shared_ci: &shared_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/workflows/run-tests.yml"
|
||||
|
||||
shared_e2e: &shared_e2e
|
||||
- "e2e/runner/**"
|
||||
- "e2e/snapshot-creators/**"
|
||||
- "e2e/support/**"
|
||||
|
||||
frontend_ci: &frontend_ci
|
||||
- *shared_ci
|
||||
- ".github/workflows/frontend.yml"
|
||||
|
||||
frontend_configs: &frontend_configs
|
||||
@@ -36,14 +45,12 @@ frontend_sources: &frontend_sources
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
# Used only in .github/workflows/conventional-commit-pr-title-reminder.yml
|
||||
frontend_embedding_sources:
|
||||
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
frontend_embedding_sdk_sources:
|
||||
- *frontend_configs
|
||||
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
|
||||
|
||||
# Used only in .github/workflows/embedding-sdk-labels-reminder.yml
|
||||
frontend_embedding_sdk_package_sources:
|
||||
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"
|
||||
|
||||
@@ -71,8 +78,7 @@ frontend_loki_ci: &frontend_loki_ci
|
||||
- ".storybook/**"
|
||||
|
||||
backend_ci: &backend_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- *shared_ci
|
||||
- ".github/actions/test-driver/**"
|
||||
- ".github/workflows/backend.yml"
|
||||
- ".github/workflows/drivers.yml"
|
||||
@@ -112,9 +118,9 @@ sources: &sources
|
||||
- *backend_sources
|
||||
|
||||
e2e_ci: &e2e_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- *shared_ci
|
||||
- ".github/actions/prepare-uberjar-artifact/**"
|
||||
- ".github/actions/fetch-artifact/action.yml"
|
||||
- ".github/actions/e2e-prepare-containers/**"
|
||||
- ".github/actions/prepare-cypress/**"
|
||||
- ".github/workflows/e2e-*"
|
||||
@@ -123,22 +129,26 @@ e2e_specs: &e2e_specs
|
||||
- e2e/!(test-host-app)**/*.cy.*.{js,ts}
|
||||
|
||||
embedding_sdk_ci: &embedding_sdk_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- *shared_ci
|
||||
- ".github/actions/prepare-uberjar-artifact/**"
|
||||
- ".github/actions/fetch-artifact/action.yml"
|
||||
- ".github/actions/prepare-cypress/**"
|
||||
- ".github/workflows/embedding-sdk*.yml"
|
||||
|
||||
embedding_sdk_components:
|
||||
- *default
|
||||
- *embedding_sdk_ci
|
||||
- *sources
|
||||
- "e2e/component/**/*.cy.*.{js,ts}"
|
||||
- *shared_e2e
|
||||
- "e2e/test-component/**/*.cy.*.{js,jsx,ts,tsx}"
|
||||
|
||||
embedding_sdk_host_sample_apps:
|
||||
- *default
|
||||
- *embedding_sdk_ci
|
||||
- *frontend_sources
|
||||
- *backend_sources
|
||||
- *shared_e2e
|
||||
- "e2e/embedding-sdk-host-apps/**"
|
||||
- "e2e/runner/embedding-sdk/**"
|
||||
- "e2e/test-host-app/**/*.cy.*.{js,ts}"
|
||||
|
||||
snowplow: &snowplow
|
||||
|
||||
5
.github/team.json
vendored
5
.github/team.json
vendored
@@ -50,7 +50,7 @@
|
||||
"nemanjaglumac"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"name": "Metabot",
|
||||
"label": ".Team/Metabot",
|
||||
"members": [
|
||||
@@ -92,7 +92,8 @@
|
||||
"deniskaber",
|
||||
"alxnddr",
|
||||
"bpander",
|
||||
"andsalves"
|
||||
"andsalves",
|
||||
"gantoreno"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
44
.github/workflows/cache-generator.yml
vendored
44
.github/workflows/cache-generator.yml
vendored
@@ -51,14 +51,19 @@ jobs:
|
||||
eslint-cache-key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
|
||||
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
generate-m2-cache:
|
||||
generate-caches:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
needs: get-cache-keys
|
||||
env:
|
||||
M2_CACHE_KEY: ${{ needs.get-cache-keys.outputs.m2-cache-key }}
|
||||
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
|
||||
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
|
||||
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Backend preparation and cache
|
||||
- name: Prepare backend
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
@@ -75,16 +80,7 @@ jobs:
|
||||
~/.gitlibs
|
||||
key: ${{ env.M2_CACHE_KEY }}
|
||||
|
||||
generate-frontend-caches:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 15
|
||||
needs: get-cache-keys
|
||||
env:
|
||||
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
|
||||
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
|
||||
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Frontend preparation and caches
|
||||
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
|
||||
# It then (3) runs `yarn install --frozen-lockfile --prefer-offline` which ensures the local cache is fully populated.
|
||||
# IMPORTANT: Caching node_modules after patches are already applied will break any PR that attempts to either:
|
||||
@@ -111,6 +107,11 @@ jobs:
|
||||
yarn cypress verify
|
||||
shell: bash
|
||||
|
||||
# Run Prettier to populate its cache (stored in node_modules/.cache/prettier)
|
||||
# so it gets saved as part of the node_modules cache below
|
||||
- name: Run Prettier to populate cache
|
||||
run: yarn lint-prettier-pure
|
||||
|
||||
- name: Update node_modules cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
@@ -119,14 +120,21 @@ jobs:
|
||||
node_modules
|
||||
key: ${{ env.NODE_CACHE_KEY }}
|
||||
|
||||
- name: Update eslint cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ${{ github.workspace}}/.eslintcache
|
||||
key: ${{ env.ESLINT_CACHE_KEY }}
|
||||
|
||||
- name: Update Cypress cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ env.CYPRESS_CACHE_KEY }}
|
||||
|
||||
# ESLint cache (requires both backend and frontend)
|
||||
- name: Compile CLJS
|
||||
run: yarn build-pure:cljs
|
||||
|
||||
- name: Run ESLint to populate cache
|
||||
run: yarn lint-eslint-pure
|
||||
|
||||
- name: Update eslint cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ${{ github.workspace}}/.eslintcache
|
||||
key: ${{ env.ESLINT_CACHE_KEY }}
|
||||
|
||||
1
.github/workflows/claude-docs-review.yml
vendored
1
.github/workflows/claude-docs-review.yml
vendored
@@ -23,6 +23,7 @@ concurrency:
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
12
.github/workflows/drivers.yml
vendored
12
.github/workflows/drivers.yml
vendored
@@ -972,12 +972,22 @@ jobs:
|
||||
test-args: >-
|
||||
:only-tags [:mb/upload-tests]
|
||||
:fail-fast? true
|
||||
- name: Redshift (Excluding Upload Tests)
|
||||
- name: Redshift (Excluding Upload Tests) Part 1
|
||||
skip: false
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: Redshift (Excluding Upload Tests) Part 2
|
||||
skip: false
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags '[:mb/upload-tests :mb/transforms-python-test]'
|
||||
:fail-fast? true
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
- name: Redshift Transforms Python Tests
|
||||
skip: false
|
||||
test-args: >-
|
||||
|
||||
23
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
23
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
@@ -22,14 +22,10 @@ on:
|
||||
required: false
|
||||
default: "ee"
|
||||
qa_db:
|
||||
description: "Select the type of Database you want to run your tests with"
|
||||
type: choice
|
||||
description: "Whether to set up and include external QA databases"
|
||||
type: boolean
|
||||
required: true
|
||||
default: "none"
|
||||
options:
|
||||
- none
|
||||
- sql
|
||||
- mongo
|
||||
default: false
|
||||
enable_network_throttling:
|
||||
description: "Enable network throttling simulation (4g slow like in chrome dev tools)"
|
||||
type: boolean
|
||||
@@ -81,8 +77,6 @@ jobs:
|
||||
name: Stress test E2E flake fix
|
||||
env:
|
||||
DISPLAY: ""
|
||||
QA_DB_ENABLED: ${{ contains(fromJSON('["sql", "mongo"]'), inputs.qa_db) }}
|
||||
CYPRESS_QA_DB_MONGO: ${{ inputs.qa_db == 'mongo' }}
|
||||
MB_RUN_MODE: e2e
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
@@ -109,9 +103,9 @@ jobs:
|
||||
maildev: true
|
||||
openldap: true
|
||||
snowplow: true
|
||||
postgres: ${{ inputs.qa_db == 'sql' }}
|
||||
mysql: ${{ inputs.qa_db == 'sql' }}
|
||||
mongo: ${{ inputs.qa_db == 'mongo' }}
|
||||
postgres: ${{ inputs.qa_db }}
|
||||
mysql: ${{ inputs.qa_db }}
|
||||
mongo: ${{ inputs.qa_db }}
|
||||
|
||||
- name: Download Metabase ${{ matrix.edition }} uberjar
|
||||
uses: ./.github/actions/e2e-download-uberjar
|
||||
@@ -140,8 +134,11 @@ jobs:
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Generate database snapshots
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--env grepTags="${{inputs.qa_db && '' || '-@external'}}"
|
||||
|
||||
# For all options and fine-grained control, take a look at the documentation
|
||||
# https://github.com/cypress-io/cypress/tree/develop/npm/grep
|
||||
|
||||
8
.github/workflows/e2e-test.yml
vendored
8
.github/workflows/e2e-test.yml
vendored
@@ -37,7 +37,6 @@ jobs:
|
||||
env:
|
||||
MB_EDITION: ${{ inputs.edition }}
|
||||
DISPLAY: ""
|
||||
QA_DB_ENABLED: true
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HASH: ${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }}
|
||||
@@ -58,7 +57,6 @@ jobs:
|
||||
MB_SNOWPLOW_URL: "http://localhost:9090" # Snowplow micro
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
CYPRESS_QA_DB_MONGO: ${{ inputs.name == 'mongo' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -80,9 +78,9 @@ jobs:
|
||||
openldap: true
|
||||
webhook: true
|
||||
snowplow: true
|
||||
postgres: ${{ inputs.name != 'mongo' }}
|
||||
mysql: ${{ inputs.name != 'mongo' }}
|
||||
mongo: ${{ inputs.name == 'mongo' }}
|
||||
postgres: true
|
||||
mysql: true
|
||||
mongo: true
|
||||
|
||||
- name: Retrieve uberjar artifact for ${{ inputs.edition }}
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
|
||||
@@ -62,7 +62,10 @@ jobs:
|
||||
- name: Make app db snapshot
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
# Component tests only need the default snapshot
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
|
||||
# Has to be run after the app db snapshot is made, as it is not compatible with React 19
|
||||
- name: Change React version to ${{ matrix.react-version }}
|
||||
@@ -201,7 +204,10 @@ jobs:
|
||||
- name: Make app db snapshot
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
# Component tests only need the default snapshot
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
env:
|
||||
|
||||
@@ -278,7 +278,9 @@ jobs:
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Generate database snapshots
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js snapshot \
|
||||
--spec "e2e/snapshot-creators/default.cy.snap.js"
|
||||
|
||||
- name: Prepare and launch Host App
|
||||
run: npx tsx ./e2e/runner/embedding-sdk/host-apps/start-ci.ts ${{ matrix.test_suite }}
|
||||
|
||||
14
.github/workflows/embedding-sdk.yml
vendored
14
.github/workflows/embedding-sdk.yml
vendored
@@ -23,6 +23,10 @@ on:
|
||||
description: "Whether to run host and sample apps compatibility tests."
|
||||
required: true
|
||||
type: boolean
|
||||
skip:
|
||||
description: Whether to skip running tests due to one of the artifacts failing to build.
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
@@ -30,7 +34,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
embedding-sdk-cli-snippets-type-check:
|
||||
if: inputs.cli-snippets-type-check
|
||||
if: inputs.cli-snippets-type-check && !inputs.skip
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -46,7 +50,7 @@ jobs:
|
||||
run: yarn run embedding-sdk:cli-snippets:type-check
|
||||
|
||||
embedding-sdk-docs-snippets-type-check:
|
||||
if: inputs.docs-snippets-type-check
|
||||
if: inputs.docs-snippets-type-check && !inputs.skip
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -62,7 +66,7 @@ jobs:
|
||||
run: yarn run embedding-sdk:docs-snippets:type-check
|
||||
|
||||
embedding-sdk-api-documentation-generation-validation:
|
||||
if: inputs.documentation-validation
|
||||
if: inputs.documentation-validation && !inputs.skip
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -110,13 +114,13 @@ jobs:
|
||||
# See `Component e2e tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
embedding-sdk-component-tests:
|
||||
name: "SDK component tests"
|
||||
if: inputs.component-tests
|
||||
if: inputs.component-tests && !inputs.skip
|
||||
uses: ./.github/workflows/embedding-sdk-component-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
not-required-embedding-sdk-apps-compatibility-tests:
|
||||
name: "SDK e2e tests for compatibility with (host/sample) apps"
|
||||
if: inputs.host-sample-apps-tests
|
||||
if: inputs.host-sample-apps-tests && !inputs.skip
|
||||
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
|
||||
12
.github/workflows/run-tests.yml
vendored
12
.github/workflows/run-tests.yml
vendored
@@ -25,7 +25,6 @@ jobs:
|
||||
frontend_all: ${{ steps.changes.outputs.frontend_all }}
|
||||
frontend_sources: ${{ steps.changes.outputs.frontend_sources }}
|
||||
documentation: ${{ steps.changes.outputs.documentation }}
|
||||
frontend_embedding_sdk_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_sources }}
|
||||
embedding_documentation: ${{ steps.changes.outputs.embedding_documentation }}
|
||||
embedding_sdk_components: ${{ steps.changes.outputs.embedding_sdk_components }}
|
||||
embedding_sdk_host_sample_apps: ${{ steps.changes.outputs.embedding_sdk_host_sample_apps }}
|
||||
@@ -102,7 +101,7 @@ jobs:
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(needs.files-changed.outputs.embedding_sdk_components == 'true' ||
|
||||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' ||
|
||||
needs.files-changed.outputs.frontend_sources == 'true' ||
|
||||
needs.files-changed.outputs.embedding_documentation == 'true' ||
|
||||
needs.files-changed.outputs.embedding_sdk_ci == 'true')
|
||||
uses: ./.github/workflows/embedding-sdk-package-build.yml
|
||||
@@ -110,17 +109,16 @@ jobs:
|
||||
|
||||
sdk-tests:
|
||||
needs: [uberjar, embedding-sdk-package, files-changed]
|
||||
if: |
|
||||
needs.embedding-sdk-package.result == 'success' &&
|
||||
needs.uberjar.result == 'success'
|
||||
if: always()
|
||||
uses: ./.github/workflows/embedding-sdk.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
|
||||
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}
|
||||
docs-snippets-type-check: ${{ needs.files-changed.outputs.embedding_documentation == 'true' }}
|
||||
documentation-validation: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
|
||||
documentation-validation: ${{ needs.files-changed.outputs.frontend_sources == 'true' }}
|
||||
component-tests: ${{ needs.files-changed.outputs.embedding_sdk_components == 'true' }}
|
||||
host-sample-apps-tests: ${{ needs.files-changed.outputs.embedding_sdk_host_sample_apps == 'true' }}
|
||||
skip: ${{ needs.embedding-sdk-package.result != 'success' || needs.uberjar.result != 'success' }}
|
||||
|
||||
pr-env:
|
||||
needs: [uberjar]
|
||||
|
||||
8
.github/workflows/uberjar.yml
vendored
8
.github/workflows/uberjar.yml
vendored
@@ -146,7 +146,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Is ${{ matrix.edition }} (java ${{ matrix.java-version }}) healthy?
|
||||
needs: [build, check-existing-artifacts]
|
||||
if: always() && !cancelled() && (needs.build.result == 'success' || (needs.check-existing-artifacts.outputs.should_build == 'false' && needs.check-existing-artifacts.outputs.ee_exists == 'true' && needs.check-existing-artifacts.outputs.oss_exists == 'true'))
|
||||
# We only care about the explicit JAR health check on push to `master` and the `release-` branches.
|
||||
# JARs created in random branches on PRs are 99% of the time treated as ephemeral, and used only in e2e tests.
|
||||
# E2E tests already have a step where they wait for the instance health check.
|
||||
if: |
|
||||
always() && !cancelled() &&
|
||||
(github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) &&
|
||||
(needs.build.result == 'success' || (needs.check-existing-artifacts.outputs.should_build == 'false' && needs.check-existing-artifacts.outputs.ee_exists == 'true' && needs.check-existing-artifacts.outputs.oss_exists == 'true'))
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 56 KiB |
56
bb.edn
56
bb.edn
@@ -3,10 +3,11 @@
|
||||
;; so the namespaces are mage.cli, mage.format, etc.
|
||||
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
|
||||
:deps {dev.weavejester/cljfmt {:mvn/version "0.15.3"}
|
||||
org.babashka/json {:mvn/version "0.1.6"}
|
||||
io.github.paintparty/bling {:mvn/version "0.8.8"}
|
||||
metosin/malli {:mvn/version "0.17.0"}
|
||||
table/table {:mvn/version "0.5.0"}
|
||||
mvxcvi/puget {:mvn/version "1.3.4"}}
|
||||
mvxcvi/puget {:mvn/version "1.3.4"}
|
||||
org.babashka/json {:mvn/version "0.1.6"}
|
||||
table/table {:mvn/version "0.5.0"}}
|
||||
:tasks
|
||||
{:requires [[mage.cli :as cli]
|
||||
[mage.color :as c]]
|
||||
@@ -186,24 +187,21 @@
|
||||
(nrepl-eval "metabase-enterprise.comments.core" code)))}
|
||||
|
||||
run-tests
|
||||
{:doc "Run a tests (or a single test) against nrepl server"
|
||||
:requires [[clojure.string :as str]
|
||||
[mage.be-dev :refer [nrepl-eval]]]
|
||||
:arg-schema [:tuple [:string {:name "NS/TEST" :desc "or just NS"}]]
|
||||
:options [["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:task (let [{:keys [options]
|
||||
[test] :arguments} (cli/parse! (current-task))
|
||||
test (if (or (str/starts-with? test "test/")
|
||||
(str/starts-with? test "enterprise/"))
|
||||
;; paths should come in as strings
|
||||
(format "\"%s\"" test)
|
||||
;; ns or var should be a symbol
|
||||
(str "'" test))
|
||||
code (format
|
||||
"(do ((requiring-resolve 'dev.reload/reload!)) (find-and-run-tests-repl {:only %s}))"
|
||||
test)]
|
||||
(nrepl-eval "mb.hawk.core" (str code) (:port options)))}
|
||||
|
||||
{:doc "Quickly run tests locally. Pass dirs/files as arguments to run them."
|
||||
:examples [["./bin/mage run-tests" "launch interactive test picker"]
|
||||
["./bin/mage run-tests test/metabase/db" "run tests in test/metabase/db"]
|
||||
["./bin/mage run-tests test/metabase/logger test/metabase/api_keys" "run tests in test/metabase/logger and src/metabase/api_keys"]
|
||||
["./bin/mage run-tests test/metabase/config_test.clj" "run all tests in the metabase.config-test namespace"]]
|
||||
;; TODO: add option to select files as well as dirs from fzf with styles
|
||||
:options [["-s" "--selecting STYLE" "Style of test selection" :default "all"]
|
||||
;; all, file, dir
|
||||
["-p" "--port PORT" "Port to use for the task, defaults to value in .nrepl-port"]]
|
||||
:arg-schema [:sequential
|
||||
{:details "If none are given, you will be prompted to select directories recursively from test/ and enterprise/backend/test/"}
|
||||
[:string {:description "Files or directories (recursive) to test"}]]
|
||||
:requires [[mage.quick-test-runner :as qtr]]
|
||||
:task (task! (qtr/go parsed))}
|
||||
|
||||
affected-modules
|
||||
{:doc "Prints information about which backend modules have changes compared to a Git ref e.g. `master`"
|
||||
:examples [["./bin/mage affected-modules master"]
|
||||
@@ -218,10 +216,11 @@
|
||||
:requires [[mage.modules]]
|
||||
:task (task! (mage.modules/cli-can-skip-driver-tests (:arguments parsed)))}
|
||||
|
||||
ls {:doc "List all mage public and private tasks"
|
||||
:examples [["./bin/mage ls" "List all tasks"]]
|
||||
:requires [[mage.util :as u]]
|
||||
:task (task! (u/print-tasks parsed))}
|
||||
ls
|
||||
{:doc "List all mage public and private tasks"
|
||||
:examples [["./bin/mage ls" "List all tasks"]]
|
||||
:requires [[mage.util :as u]]
|
||||
:task (task! (u/print-tasks parsed))}
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Private Tasks:
|
||||
@@ -276,8 +275,7 @@
|
||||
(apply readability/check arguments))}
|
||||
|
||||
;; Uncomment this if you want to play with the example calculator task:
|
||||
#_#_
|
||||
-example-calculator
|
||||
#_#_-example-calculator
|
||||
{;; `:doc` is a short docstring for the task, will be listed in `./bin/mage -h` and other help info.
|
||||
;; Keep these short, they need to fit on one line in the output from `./bin/mage -h`.
|
||||
:doc "The example calculator's task docstring"
|
||||
@@ -334,6 +332,6 @@
|
||||
-escherize-fix-whitespace
|
||||
{:doc "Removes all lines like '^<space>+$' from files in glob pattern."
|
||||
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
|
||||
:examples [["mage -escherize-fix-whitespace '**/*.clj'" "Removes all lines like ' ' from all clj files"]]
|
||||
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
|
||||
:arg-schema [:sequential [:string {:description "Glob pattern to match files"}]]
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
|
||||
60
bin/build-backend-for-test
Executable file
60
bin/build-backend-for-test
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Build a backend-only uberjar for e2e testing.
|
||||
#
|
||||
# This script builds a minimal backend uberjar (metabase-backend.jar) that contains
|
||||
# only the Clojure backend code. It is NOT the same as the all-in-one metabase.jar
|
||||
# which includes backend, frontend, static assets, and other resources.
|
||||
#
|
||||
# The backend-only jar is used for e2e tests where the frontend is served separately
|
||||
# and we only need the backend API server.
|
||||
#
|
||||
# Features:
|
||||
# - Outputs to target/uberjar/metabase-backend.jar
|
||||
# - Caches builds based on source file hashes to avoid unnecessary rebuilds
|
||||
# - Embeds a source hash in version.properties to detect when rebuilds are needed
|
||||
|
||||
set -eu
|
||||
|
||||
readonly VERSION_PROPERTY_NAME="src_hash"
|
||||
readonly JAR_FILENAME="metabase-backend.jar"
|
||||
readonly JAR_PATH="target/uberjar/$JAR_FILENAME"
|
||||
|
||||
backend-source-hash() {
|
||||
# hash all the files that might change a backend-only uberjar build
|
||||
(
|
||||
find src deps.edn resources/sample-database.db.mv.db -type f -print0 | xargs -0 shasum ;
|
||||
find resources -type f \( -iname \*.clj -o -iname \*.edn -o -iname \*.yaml -o -iname \*.properties -o -iname \*.html \) -not -name "version.properties" -print0 | xargs -0 shasum ;
|
||||
) | shasum | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
backend-jar-hash() {
|
||||
# pulling the version.properties directly from the jar is much faster than starting the JVM
|
||||
unzip -c "$JAR_PATH" version.properties | grep "$VERSION_PROPERTY_NAME" | cut -f2 -d=
|
||||
}
|
||||
|
||||
check-backend-jar-hash() {
|
||||
local expected_hash actual_hash
|
||||
expected_hash=$(backend-source-hash)
|
||||
actual_hash=$(backend-jar-hash)
|
||||
if [ "$expected_hash" == "$actual_hash" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
build-backend-uberjar() {
|
||||
local source_hash
|
||||
source_hash=$(backend-source-hash)
|
||||
./bin/build.sh :steps [:version]
|
||||
echo -e "\n$VERSION_PROPERTY_NAME=$source_hash" >> resources/version.properties
|
||||
MB_JAR_FILENAME="$JAR_FILENAME" ./bin/build.sh :steps [:uberjar]
|
||||
}
|
||||
|
||||
if [ ! -f "$JAR_PATH" ] || ! check-backend-jar-hash; then
|
||||
echo "Building backend uberjar for e2e testing ($JAR_FILENAME)"
|
||||
build-backend-uberjar
|
||||
else
|
||||
echo "Backend uberjar already up to date for e2e testing ($JAR_FILENAME)"
|
||||
fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
VERSION_PROPERTY_NAME="src_hash"
|
||||
|
||||
source-hash() {
|
||||
# hash all the files that might change a backend-only uberjar build (for integration tests)
|
||||
(
|
||||
find src deps.edn resources/sample-database.db.mv.db -type f -print0 | xargs -0 shasum ;
|
||||
find resources -type f \( -iname \*.clj -o -iname \*.edn -o -iname \*.yaml -o -iname \*.properties -o -iname \*.html \) -not -name "version.properties" -print0 | xargs -0 shasum ;
|
||||
) | shasum | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
uberjar-hash() {
|
||||
# java --add-opens java.base/java.nio=ALL-UNNAMED -jar target/uberjar/metabase.jar version | grep -oE 'source_hash [a-f0-9]+' | awk '{ print $2 }'
|
||||
# pulling the version.properties directly from the jar is much faster
|
||||
unzip -c target/uberjar/metabase.jar version.properties | grep "$VERSION_PROPERTY_NAME" | cut -f2 -d=
|
||||
}
|
||||
|
||||
check-uberjar-hash() {
|
||||
expected_hash=$(source-hash)
|
||||
actual_hash=$(uberjar-hash)
|
||||
if [ "$expected_hash" == "$actual_hash" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
build-uberjar-for-test() {
|
||||
./bin/build.sh :steps [:version]
|
||||
echo -e "\n$VERSION_PROPERTY_NAME=$(source-hash)" >> resources/version.properties
|
||||
./bin/build.sh :steps [:uberjar]
|
||||
}
|
||||
|
||||
if [ ! -f "target/uberjar/metabase.jar" ] || ! check-uberjar-hash; then
|
||||
echo "Building uberjar for testing"
|
||||
build-uberjar-for-test
|
||||
else
|
||||
echo "Uberjar already up to date for testing"
|
||||
fi
|
||||
@@ -18,9 +18,18 @@
|
||||
(def ^:private class-dir
|
||||
(u/filename u/project-root-directory "target" "classes"))
|
||||
|
||||
(def ^:private uberjar-dir
|
||||
"Canonical directory for all uberjar builds."
|
||||
(u/filename u/project-root-directory "target" "uberjar"))
|
||||
|
||||
(def ^:private artifact-name
|
||||
"The uberjar artifact name, customizable via the MB_JAR_FILENAME env var."
|
||||
(or (System/getenv "MB_JAR_FILENAME")
|
||||
"metabase.jar"))
|
||||
|
||||
(def uberjar-filename
|
||||
"Target filename for the Metabase uberjar."
|
||||
(u/filename u/project-root-directory "target" "uberjar" "metabase.jar"))
|
||||
"Full path to the Metabase uberjar, including the artifact name."
|
||||
(u/filename uberjar-dir artifact-name))
|
||||
|
||||
(defn- do-with-duration-ms [thunk f]
|
||||
(let [timer (u/start-timer)
|
||||
@@ -164,7 +173,7 @@
|
||||
to do it by hand for the time being."
|
||||
[]
|
||||
(u/step "Update META-INF/MANIFEST.MF"
|
||||
(u/with-open-jar-file-system [fs "target/uberjar/metabase.jar"]
|
||||
(u/with-open-jar-file-system [fs uberjar-filename]
|
||||
(let [manifest-path (u/get-path-in-filesystem fs "META-INF" "MANIFEST.MF")]
|
||||
(with-open [os (Files/newOutputStream manifest-path (into-array OpenOption [StandardOpenOption/WRITE
|
||||
StandardOpenOption/TRUNCATE_EXISTING]))]
|
||||
@@ -184,5 +193,4 @@
|
||||
(copy-resources! basis)
|
||||
(create-uberjar! basis)
|
||||
(update-manifest!))
|
||||
(u/announce "Built target/uberjar/metabase.jar in %.1f seconds."
|
||||
(/ duration-ms 1000.0)))))
|
||||
(u/announce "Built %s in %.1f seconds." uberjar-filename (/ duration-ms 1000.0)))))
|
||||
|
||||
389
bin/dev-requirements
Executable file
389
bin/dev-requirements
Executable file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print functions
|
||||
print_header() {
|
||||
echo -e "\n${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}${BLUE} $1${NC}"
|
||||
echo -e "${BOLD}${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
|
||||
}
|
||||
|
||||
print_section() {
|
||||
echo -e "\n${BOLD}$1${NC}"
|
||||
echo -e "${BOLD}$(printf '─%.0s' {1..63})${NC}"
|
||||
}
|
||||
|
||||
print_found() {
|
||||
echo -e " ${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_not_found() {
|
||||
echo -e " ${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e " ${YELLOW}⚠️${NC} $1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e " ${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
# Detection functions
|
||||
detect_version_managers() {
|
||||
local managers=()
|
||||
|
||||
# nvm - Node Version Manager
|
||||
if [ -d "$HOME/.nvm" ] || [ -n "${NVM_DIR:-}" ] || command -v nvm &> /dev/null; then
|
||||
managers+=("nvm")
|
||||
fi
|
||||
|
||||
# fnm - Fast Node Manager
|
||||
if command -v fnm &> /dev/null || [ -d "$HOME/.fnm" ]; then
|
||||
managers+=("fnm")
|
||||
fi
|
||||
|
||||
# n - Node version management (no subcommands)
|
||||
if command -v n &> /dev/null || [ -d "/usr/local/n" ]; then
|
||||
managers+=("n")
|
||||
fi
|
||||
|
||||
# volta - JavaScript Tool Manager
|
||||
if command -v volta &> /dev/null || [ -d "$HOME/.volta" ]; then
|
||||
managers+=("volta")
|
||||
fi
|
||||
|
||||
# asdf - Extendable version manager (multi-language)
|
||||
if [ -d "$HOME/.asdf" ] || command -v asdf &> /dev/null; then
|
||||
managers+=("asdf")
|
||||
fi
|
||||
|
||||
# mise - Dev tools version manager (formerly rtx)
|
||||
if command -v mise &> /dev/null || [ -d "$HOME/.local/share/mise" ] || [ -d "$HOME/.mise" ]; then
|
||||
managers+=("mise")
|
||||
fi
|
||||
|
||||
# jenv - Java version manager
|
||||
if [ -d "$HOME/.jenv" ] || command -v jenv &> /dev/null; then
|
||||
managers+=("jenv")
|
||||
fi
|
||||
|
||||
# SDKMAN! - Software Development Kit Manager (Java, Gradle, Maven, etc.)
|
||||
if [ -d "$HOME/.sdkman" ] || [ -n "${SDKMAN_DIR:-}" ]; then
|
||||
managers+=("sdkman")
|
||||
fi
|
||||
|
||||
echo "${managers[@]}"
|
||||
}
|
||||
|
||||
get_node_version() {
|
||||
if command -v node &> /dev/null; then
|
||||
node --version
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_yarn_version() {
|
||||
if command -v yarn &> /dev/null; then
|
||||
yarn --version
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_java_version() {
|
||||
if command -v java &> /dev/null; then
|
||||
java -version 2>&1 | head -n 1 | awk -F '"' '{print $2}'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_java_distribution() {
|
||||
if command -v java &> /dev/null; then
|
||||
local full_version=$(java -version 2>&1)
|
||||
|
||||
# Check for various distributions
|
||||
if echo "$full_version" | grep -qi "Temurin"; then
|
||||
echo "Eclipse Temurin"
|
||||
elif echo "$full_version" | grep -qi "Adoptium"; then
|
||||
echo "Eclipse Adoptium"
|
||||
elif echo "$full_version" | grep -qi "Oracle"; then
|
||||
echo "Oracle"
|
||||
elif echo "$full_version" | grep -qi "OpenJDK"; then
|
||||
echo "OpenJDK"
|
||||
elif echo "$full_version" | grep -qi "Amazon Corretto"; then
|
||||
echo "Amazon Corretto"
|
||||
elif echo "$full_version" | grep -qi "Azul Zulu"; then
|
||||
echo "Azul Zulu"
|
||||
elif echo "$full_version" | grep -qi "GraalVM"; then
|
||||
echo "GraalVM"
|
||||
else
|
||||
echo "Unknown"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
check_java_requirements() {
|
||||
local version=$1
|
||||
local distribution=$2
|
||||
local major_version=""
|
||||
|
||||
if [ -n "$version" ]; then
|
||||
# Extract major version (handles both "21.0.1" and "1.8.0" formats)
|
||||
if [[ $version =~ ^1\. ]]; then
|
||||
major_version=$(echo "$version" | cut -d. -f2)
|
||||
else
|
||||
major_version=$(echo "$version" | cut -d. -f1)
|
||||
fi
|
||||
|
||||
# Check version and distribution
|
||||
if [ "$major_version" = "21" ] && [[ "$distribution" =~ "Temurin" ]]; then
|
||||
echo "correct"
|
||||
elif [ "$major_version" = "21" ]; then
|
||||
echo "wrong_distribution"
|
||||
else
|
||||
echo "wrong_version"
|
||||
fi
|
||||
else
|
||||
echo "not_installed"
|
||||
fi
|
||||
}
|
||||
|
||||
get_clojure_version() {
|
||||
if command -v clojure &> /dev/null; then
|
||||
clojure --version 2>&1 | head -n 1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_required_clojure_version() {
|
||||
if [ -f "deps.edn" ]; then
|
||||
# Extract org.clojure/clojure version from deps.edn
|
||||
grep "org.clojure/clojure" deps.edn | grep ":mvn/version" | sed -E 's/.*"([0-9.]+)".*/\1/' | head -n 1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_babashka_version() {
|
||||
if command -v bb &> /dev/null; then
|
||||
bb --version
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
check_yarn_classic() {
|
||||
local version=$1
|
||||
if [ -n "$version" ]; then
|
||||
local major_version=$(echo "$version" | cut -d. -f1)
|
||||
if [ "$major_version" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
print_header "Metabase Development Environment Detection"
|
||||
|
||||
echo "This script detects your current development tool setup and provides"
|
||||
echo "guidance on how to prepare your environment for Metabase development."
|
||||
echo ""
|
||||
echo "Note: This script only detects and reports. It will not modify your system."
|
||||
|
||||
print_section "Version Managers Detected"
|
||||
|
||||
managers=($(detect_version_managers))
|
||||
|
||||
if [ ${#managers[@]} -eq 0 ]; then
|
||||
print_info "No version managers detected"
|
||||
echo " You may be using system-installed tools or manual installations."
|
||||
else
|
||||
for manager in "${managers[@]}"; do
|
||||
print_found "$manager"
|
||||
done
|
||||
|
||||
# Check for conflicts
|
||||
if [ ${#managers[@]} -gt 1 ]; then
|
||||
echo ""
|
||||
print_warning "Multiple version managers detected!"
|
||||
echo " Having multiple version managers can cause conflicts where different"
|
||||
echo " tools compete to manage the same runtime versions. This may lead to:"
|
||||
echo " • Unexpected version switches"
|
||||
echo " • PATH conflicts"
|
||||
echo " • Shell initialization slowdowns"
|
||||
echo ""
|
||||
echo -e " 💡 ${BOLD}Recommendation:${NC} Use only one version manager (we recommend mise)."
|
||||
fi
|
||||
fi
|
||||
|
||||
print_section "Installed Development Tools"
|
||||
|
||||
requirements_met=true
|
||||
|
||||
# Node.js
|
||||
node_version=$(get_node_version)
|
||||
if [ -n "$node_version" ]; then
|
||||
node_major=$(echo "$node_version" | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$node_major" -ge 22 ]; then
|
||||
print_found "Node.js $node_version (meets requirement: ≥22)"
|
||||
else
|
||||
print_warning "Node.js $node_version (required: ≥22)"
|
||||
requirements_met=false
|
||||
fi
|
||||
else
|
||||
print_not_found "Node.js (required: ≥22)"
|
||||
requirements_met=false
|
||||
fi
|
||||
|
||||
# Yarn
|
||||
yarn_version=$(get_yarn_version)
|
||||
if [ -n "$yarn_version" ]; then
|
||||
if check_yarn_classic "$yarn_version"; then
|
||||
print_found "Yarn $yarn_version (Yarn Classic - correct!)"
|
||||
else
|
||||
print_warning "Yarn $yarn_version (required: Yarn Classic 1.x)"
|
||||
requirements_met=false
|
||||
fi
|
||||
else
|
||||
print_not_found "Yarn (required: Yarn Classic 1.x)"
|
||||
requirements_met=false
|
||||
fi
|
||||
|
||||
# Java
|
||||
java_version=$(get_java_version)
|
||||
java_distribution=$(get_java_distribution)
|
||||
java_status=$(check_java_requirements "$java_version" "$java_distribution")
|
||||
|
||||
case "$java_status" in
|
||||
"correct")
|
||||
print_found "Java $java_version ($java_distribution) - correct!"
|
||||
;;
|
||||
"wrong_distribution")
|
||||
print_warning "Java $java_version ($java_distribution)"
|
||||
echo " Required: Java 21 (Eclipse Temurin)"
|
||||
requirements_met=false
|
||||
;;
|
||||
"wrong_version")
|
||||
print_warning "Java $java_version ($java_distribution)"
|
||||
echo " Required: Java 21 (Eclipse Temurin)"
|
||||
requirements_met=false
|
||||
;;
|
||||
"not_installed")
|
||||
print_not_found "Java (required: Java 21 Eclipse Temurin)"
|
||||
requirements_met=false
|
||||
;;
|
||||
esac
|
||||
|
||||
# Clojure CLI
|
||||
clojure_version=$(get_clojure_version)
|
||||
required_clojure_version=$(get_required_clojure_version)
|
||||
|
||||
if [ -n "$clojure_version" ]; then
|
||||
if [ -n "$required_clojure_version" ]; then
|
||||
print_found "Clojure CLI ($clojure_version) - requires Clojure $required_clojure_version"
|
||||
else
|
||||
print_found "Clojure CLI ($clojure_version)"
|
||||
fi
|
||||
else
|
||||
if [ -n "$required_clojure_version" ]; then
|
||||
print_not_found "Clojure CLI (required for Clojure $required_clojure_version)"
|
||||
else
|
||||
print_not_found "Clojure CLI"
|
||||
fi
|
||||
requirements_met=false
|
||||
fi
|
||||
|
||||
# Babashka
|
||||
bb_version=$(get_babashka_version)
|
||||
if [ -n "$bb_version" ]; then
|
||||
print_found "Babashka $bb_version"
|
||||
else
|
||||
print_not_found "Babashka (required for mage task automation)"
|
||||
requirements_met=false
|
||||
fi
|
||||
|
||||
# Show warning if requirements are not met
|
||||
if [ "$requirements_met" = false ]; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}${RED}⚠ REQUIREMENTS NOT MET ⚠${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Your current setup does not meet Metabase's requirements.${NC}"
|
||||
echo " The project will not work until the issues above are resolved."
|
||||
echo ""
|
||||
echo " Please install or update the missing/incorrect tools listed above."
|
||||
fi
|
||||
|
||||
print_section "Metabase Project Requirements"
|
||||
|
||||
required_clojure_version=$(get_required_clojure_version)
|
||||
|
||||
echo -e " ${BOLD}Required Tools:${NC}"
|
||||
echo " • Node.js ≥22 (currently specified in .nvmrc)"
|
||||
echo " • Yarn Classic 1.x (NOT Yarn 2+, NO Corepack)"
|
||||
echo " • Java 21 with Eclipse Temurin distribution (for Clojure backend)"
|
||||
if [ -n "$required_clojure_version" ]; then
|
||||
echo " • Clojure CLI (for Clojure $required_clojure_version specified in deps.edn)"
|
||||
else
|
||||
echo " • Clojure CLI"
|
||||
fi
|
||||
echo " • Babashka (for mage task automation)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Optional Tools (quality of life):${NC}"
|
||||
if command -v bat &> /dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} bat - A cat clone with syntax highlighting (https://github.com/sharkdp/bat)"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} bat - A cat clone with syntax highlighting (https://github.com/sharkdp/bat)"
|
||||
fi
|
||||
if command -v fzf &> /dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} fzf - A fuzzy finder for the terminal (https://github.com/junegunn/fzf)"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} fzf - A fuzzy finder for the terminal (https://github.com/junegunn/fzf)"
|
||||
fi
|
||||
if command -v fd &> /dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} fd - A fast alternative to find (https://github.com/sharkdp/fd)"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} fd - A fast alternative to find (https://github.com/sharkdp/fd)"
|
||||
fi
|
||||
if command -v rg &> /dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} ripgrep - A fast alternative to grep (https://github.com/BurntSushi/ripgrep)"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} ripgrep - A fast alternative to grep (https://github.com/BurntSushi/ripgrep)"
|
||||
fi
|
||||
|
||||
print_section "Recommended Setup"
|
||||
|
||||
echo -e " ${BOLD}${GREEN}We recommend using 'mise'${NC} for version management because:"
|
||||
echo " • Manages Node.js, Java, and other tools in one place"
|
||||
echo " • Automatically switches versions per-project"
|
||||
echo " • Cross-platform and actively maintained"
|
||||
echo ""
|
||||
echo " Learn more: https://mise.jdx.dev/getting-started.html"
|
||||
|
||||
# Footer
|
||||
print_header "Detection Complete"
|
||||
|
||||
echo "For detailed development environment setup, see:"
|
||||
echo " docs/developers-guide/devenv.md"
|
||||
echo ""
|
||||
echo "To re-run this detection script:"
|
||||
echo " ./bin/dev-requirements"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -9,6 +9,6 @@
|
||||
"express": "^5.0.1",
|
||||
"express-session": "^1.18.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"next": "^15.2.3"
|
||||
"next": "^15.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,185 +2,204 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@emnapi/runtime@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d"
|
||||
integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==
|
||||
"@emnapi/runtime@^1.7.0":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791"
|
||||
integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@img/sharp-darwin-arm64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz#65049ef7c6be7857da742cd028f97602ce209635"
|
||||
integrity sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==
|
||||
"@img/colour@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311"
|
||||
integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==
|
||||
|
||||
"@img/sharp-darwin-arm64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86"
|
||||
integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-darwin-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-darwin-arm64" "1.2.4"
|
||||
|
||||
"@img/sharp-darwin-x64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz#d37ff7c75c46d5a68a3756e3f1924ef7ca7b285e"
|
||||
integrity sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==
|
||||
"@img/sharp-darwin-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b"
|
||||
integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-darwin-x64" "1.1.0"
|
||||
"@img/sharp-libvips-darwin-x64" "1.2.4"
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz#843f7c09c7245dc0d3cfec2b3c83bb08799a704f"
|
||||
integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==
|
||||
"@img/sharp-libvips-darwin-arm64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
|
||||
integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
|
||||
|
||||
"@img/sharp-libvips-darwin-x64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz#1239c24426c06a8e833815562f78047a3bfbaaf8"
|
||||
integrity sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==
|
||||
"@img/sharp-libvips-darwin-x64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
|
||||
integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz#20d276cefd903ee483f0441ba35961679c286315"
|
||||
integrity sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==
|
||||
"@img/sharp-libvips-linux-arm64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
|
||||
integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
|
||||
|
||||
"@img/sharp-libvips-linux-arm@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz#067c0b566eae8063738cf1b1db8f8a8573b5465c"
|
||||
integrity sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==
|
||||
"@img/sharp-libvips-linux-arm@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
|
||||
integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802"
|
||||
integrity sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==
|
||||
"@img/sharp-libvips-linux-ppc64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
|
||||
integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
|
||||
|
||||
"@img/sharp-libvips-linux-s390x@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz#82fcd68444b3666384235279c145c2b28d8ee302"
|
||||
integrity sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==
|
||||
"@img/sharp-libvips-linux-riscv64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
|
||||
integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
|
||||
|
||||
"@img/sharp-libvips-linux-x64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz#65b2b908bf47156b0724fde9095676c83a18cf5a"
|
||||
integrity sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==
|
||||
"@img/sharp-libvips-linux-s390x@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
|
||||
integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz#72accf924e80b081c8db83b900b444a67c203f01"
|
||||
integrity sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==
|
||||
"@img/sharp-libvips-linux-x64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
|
||||
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz#1fa052737e203f46bf44192acd01f9faf11522d7"
|
||||
integrity sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
|
||||
integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
|
||||
|
||||
"@img/sharp-linux-arm64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz#c9690fac5f3137eaab3f7ad6065390d10f66f1fa"
|
||||
integrity sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
|
||||
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
|
||||
|
||||
"@img/sharp-linux-arm64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc"
|
||||
integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-arm64" "1.2.4"
|
||||
|
||||
"@img/sharp-linux-arm@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz#771dd2ec645f85f98441359bfc118afaf38cbd8b"
|
||||
integrity sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==
|
||||
"@img/sharp-linux-arm@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d"
|
||||
integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-arm" "1.1.0"
|
||||
"@img/sharp-libvips-linux-arm" "1.2.4"
|
||||
|
||||
"@img/sharp-linux-s390x@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz#82132d158abff57bd90b53574f2865f72f94e6c8"
|
||||
integrity sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==
|
||||
"@img/sharp-linux-ppc64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813"
|
||||
integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-s390x" "1.1.0"
|
||||
"@img/sharp-libvips-linux-ppc64" "1.2.4"
|
||||
|
||||
"@img/sharp-linux-x64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz#d815fb87899d462b28b62a9252ad127f02fe0740"
|
||||
integrity sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==
|
||||
"@img/sharp-linux-riscv64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60"
|
||||
integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-x64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-riscv64" "1.2.4"
|
||||
|
||||
"@img/sharp-linuxmusl-arm64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz#cfac45b2abbc04628f676e123bfe3aeb300266c7"
|
||||
integrity sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==
|
||||
"@img/sharp-linux-s390x@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7"
|
||||
integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-s390x" "1.2.4"
|
||||
|
||||
"@img/sharp-linuxmusl-x64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz#b876c23ff51d0fb6d9f3b0a07e2f4d1436c203ad"
|
||||
integrity sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==
|
||||
"@img/sharp-linux-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8"
|
||||
integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-x64" "1.2.4"
|
||||
|
||||
"@img/sharp-wasm32@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz#b1dd0bab547dccf517586eb1fa5852160bba3b82"
|
||||
integrity sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==
|
||||
"@img/sharp-linuxmusl-arm64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086"
|
||||
integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
|
||||
|
||||
"@img/sharp-linuxmusl-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f"
|
||||
integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
|
||||
|
||||
"@img/sharp-wasm32@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0"
|
||||
integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==
|
||||
dependencies:
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.7.0"
|
||||
|
||||
"@img/sharp-win32-arm64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz#f37bee0f60c167f825a09d2b8de6849b823e8b30"
|
||||
integrity sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==
|
||||
"@img/sharp-win32-arm64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
|
||||
integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
|
||||
|
||||
"@img/sharp-win32-ia32@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz#8fc30b6655bc6ff8910344a2020d334aa6361672"
|
||||
integrity sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==
|
||||
"@img/sharp-win32-ia32@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
|
||||
integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
|
||||
|
||||
"@img/sharp-win32-x64@0.34.2":
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz#ecf19250f8fe35de684aa2b0ec6f773b3447247b"
|
||||
integrity sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==
|
||||
"@img/sharp-win32-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
|
||||
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
|
||||
|
||||
"@next/env@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.2.tgz#7143eafa9b11cfdf3d3c7318b0facb9dfdb2948f"
|
||||
integrity sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==
|
||||
"@next/env@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.6.tgz#7009d88d419a36a4ba9e110c151604444744a74d"
|
||||
integrity sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==
|
||||
|
||||
"@next/swc-darwin-arm64@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz#1a7b36bf3c439f899065c878a580bc57a3630ec7"
|
||||
integrity sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==
|
||||
"@next/swc-darwin-arm64@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz#f80d3fe536f29f3217ca07d031f7b43862234059"
|
||||
integrity sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==
|
||||
|
||||
"@next/swc-darwin-x64@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz#3742026344f49128cf1b0f43814c67e880db7361"
|
||||
integrity sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==
|
||||
"@next/swc-darwin-x64@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz#289334478617318a0d8d9f1f6661a15952f4e4ab"
|
||||
integrity sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz#fb29d45c034e3d2eef89b0e2801d62eb86155823"
|
||||
integrity sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==
|
||||
"@next/swc-linux-arm64-gnu@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz#efdd993cd9ad88b82c948c8e518e045566dd2f98"
|
||||
integrity sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz#396784ef312666600ab1ae481e34cb1f6e3ae730"
|
||||
integrity sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==
|
||||
"@next/swc-linux-arm64-musl@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz#a45185126faf69eb65a017bd2c015ad7e86f5c84"
|
||||
integrity sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz#ac01fda376878e02bc6b57d1e88ab8ceae9f868e"
|
||||
integrity sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==
|
||||
"@next/swc-linux-x64-gnu@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz#8174dc8e03a1f7df292bead360f83c53f8dd8b73"
|
||||
integrity sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz#327a5023003bcb3ca436efc08733f091bba2b1e8"
|
||||
integrity sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==
|
||||
"@next/swc-linux-x64-musl@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz#6d0776d81c5bd6a1780e6c39f32d7ef172900635"
|
||||
integrity sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz#ce3a6588bd9c020960704011ab20bd0440026965"
|
||||
integrity sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==
|
||||
"@next/swc-win32-arm64-msvc@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz#4b63d511b5c41278a48168fccb89cf00912411af"
|
||||
integrity sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.3.2":
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz#43cc36097ac27639e9024a5ceaa6e7727fa968c8"
|
||||
integrity sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==
|
||||
|
||||
"@swc/counter@0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
|
||||
"@next/swc-win32-x64-msvc@15.5.6":
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz#9ed5e84bd85009625dd35c444668d13061452a50"
|
||||
integrity sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==
|
||||
|
||||
"@swc/helpers@0.5.15":
|
||||
version "0.5.15"
|
||||
@@ -310,13 +329,6 @@ buffer-equal-constant-time@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||
dependencies:
|
||||
streamsearch "^1.1.0"
|
||||
|
||||
bytes@3.1.2, bytes@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
@@ -348,34 +360,6 @@ client-only@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@^1.0.0, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-string@^1.9.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
|
||||
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
content-disposition@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2"
|
||||
@@ -447,10 +431,10 @@ destroy@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
|
||||
detect-libc@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
|
||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||
detect-libc@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
|
||||
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -665,11 +649,6 @@ ipaddr.js@1.9.1:
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
is-arrayish@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-promise@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
|
||||
@@ -812,28 +791,26 @@ negotiator@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
|
||||
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
|
||||
|
||||
next@^15.2.3:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.3.2.tgz#97510629e38a058dd154782a5c2ec9c9ab94d0d8"
|
||||
integrity sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==
|
||||
next@^15.4.7:
|
||||
version "15.5.6"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.5.6.tgz#16d9d1e9ba2e8caf82ba15e060a12286cd25db30"
|
||||
integrity sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==
|
||||
dependencies:
|
||||
"@next/env" "15.3.2"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@next/env" "15.5.6"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.3.2"
|
||||
"@next/swc-darwin-x64" "15.3.2"
|
||||
"@next/swc-linux-arm64-gnu" "15.3.2"
|
||||
"@next/swc-linux-arm64-musl" "15.3.2"
|
||||
"@next/swc-linux-x64-gnu" "15.3.2"
|
||||
"@next/swc-linux-x64-musl" "15.3.2"
|
||||
"@next/swc-win32-arm64-msvc" "15.3.2"
|
||||
"@next/swc-win32-x64-msvc" "15.3.2"
|
||||
sharp "^0.34.1"
|
||||
"@next/swc-darwin-arm64" "15.5.6"
|
||||
"@next/swc-darwin-x64" "15.5.6"
|
||||
"@next/swc-linux-arm64-gnu" "15.5.6"
|
||||
"@next/swc-linux-arm64-musl" "15.5.6"
|
||||
"@next/swc-linux-x64-gnu" "15.5.6"
|
||||
"@next/swc-linux-x64-musl" "15.5.6"
|
||||
"@next/swc-win32-arm64-msvc" "15.5.6"
|
||||
"@next/swc-win32-x64-msvc" "15.5.6"
|
||||
sharp "^0.34.3"
|
||||
|
||||
object-assign@^4:
|
||||
version "4.1.1"
|
||||
@@ -954,10 +931,10 @@ semver@^7.5.4:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
|
||||
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
|
||||
|
||||
semver@^7.7.2:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
semver@^7.7.3:
|
||||
version "7.7.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
|
||||
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
|
||||
|
||||
send@^1.0.0, send@^1.1.0:
|
||||
version "1.1.0"
|
||||
@@ -992,36 +969,39 @@ setprototypeof@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
sharp@^0.34.1:
|
||||
version "0.34.2"
|
||||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.2.tgz#648bd639854dbe48047b0b420213c186d036b32d"
|
||||
integrity sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==
|
||||
sharp@^0.34.3:
|
||||
version "0.34.5"
|
||||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0"
|
||||
integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==
|
||||
dependencies:
|
||||
color "^4.2.3"
|
||||
detect-libc "^2.0.4"
|
||||
semver "^7.7.2"
|
||||
"@img/colour" "^1.0.0"
|
||||
detect-libc "^2.1.2"
|
||||
semver "^7.7.3"
|
||||
optionalDependencies:
|
||||
"@img/sharp-darwin-arm64" "0.34.2"
|
||||
"@img/sharp-darwin-x64" "0.34.2"
|
||||
"@img/sharp-libvips-darwin-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-darwin-x64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-arm" "1.1.0"
|
||||
"@img/sharp-libvips-linux-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-ppc64" "1.1.0"
|
||||
"@img/sharp-libvips-linux-s390x" "1.1.0"
|
||||
"@img/sharp-libvips-linux-x64" "1.1.0"
|
||||
"@img/sharp-libvips-linuxmusl-arm64" "1.1.0"
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.1.0"
|
||||
"@img/sharp-linux-arm" "0.34.2"
|
||||
"@img/sharp-linux-arm64" "0.34.2"
|
||||
"@img/sharp-linux-s390x" "0.34.2"
|
||||
"@img/sharp-linux-x64" "0.34.2"
|
||||
"@img/sharp-linuxmusl-arm64" "0.34.2"
|
||||
"@img/sharp-linuxmusl-x64" "0.34.2"
|
||||
"@img/sharp-wasm32" "0.34.2"
|
||||
"@img/sharp-win32-arm64" "0.34.2"
|
||||
"@img/sharp-win32-ia32" "0.34.2"
|
||||
"@img/sharp-win32-x64" "0.34.2"
|
||||
"@img/sharp-darwin-arm64" "0.34.5"
|
||||
"@img/sharp-darwin-x64" "0.34.5"
|
||||
"@img/sharp-libvips-darwin-arm64" "1.2.4"
|
||||
"@img/sharp-libvips-darwin-x64" "1.2.4"
|
||||
"@img/sharp-libvips-linux-arm" "1.2.4"
|
||||
"@img/sharp-libvips-linux-arm64" "1.2.4"
|
||||
"@img/sharp-libvips-linux-ppc64" "1.2.4"
|
||||
"@img/sharp-libvips-linux-riscv64" "1.2.4"
|
||||
"@img/sharp-libvips-linux-s390x" "1.2.4"
|
||||
"@img/sharp-libvips-linux-x64" "1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
|
||||
"@img/sharp-linux-arm" "0.34.5"
|
||||
"@img/sharp-linux-arm64" "0.34.5"
|
||||
"@img/sharp-linux-ppc64" "0.34.5"
|
||||
"@img/sharp-linux-riscv64" "0.34.5"
|
||||
"@img/sharp-linux-s390x" "0.34.5"
|
||||
"@img/sharp-linux-x64" "0.34.5"
|
||||
"@img/sharp-linuxmusl-arm64" "0.34.5"
|
||||
"@img/sharp-linuxmusl-x64" "0.34.5"
|
||||
"@img/sharp-wasm32" "0.34.5"
|
||||
"@img/sharp-win32-arm64" "0.34.5"
|
||||
"@img/sharp-win32-ia32" "0.34.5"
|
||||
"@img/sharp-win32-x64" "0.34.5"
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1063,13 +1043,6 @@ side-channel@^1.0.6, side-channel@^1.1.0:
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
|
||||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
@@ -1080,11 +1053,6 @@ statuses@2.0.1, statuses@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499"
|
||||
|
||||
20
bin/mage.bb
20
bin/mage.bb
@@ -62,6 +62,10 @@
|
||||
(.getCause e)
|
||||
(assoc :cause (summarize-exception (.getCause e)))))
|
||||
|
||||
(def ^:private ex-message-label (c/red (c/reverse-color "ex-message : ")))
|
||||
(def ^:private ex-data-label (c/yellow (c/reverse-color "ex-data : ")))
|
||||
(def ^:private mage-error-label (c/blue (c/reverse-color "mage/error : ")))
|
||||
|
||||
(defn -main [& _]
|
||||
(cond
|
||||
;; help
|
||||
@@ -73,7 +77,9 @@
|
||||
|
||||
;; errors
|
||||
(invalid-task?)
|
||||
(do (print-help) (System/exit 1))
|
||||
(do (print-help)
|
||||
#_{:clj-kondo/ignore [:discouraged-java-method]}
|
||||
(System/exit 1))
|
||||
|
||||
:else
|
||||
;; at this point, we always have a valid task, and we are running in bb, so
|
||||
@@ -101,13 +107,13 @@
|
||||
(cond-> e
|
||||
(not (u/env "MAGE_DEBUG" (constantly nil)))
|
||||
summarize-exception)))
|
||||
(when (and message (not (str/blank? message)))
|
||||
(println (c/red (c/reverse-color "ex-message : ")) message))
|
||||
(when data
|
||||
(println (c/yellow (c/reverse-color "ex-data : ")) (pr-str
|
||||
(dissoc data :mage/error))))
|
||||
(when (not (str/blank? message))
|
||||
(println ex-message-label message))
|
||||
(when (not-empty (dissoc data :mage/error))
|
||||
(println ex-data-label (u/pp (dissoc data :mage/error))))
|
||||
(when (:mage/error data)
|
||||
(println (c/blue (c/reverse-color "mage/error : ")) (:mage/error data)))
|
||||
(println mage-error-label (:mage/error data)))
|
||||
#_{:clj-kondo/ignore [:discouraged-java-method]}
|
||||
(System/exit (:babashka/exit data 1))))))))))
|
||||
|
||||
(when (= *file* (System/getProperty "babashka.file"))
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"ignored": {
|
||||
"vars": [
|
||||
"metabase.session.models.session-test/send-email-on-first-login-from-new-device-test",
|
||||
"metabase-enterprise.impersonation.driver-test/nested-do-with-connection-with-options-test",
|
||||
"metabase.warehouse-schema.api.table-test/update-table-sync-test",
|
||||
"metabase.warehouses.api-test/can-rescan-fieldvalues-for-a-db",
|
||||
"metabase-enterprise.transforms-python.transforms-api-test/python-transform-schema-change-integration-test"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"last_updated": "2025-08-02",
|
||||
"updated_by": "dpsutton",
|
||||
"reason": "Two flakes: send-email-on-first-login-from-new-device-test has schema error if emails from another thread are present; nested-do-with-connection-with-options-test is causing deadlocks i believe."
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,4 @@
|
||||
"MB_PRO_CLOUD_TOKEN": "get it from the password manager",
|
||||
"MB_PRO_SELF_HOSTED_TOKEN": "get it from the password manager",
|
||||
"MB_EDITION": "ee",
|
||||
"QA_DB_ENABLED": "true"
|
||||
}
|
||||
|
||||
18
deps.edn
18
deps.edn
@@ -19,7 +19,7 @@
|
||||
org.bouncycastle/bcprov-jdk18on]}
|
||||
buddy/buddy-sign {:mvn/version "3.6.1-359"} ; JSON Web Tokens; High-Level message signing library
|
||||
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} ; util functions for converting between camel, snake, and kebob case
|
||||
cheshire/cheshire {:mvn/version "6.0.0"} ; fast JSON encoding (used by Ring JSON middleware)
|
||||
cheshire/cheshire {:mvn/version "6.1.0"} ; fast JSON encoding (used by Ring JSON middleware)
|
||||
clj-bom/clj-bom {:mvn/version "0.1.2"} ; handle BOMs in imported CSVs
|
||||
clj-commons/clj-yaml {:mvn/version "1.0.29"} ; Clojure wrapper for YAML library SnakeYAML
|
||||
clj-commons/iapetos {:mvn/version "0.1.14"} ; prometheus metrics
|
||||
@@ -53,7 +53,7 @@
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"} ; Talk to JDBC DBs
|
||||
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.8"} ; Telemetry library
|
||||
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.5"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
|
||||
com.google.guava/guava {:mvn/version "33.5.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
|
||||
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
|
||||
{:mvn/version "2.1.214"} ; embedded SQL database
|
||||
com.ibm.icu/icu4j {:mvn/version "78.1"} ; Used to transliterate Unicode characters into Latin characters
|
||||
@@ -89,6 +89,7 @@
|
||||
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
|
||||
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
io.github.metabase/representations {:mvn/version "0.1.13"}
|
||||
;; TODO: replace back to normal artifact
|
||||
io.github.metabase/macaw {:mvn/version "0.2.29"} ; Parse native SQL queries
|
||||
io.github.resilience4j/resilience4j-retry ^:antq/exclude ; 2.x do not support Java 8
|
||||
@@ -120,7 +121,7 @@
|
||||
junit/junit]}
|
||||
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
|
||||
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
|
||||
org.apache.commons/commons-lang3 {:mvn/version "3.18.0"} ; helper methods for working with java.lang stuff
|
||||
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
|
||||
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.2"} ; apache logging framework
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.2"} ; add compatibility with log4j 1.2
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.2"} ; apache logging framework
|
||||
@@ -160,7 +161,7 @@
|
||||
org.clojure/java.jmx {:mvn/version "1.1.0"} ; JMX bean library, for exporting diagnostic info
|
||||
org.clojure/math.combinatorics {:mvn/version "0.3.0"} ; combinatorics functions
|
||||
org.clojure/math.numeric-tower {:mvn/version "0.1.0"} ; math functions like `ceil`
|
||||
org.clojure/tools.cli {:mvn/version "1.1.230"} ; command-line argument parsing
|
||||
org.clojure/tools.cli {:mvn/version "1.2.245"} ; command-line argument parsing
|
||||
org.clojure/tools.logging {:mvn/version "1.3.0"} ; logging framework
|
||||
org.clojure/tools.macro {:mvn/version "0.2.1"} ; local macros
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
@@ -174,7 +175,7 @@
|
||||
org.flatland/ordered {:mvn/version "1.15.12"} ; ordered maps & sets
|
||||
org.graalvm.polyglot/js-community {:mvn/version "25.0.1" :extension "pom"} ; JavaScript engine
|
||||
org.graalvm.polyglot/polyglot {:mvn/version "25.0.1"} ; GraalVM platform, required by JS engine
|
||||
org.jsoup/jsoup {:mvn/version "1.21.1"} ; required by hickory
|
||||
org.jsoup/jsoup {:mvn/version "1.21.2"} ; required by hickory
|
||||
org.liquibase/liquibase-core {:mvn/version "4.33.0" ; migration management (Java lib)
|
||||
:exclusions [ch.qos.logback/logback-classic]}
|
||||
org.mariadb.jdbc/mariadb-java-client ^:antq/exclude ; 3.x only support jdbc:mariadb, so using 2.x for now
|
||||
@@ -860,6 +861,13 @@
|
||||
{:exec-args {:only ["test/metabase/notification"
|
||||
"test/metabase/channel"]}}
|
||||
|
||||
;; test the transforms feature
|
||||
;; clj -X:dev:ee:ee-dev:test:test/transforms
|
||||
:test/transforms
|
||||
{:exec-args {:only ["enterprise/backend/test/metabase_enterprise/transforms"
|
||||
"enterprise/backend/test/metabase_enterprise/transforms_python"]}}
|
||||
|
||||
|
||||
;; test all MBQL related stuff: MLv2, the legacy shared `metabase.legacy-mbql` code, and the QP
|
||||
;;
|
||||
;; clj -X:dev:ee:ee-dev:test:test/mbql
|
||||
|
||||
@@ -204,6 +204,21 @@
|
||||
;; has 2 defenterprise-schemas, both to the same ns:
|
||||
(find-defenterprise-schemas "src/metabase/sso/ldap/default_implementation.clj"))
|
||||
|
||||
(def ^:private ^String project-root (System/getProperty "user.dir"))
|
||||
|
||||
(defn- file->path-relative-to-project-root
|
||||
"Get the path of a file relative to the project (repo) root directory e.g.
|
||||
|
||||
(file->path-relative-to-project-root \"/home/cam/metabase/deps.edn\")
|
||||
;; =>
|
||||
\"deps.edn\""
|
||||
[file]
|
||||
(let [file (io/file file)
|
||||
path (.getAbsolutePath file)]
|
||||
(if (str/starts-with? path project-root)
|
||||
(subs path (inc (count project-root))) ; project root won't include trailing `/`
|
||||
path)))
|
||||
|
||||
(def ^:private ignored-dependencies
|
||||
"Technically `config` 'uses' `enterprise/core` and `test` since it tries to load them to see if they exist so we know
|
||||
if EE/test code is available; however we can ignore them since they're not 'real' usages. So add them here so we
|
||||
@@ -212,13 +227,16 @@
|
||||
|
||||
(mu/defn- file-dependencies :- [:map
|
||||
[:namespace simple-symbol?]
|
||||
[:filename string?] ; filename is relative to [[project-root]]
|
||||
[:module symbol?]
|
||||
[:deps [:sequential
|
||||
[:map
|
||||
[:namespace simple-symbol?]
|
||||
[:module symbol?]
|
||||
[:dynamic {:optional true} :keyword]]]]]
|
||||
[file]
|
||||
[file :- [:or
|
||||
string?
|
||||
[:fn {:error/message "Instance of a java.io.File"} #(instance? java.io.File %)]]]
|
||||
(try
|
||||
(let [decl (ns.file/read-file-ns-decl file)
|
||||
ns-symb (ns.parse/name-from-ns-decl decl)
|
||||
@@ -239,6 +257,7 @@
|
||||
#_defenterprise-deps
|
||||
#_defenterprise-schema-deps])]
|
||||
{:namespace ns-symb
|
||||
:filename (file->path-relative-to-project-root file)
|
||||
:module (module ns-symb)
|
||||
:deps (sort-by pr-str
|
||||
(keep (fn [required-ns]
|
||||
@@ -262,7 +281,10 @@
|
||||
|
||||
(file-dependencies "src/metabase/query_processor/middleware/permissions.clj"))
|
||||
|
||||
(defn dependencies []
|
||||
(defn dependencies
|
||||
"Calculate information about all the modules dependencies for all *SOURCE* files in the Metabase project by parsing
|
||||
the files."
|
||||
[]
|
||||
(pmap file-dependencies (find-source-files)))
|
||||
|
||||
(defn external-usages
|
||||
@@ -518,14 +540,15 @@
|
||||
|
||||
(defn leaf-modules
|
||||
"Modules that are leaf nodes in the module dependency tree -- nothing else depends on them."
|
||||
[]
|
||||
(let [deps (dependencies)]
|
||||
(into (sorted-set)
|
||||
(comp (map :module)
|
||||
(keep (fn [module]
|
||||
(when (zero? (count (external-usages deps module)))
|
||||
module))))
|
||||
deps)))
|
||||
([]
|
||||
(leaf-modules (dependencies)))
|
||||
([deps]
|
||||
(into (sorted-set)
|
||||
(comp (map :module)
|
||||
(keep (fn [module]
|
||||
(when (zero? (count (external-usages deps module)))
|
||||
module))))
|
||||
deps)))
|
||||
|
||||
(defn non-dependencies
|
||||
"Modules that `module` does not depend on, either directly or indirectly -- changes to any of these modules should not
|
||||
@@ -534,6 +557,7 @@
|
||||
(let [deps (dependencies)
|
||||
all-modules (into (sorted-set) (map :module) deps)
|
||||
module-deps (set (keys (all-module-deps-paths deps module)))]
|
||||
#_{:clj-kondo/ignore [:discouraged-var]}
|
||||
(printf "Module %s depends on %d/%d (%.1f%%) other modules.\n"
|
||||
module
|
||||
(count module-deps)
|
||||
@@ -575,3 +599,129 @@
|
||||
new-deps (simulate-rename deps old-namespace->new-namespace)
|
||||
new-module-deps (into (sorted-set) (keys (all-module-deps-paths new-deps module)))]
|
||||
(set/difference old-module-deps new-module-deps)))
|
||||
|
||||
(mu/defn- module->source-files :- [:set :string]
|
||||
"Return the set of all *source* filenames (relative to the [[project-root]] directory) for a `module`."
|
||||
[deps module]
|
||||
(into
|
||||
(sorted-set)
|
||||
(comp (filter #(= (:module %) module))
|
||||
(map :filename))
|
||||
deps))
|
||||
|
||||
(defn- file->namespace
|
||||
"Infer the Clojure namespace from a core project filename.
|
||||
|
||||
(file->namespace \"/home/cam/metabase/enterprise/backend/src/metabase_enterprise/advanced_permissions/common.clj\")
|
||||
;; =>
|
||||
metabase-enterprise.advanced-permissions.common"
|
||||
[file]
|
||||
(-> file
|
||||
file->path-relative-to-project-root
|
||||
(str/replace #"^enterprise/backend/" "")
|
||||
(str/replace #"^(?:(?:src)|(?:test))/" "")
|
||||
(str/replace #"\.clj[cs]?$" "")
|
||||
(str/replace #"/" ".")
|
||||
(str/replace #"_" "-")
|
||||
symbol))
|
||||
|
||||
(defn- module->all-deps [deps module]
|
||||
(keys (all-module-deps-paths deps module)))
|
||||
|
||||
(defn test-filenames->relevant-source-filenames
|
||||
"Given a collection of `test-filenames`, return the set of source filenames (relative to the project root directory)
|
||||
that when changed should trigger these tests."
|
||||
([test-filenames]
|
||||
(test-filenames->relevant-source-filenames (dependencies) test-filenames))
|
||||
([deps test-filenames]
|
||||
(into
|
||||
(sorted-set)
|
||||
(comp (map file->namespace)
|
||||
(map module)
|
||||
(distinct)
|
||||
(mapcat (fn [module]
|
||||
(into #{module} (module->all-deps deps module))))
|
||||
(distinct)
|
||||
(mapcat #(module->source-files deps %)))
|
||||
test-filenames)))
|
||||
|
||||
(comment
|
||||
;; should include source files for `settings-rest` itself as well as all modules used either directly or indirectly
|
||||
;; by `settings-rest` (currently this set is huge, 955 files as of 2025-11-26)
|
||||
(test-filenames->relevant-source-filenames ["test/metabase/settings_rest/api_test.clj"]))
|
||||
|
||||
(defn- direct-dependents
|
||||
"Set of modules that directly depend on `module`."
|
||||
[deps module]
|
||||
(into (sorted-set)
|
||||
(keep (fn [ns-info]
|
||||
(when (some (fn [ns-deps]
|
||||
(= (:module ns-deps) module))
|
||||
(:deps ns-info))
|
||||
(:module ns-info))))
|
||||
deps))
|
||||
|
||||
(comment
|
||||
(direct-dependents (dependencies) 'driver)
|
||||
(direct-dependents (dependencies) 'settings-rest))
|
||||
|
||||
(defn- indirect-dependents
|
||||
"Set of modules that either directly or indirectly depend on `module`."
|
||||
([deps module]
|
||||
(indirect-dependents deps module (sorted-set)))
|
||||
([deps module acc]
|
||||
(let [module-deps (direct-dependents deps module)
|
||||
new-deps (set/difference module-deps acc)
|
||||
acc (into acc new-deps)]
|
||||
(reduce
|
||||
(fn [acc new-dep]
|
||||
(indirect-dependents deps new-dep acc))
|
||||
acc
|
||||
new-deps))))
|
||||
|
||||
(comment
|
||||
(indirect-dependents (dependencies) 'settings-rest))
|
||||
|
||||
(defn- module->dependents [deps module]
|
||||
(into #{module}
|
||||
(indirect-dependents deps module)))
|
||||
|
||||
(comment
|
||||
(module->dependents (dependencies) 'settings-rest))
|
||||
|
||||
(defn- module->test-directory [module]
|
||||
(let [parent-dir (case (namespace module)
|
||||
nil "test/metabase/"
|
||||
"enterprise" "enterprise/backend/test/metabase_enterprise/")
|
||||
module-dir (str/replace (name module) #"-" "_")]
|
||||
(str parent-dir module-dir)))
|
||||
|
||||
(mu/defn- module->test-files :- [:set :string]
|
||||
"Return the set of test filenames associated with a `module`."
|
||||
[module :- :symbol]
|
||||
(let [test-dir (module->test-directory module)
|
||||
test-filenames (ns.find/find-sources-in-dir (io/file test-dir))]
|
||||
(into
|
||||
(sorted-set)
|
||||
(map file->path-relative-to-project-root)
|
||||
test-filenames)))
|
||||
|
||||
(defn source-filenames->relevant-test-filenames
|
||||
"Given a collection of `source-filenames`, return the set of test filenames (relative to the project root directory)
|
||||
that we should re-run when any of `source-filenames` change."
|
||||
([source-filenames]
|
||||
(source-filenames->relevant-test-filenames (dependencies) source-filenames))
|
||||
([deps source-filenames]
|
||||
(into
|
||||
(sorted-set)
|
||||
(comp (map file->namespace)
|
||||
(map module)
|
||||
(distinct)
|
||||
(mapcat #(module->dependents deps %))
|
||||
(mapcat module->test-files))
|
||||
source-filenames)))
|
||||
|
||||
(comment
|
||||
;; should only include tests for `settings-rest`, `api-routes`, `core`, and the handful of random modules that use
|
||||
;; `settings-rest` (21 files as of 2025-11-26)
|
||||
(source-filenames->relevant-test-filenames ["src/metabase/settings_rest/api.clj"]))
|
||||
|
||||
730
docs/api.json
730
docs/api.json
@@ -245,6 +245,18 @@
|
||||
} ]
|
||||
},
|
||||
"mbql.clause.coalesce" : { },
|
||||
"mbql.clause.collate" : {
|
||||
"type" : "array",
|
||||
"prefixItems" : [ {
|
||||
"const" : "collate"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/metabase.lib.schema.common.options"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/metabase.lib.schema.expression.string"
|
||||
}, {
|
||||
"type" : "string"
|
||||
} ]
|
||||
},
|
||||
"mbql.clause.concat" : { },
|
||||
"mbql.clause.contains" : { },
|
||||
"mbql.clause.convert-timezone" : { },
|
||||
@@ -1125,6 +1137,45 @@
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.action-v2.api.api-action-expression"
|
||||
} ]
|
||||
},
|
||||
"metabase-enterprise.data-studio.api.table.data-authorities" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "computed", "authoritative", "unconfigured", "ingested" ]
|
||||
},
|
||||
"metabase-enterprise.data-studio.api.table.data-layers" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "copper", "bronze", "gold", "silver" ]
|
||||
},
|
||||
"metabase-enterprise.data-studio.api.table.data-sources" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "source-data", "transform", "unknown", "ingested", "metabase-transform", "upload" ]
|
||||
},
|
||||
"metabase-enterprise.data-studio.api.table.table-selectors" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"database_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
},
|
||||
"schema_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"table_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metabase-enterprise.dependencies.api.card-body" : {
|
||||
"description" : "Schema for an instance of a `:model/Card` (everything is optional to support updates).",
|
||||
"type" : "object",
|
||||
@@ -3094,6 +3145,9 @@
|
||||
"metabase-enterprise.metabot-v3.tools.api.table-result" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"database_engine" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"id" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
@@ -3132,7 +3186,7 @@
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "id", "type", "name", "display_name", "database_id", "fields" ]
|
||||
"required" : [ "id", "type", "name", "display_name", "database_id", "database_engine", "fields" ]
|
||||
},
|
||||
"metabase-enterprise.metabot-v3.tools.api.temporal-extraction-filter" : {
|
||||
"allOf" : [ {
|
||||
@@ -3259,6 +3313,114 @@
|
||||
"description" : "value must be a valid attribute remappings map (attribute name -> remapped name)",
|
||||
"optional" : true
|
||||
},
|
||||
"metabase-enterprise.support-access-grants.schema.create-grant-request" : {
|
||||
"description" : "Schema for POST /api/ee/support-access-grants request body.",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"grant_duration_minutes" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 1,
|
||||
"maximum" : 10080
|
||||
},
|
||||
"notes" : {
|
||||
"type" : "string",
|
||||
"minLength" : 1,
|
||||
"maxLength" : 255
|
||||
},
|
||||
"ticket_number" : {
|
||||
"type" : "string",
|
||||
"minLength" : 1,
|
||||
"maxLength" : 100
|
||||
}
|
||||
},
|
||||
"required" : [ "grant_duration_minutes" ]
|
||||
},
|
||||
"metabase-enterprise.support-access-grants.schema.current-grant-response" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response",
|
||||
"description" : "Schema for GET /api/ee/support-access-grants/current response.",
|
||||
"optional" : true
|
||||
},
|
||||
"metabase-enterprise.support-access-grants.schema.grant-response" : {
|
||||
"description" : "Schema for a support access grant object in API responses.",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"user_id" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"token" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"user_name" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"user_email" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"grant_end_timestamp" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
|
||||
},
|
||||
"revoked_at" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
|
||||
},
|
||||
"revoked_by_user_id" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"id" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"ticket_number" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"updated_at" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
|
||||
},
|
||||
"created_at" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
|
||||
},
|
||||
"notes" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"grant_start_timestamp" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.timestamp"
|
||||
}
|
||||
},
|
||||
"required" : [ "id", "user_id", "grant_start_timestamp", "grant_end_timestamp", "created_at", "updated_at" ]
|
||||
},
|
||||
"metabase-enterprise.support-access-grants.schema.list-grants-response" : {
|
||||
"description" : "Schema for GET /api/ee/support-access-grants response (paginated list).",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"data" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
|
||||
}
|
||||
},
|
||||
"limit" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"offset" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 0
|
||||
},
|
||||
"total" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 0
|
||||
}
|
||||
},
|
||||
"required" : [ "data", "total", "limit", "offset" ]
|
||||
},
|
||||
"metabase-enterprise.support-access-grants.schema.timestamp" : {
|
||||
"anyOf" : [ { }, { } ]
|
||||
},
|
||||
"metabase-enterprise.transforms.api.run-trigger" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "none", "global-schedule" ]
|
||||
@@ -5183,6 +5345,8 @@
|
||||
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.text"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.split-part"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/metabase.legacy-mbql.schema.collate"
|
||||
} ]
|
||||
},
|
||||
"metabase.legacy-mbql.schema.StringExpressionArg" : {
|
||||
@@ -5397,6 +5561,10 @@
|
||||
"description" : "schema for a valid MBQL 4 :coalesce clause",
|
||||
"allOf" : [ ]
|
||||
},
|
||||
"metabase.legacy-mbql.schema.collate" : {
|
||||
"description" : "schema for a valid MBQL 4 :collate clause",
|
||||
"allOf" : [ ]
|
||||
},
|
||||
"metabase.legacy-mbql.schema.concat" : {
|
||||
"description" : "schema for a valid MBQL 4 :concat clause",
|
||||
"allOf" : [ ]
|
||||
@@ -6830,6 +6998,8 @@
|
||||
"$ref" : "#/components/schemas/mbql.clause.ceil"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/mbql.clause.coalesce"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/mbql.clause.collate"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/mbql.clause.concat"
|
||||
}, {
|
||||
@@ -9272,6 +9442,14 @@
|
||||
"type" : "string",
|
||||
"enum" : [ "computed", "authoritative", "unconfigured", "ingested" ]
|
||||
},
|
||||
"metabase.warehouse-schema-rest.api.table.data-layers" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "copper", "bronze", "gold", "silver" ]
|
||||
},
|
||||
"metabase.warehouse-schema-rest.api.table.data-sources" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "source-data", "transform", "unknown", "ingested", "metabase-transform", "upload" ]
|
||||
},
|
||||
"metabase.warehouses-rest.api.available-settings" : {
|
||||
"type" : "object",
|
||||
"additionalProperties" : {
|
||||
@@ -9704,6 +9882,7 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"deprecated" : true,
|
||||
"tags" : [ "/api/activity" ]
|
||||
}
|
||||
},
|
||||
@@ -12394,10 +12573,6 @@
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"type" : {
|
||||
"type" : "string",
|
||||
"enum" : [ "remote-synced" ]
|
||||
}
|
||||
},
|
||||
"required" : [ "name" ]
|
||||
@@ -12548,7 +12723,7 @@
|
||||
"/api/collection/root/items" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/collection/root/items",
|
||||
"description" : "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection\n doesn't actually exist as a row in the application DB: it's simply a virtual Collection where things with no\n `collection_id` exist. It does, however, have its own set of Permissions.\n\n This endpoint will actually show objects with no `collection_id` for Users that have Root Collection\n permissions, but for people without Root Collection perms, we'll just show the objects that have an effective\n location of `/`.\n\n This endpoint is intended to power a 'Root Folder View' for the Current User, so regardless you'll see all the\n top-level objects you're allowed to access.\n\n By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as\n `snippets`, you can pass the `?namespace=` parameter.\n\n Note that this endpoint should return results in a similar shape to `/api/dashboard/:id/items`, so if this is\n changed, that should too.",
|
||||
"description" : "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection\n doesn't actually exist as a row in the application DB: it's simply a virtual Collection where things with no\n `collection_id` exist. It does, however, have its own set of Permissions.\n\n This endpoint will actually show objects with no `collection_id` for Users that have Root Collection\n permissions, but for people without Root Collection perms, we'll just show the objects that have an effective\n location of `/`.\n\n This endpoint is intended to power a 'Root Folder View' for the Current User, so regardless you'll see all the\n top-level objects you're allowed to access.\n\n By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as\n `snippets`, you can pass the `?namespace=` parameter.\n\n By default, library collections are excluded from the results; to include them, pass `?include_library=true`.\n\n Note that this endpoint should return results in a similar shape to `/api/dashboard/:id/items`, so if this is\n changed, that should too.",
|
||||
"parameters" : [ {
|
||||
"in" : "query",
|
||||
"name" : "models",
|
||||
@@ -12592,6 +12767,14 @@
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "include_library",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "pinned_state",
|
||||
@@ -12731,6 +12914,14 @@
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "include-library",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "namespace",
|
||||
@@ -13109,6 +13300,7 @@
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"deprecated" : true,
|
||||
"tags" : [ "/api/dashboard" ]
|
||||
},
|
||||
"post" : {
|
||||
@@ -16598,6 +16790,222 @@
|
||||
"tags" : [ "/api/ee/content-translation" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/data-studio/table/discard-values" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/data-studio/table/discard-values",
|
||||
"description" : "Batch version of /table/:id/discard_values. Takes an abstract table selection as /table/edit does.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-studio/table" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/data-studio/table/edit" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/data-studio/table/edit",
|
||||
"description" : "Bulk updating tables.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"data_authority" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-authorities"
|
||||
},
|
||||
"data_layer" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-layers"
|
||||
},
|
||||
"table_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
},
|
||||
"owner_email" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"owner_user_id" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
"data_source" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.data-sources"
|
||||
},
|
||||
"schema_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"entity_type" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"database_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties" : false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-studio/table" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/data-studio/table/publish-model" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/data-studio/table/publish-model",
|
||||
"description" : "Create a model for each of selected tables",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"database_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
},
|
||||
"schema_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"table_ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "value must be an integer greater than zero.",
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}
|
||||
},
|
||||
"target_collection_id" : {
|
||||
"anyOf" : [ {
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
}, {
|
||||
"const" : "library"
|
||||
} ]
|
||||
}
|
||||
},
|
||||
"required" : [ ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-studio/table" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/data-studio/table/rescan-values" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/data-studio/table/rescan-values",
|
||||
"description" : "Batch version of /table/:id/rescan_values. Takes an abstract table selection as /table/edit does.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-studio/table" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/data-studio/table/sync-schema" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/data-studio/table/sync-schema",
|
||||
"description" : "Batch version of /table/:id/sync_schema. Takes an abstract table selection as /table/edit does.\n - Currently checks policy before returning (so you might receive a 4xx on e.g. AuthZ policy failure)\n - The sync itself is however, asyncronous. This call may return before all tables synced.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.data-studio.api.table.table-selectors"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/data-studio/table" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/database-replication/connection/{database-id}" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/database-replication/connection/{database-id}",
|
||||
@@ -17760,6 +18168,61 @@
|
||||
"tags" : [ "/api/ee/gsheets" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/library/" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/library/",
|
||||
"description" : "Creates the Library if it doesn't exist. Returns the created collection.\n\n Requires superuser permissions.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/library" ]
|
||||
},
|
||||
"get" : {
|
||||
"summary" : "GET /api/ee/library/",
|
||||
"description" : "Get the Library. If no library exists, it doesn't fail but returns an empty response.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/library" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/library/tree" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/ee/library/tree",
|
||||
"description" : "This matches /api/collection/tree but only returns the library collection.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/library" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/logs/query_execution/{yyyy-mm}" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/ee/logs/query_execution/{yyyy-mm}",
|
||||
@@ -21087,6 +21550,149 @@
|
||||
"tags" : [ "/api/ee/stale" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/support-access-grant/" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/support-access-grant/",
|
||||
"description" : "Create a new support access grant.\n\n Requires superuser permissions. Only one active grant can exist at a time.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Schema for a support access grant object in API responses.",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"requestBody" : {
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.create-grant-request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/support-access-grant" ]
|
||||
},
|
||||
"get" : {
|
||||
"summary" : "GET /api/ee/support-access-grant/",
|
||||
"description" : "List support access grants with optional filtering and pagination.\n\n Requires superuser permissions.\n\n Query parameters:\n - ticket-number: Filter by ticket number\n - user-id: Filter by user who created the grant\n - include-revoked: Include revoked grants (default false)",
|
||||
"parameters" : [ {
|
||||
"in" : "query",
|
||||
"name" : "ticket-number",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "user-id",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"description" : "value must be an integer greater than zero."
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "include-revoked",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "boolean"
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Schema for GET /api/ee/support-access-grants response (paginated list).",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.list-grants-response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/support-access-grant" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/support-access-grant/current" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/ee/support-access-grant/current",
|
||||
"description" : "Get the currently active support access grant, if one exists.\n\n Requires superuser permissions.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Schema for GET /api/ee/support-access-grants/current response.",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.current-grant-response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/support-access-grant" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/support-access-grant/{id}/revoke" : {
|
||||
"put" : {
|
||||
"summary" : "PUT /api/ee/support-access-grant/{id}/revoke",
|
||||
"description" : "Revoke an existing support access grant.\n\n Requires superuser permissions. Any admin can revoke any grant.",
|
||||
"parameters" : [ {
|
||||
"in" : "path",
|
||||
"name" : "id",
|
||||
"required" : true,
|
||||
"schema" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"description" : "value must be an integer greater than zero."
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Schema for a support access grant object in API responses.",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase-enterprise.support-access-grants.schema.grant-response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/ee/support-access-grant" ]
|
||||
}
|
||||
},
|
||||
"/api/ee/transform-job/" : {
|
||||
"post" : {
|
||||
"summary" : "POST /api/ee/transform-job/",
|
||||
@@ -25813,6 +26419,25 @@
|
||||
"tags" : [ "/api/preview_embed" ]
|
||||
}
|
||||
},
|
||||
"/api/preview_embed/dashboard/{token}/params/{param-key}/search/{prefix}" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/preview_embed/dashboard/{token}/params/{param-key}/search/{prefix}",
|
||||
"description" : "Embedded version of chain filter search endpoint.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
},
|
||||
"4XX" : {
|
||||
"description" : "Client error response"
|
||||
},
|
||||
"5XX" : {
|
||||
"description" : "Server error response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "/api/preview_embed" ]
|
||||
}
|
||||
},
|
||||
"/api/preview_embed/dashboard/{token}/params/{param-key}/values" : {
|
||||
"get" : {
|
||||
"summary" : "GET /api/preview_embed/dashboard/{token}/params/{param-key}/values",
|
||||
@@ -27761,6 +28386,15 @@
|
||||
"type" : "boolean",
|
||||
"default" : false
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "collection",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "integer",
|
||||
"minimum" : 1
|
||||
},
|
||||
"description" : "value must be an integer greater than zero."
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "table_db_id",
|
||||
@@ -28771,7 +29405,63 @@
|
||||
"get" : {
|
||||
"summary" : "GET /api/table/",
|
||||
"description" : "Get all `Tables`.",
|
||||
"parameters" : [ ],
|
||||
"parameters" : [ {
|
||||
"in" : "query",
|
||||
"name" : "term",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "visibility-type",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "data-layer",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-layers"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "data-source",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-sources"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "owner-user-id",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "integer"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "owner-email",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "orphan-only",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "boolean"
|
||||
}
|
||||
}, {
|
||||
"in" : "query",
|
||||
"name" : "unused-only",
|
||||
"required" : false,
|
||||
"schema" : {
|
||||
"type" : "boolean"
|
||||
}
|
||||
} ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
"description" : "Successful response"
|
||||
@@ -28787,7 +29477,7 @@
|
||||
},
|
||||
"put" : {
|
||||
"summary" : "PUT /api/table/",
|
||||
"description" : "Update all `Table` in `ids`.",
|
||||
"description" : "Update all `Table` in `ids`.\n\n Deprecated, should use PUT /table/edit from now on.",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"2XX" : {
|
||||
@@ -28816,9 +29506,18 @@
|
||||
"data_authority" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
|
||||
},
|
||||
"data_layer" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"show_in_getting_started" : {
|
||||
"type" : "boolean"
|
||||
},
|
||||
"owner_email" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"owner_user_id" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
"ids" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
@@ -28831,6 +29530,9 @@
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
},
|
||||
"data_source" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"caveats" : {
|
||||
"type" : "string"
|
||||
},
|
||||
@@ -28979,13 +29681,25 @@
|
||||
"data_authority" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
|
||||
},
|
||||
"data_layer" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"show_in_getting_started" : {
|
||||
"type" : "boolean"
|
||||
},
|
||||
"owner_email" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"owner_user_id" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
"display_name" : {
|
||||
"type" : "string",
|
||||
"minLength" : 1
|
||||
},
|
||||
"data_source" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"caveats" : {
|
||||
"type" : "string"
|
||||
},
|
||||
|
||||
@@ -11,9 +11,16 @@ Admins can enable editable table data in Metabase.
|
||||
|
||||

|
||||
|
||||
## Databases that support editable tables
|
||||
|
||||
Currently, editable tables are available for the following databases:
|
||||
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- Metabase Sample Database
|
||||
|
||||
## Setting up editable tables
|
||||
|
||||
_Currently, editable table data is only available for PostgreSQL and MySQL._
|
||||
|
||||
To set up table editing, admins can:
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ Both components are built and assembled together into a single JAR file. In the
|
||||
|
||||
## Quick start
|
||||
|
||||
First, make sure your local dev tooling matches the Metabase requirements. Run:
|
||||
|
||||
```
|
||||
./bin/dev-requirements
|
||||
```
|
||||
|
||||
To spin up a development environment, run:
|
||||
|
||||
```
|
||||
@@ -152,10 +158,12 @@ use Metabase, don't forget to start the frontend as well (e.g. with `yarn build-
|
||||
By default Rspack runs the development server on port `8088`. You can run multiple instances of Metabase on the same machine by specifying a different port for each instance.
|
||||
|
||||
Frontend:
|
||||
|
||||
- If you are running the frontend with `yarn build-hot`, set the `MB_FRONTEND_DEV_PORT` environment variable: `MB_FRONTEND_DEV_PORT=8089 MB_EDITION=ee yarn build-hot`
|
||||
- If you are building the frontend statically with `yarn build`, there is nothing different to do
|
||||
|
||||
Backend:
|
||||
|
||||
- Set the `MB_JETTY_PORT` environment variable and `MB_FRONTEND_DEV_PORT` to the same one as for the frontend.
|
||||
|
||||
### The application database
|
||||
|
||||
@@ -4,6 +4,12 @@ title: Driver interface changelog
|
||||
|
||||
# Driver Interface Changelog
|
||||
|
||||
## Metabase 0.58.0
|
||||
|
||||
- Added a `:collate` feature for drivers that support collation settings on text fields
|
||||
|
||||
- Added `metabase.driver/compile-insert` to implement incremental transforms.
|
||||
|
||||
## Metabase 0.57.0
|
||||
|
||||
- `driver/field-reference-mlv2` is now deprecated, and is no longer used. Please remove your implementations.
|
||||
|
||||
@@ -122,28 +122,27 @@ One great feature of Cypress is that you can use the Chrome inspector after each
|
||||
|
||||
`yarn build` and `yarn build-hot` each overwrite an HTML template to reference the correct JavaScript files. If you run `yarn build` before building an Uberjar for Cypress tests, you won’t see changes to your JavaScript reflected even if you then start `yarn build-hot`.
|
||||
|
||||
### Running Cypress on M1 machines
|
||||
### Running Cypress on Apple Silicon
|
||||
|
||||
You might run into problems when running Cypress on Apple Silicon processors.
|
||||
|
||||
You might run into problems when running Cypress on M1 machine.
|
||||
This is caused by the `@bahmutov/cypress-esbuild-preprocessor` that is using `esbuild` as a dependency. The error might look [like this](https://github.com/evanw/esbuild/issues/1819#issuecomment-1018771557). [The solution](https://github.com/evanw/esbuild/issues/1819#issuecomment-1080720203) is to install NodeJS using one of the Node version managers like [nvm](https://github.com/nvm-sh/nvm) or [n](https://github.com/tj/n).
|
||||
|
||||
Another issue you will almost surely face is the inability to connect to our Mongo QA Database. You can solve it by providing the following env:
|
||||
Another issue you will almost surely face is the inability to connect to our Mongo QA Database. The supported Docker image is incompatible (AMD64). You can solve it by providing the following env:
|
||||
|
||||
```shell
|
||||
export EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
|
||||
```
|
||||
|
||||
Please note that some users experienced Mongo connection timeouts even with this env var set. If that happens, try using OrbStack instead of Docker Desktop.
|
||||
|
||||
### Running tests that depend on Docker images
|
||||
|
||||
A subset of our tests depend on the external services that are available through the Docker images. At the time of this writing, those are the three supported external QA databases, Webmail, Snowplow and LDAP servers. The default cypress command will spin up all necessary docker containers for these tests to function properly, but you can toggle them off if you want
|
||||
|
||||
```sh
|
||||
START_CONTAINERS=false yarn test-cypress
|
||||
```
|
||||
A large portion of our tests depend on the external services that are available through the Docker images. At the time of this writing, those are the three supported external QA databases, Webmail, Snowplow and LDAP servers. See `e2e/test/scenarios/docker-compose.yml` for up to date information. The default cypress command will spin up all necessary Docker containers for the tests to function properly. You can manually set up the e2e environment without them but be aware that you will run into test failures.
|
||||
|
||||
### Running tests with Snowplow involved
|
||||
|
||||
Tests that depend on Snowplow expect a running server. This is enabled by default. You can manually enable them as well by spinning up the snowplow micro docker container and setting the appropriate environment variables:
|
||||
Tests that depend on Snowplow expect a running server. This is enabled by default. You can manually enable them as well by spinning up the Snowplow micro Docker container and setting the appropriate environment variables:
|
||||
|
||||
```
|
||||
docker-compose -f ./snowplow/docker-compose.yml up -d
|
||||
|
||||
@@ -18,7 +18,7 @@ Metabase will print out the help text for available commands.
|
||||
|
||||
## `api-documentation`
|
||||
|
||||
Generate a markdown file containing documentation for all API endpoints. This is written to a file called `docs/api-documentation.md`.
|
||||
Generate an HTML file and a JSON file for Scalar docs for the Metabase API.
|
||||
|
||||
## `command-documentation`
|
||||
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
const cypress = require("cypress");
|
||||
|
||||
const { FAILURE_EXIT_CODE } = require("./constants/exit-code");
|
||||
|
||||
/**
|
||||
* This simply runs Cypress through the Cypress Module API rather than the CLI.
|
||||
* Runs Cypress through the Module API and throws on any failure.
|
||||
* See: https://docs.cypress.io/app/references/module-api
|
||||
*
|
||||
* @param {Partial<CypressCommandLine.CypressRunOptions> | Partial<CypressCommandLine.CypressOpenOptions>} options - Cypress configuration options for running or opening tests
|
||||
* @param {function(number): Promise<void>} exitFunction - Async function to call on exit with the exit code
|
||||
* @returns {Promise<CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult | void>}
|
||||
* @throws {Error} When Cypress fails to run tests
|
||||
* Errors are propagated to the parent process rather than handled here.
|
||||
* The caller is responsible for catching errors and performing cleanup.
|
||||
*
|
||||
* This wrapper exists because cypress.run() resolves successfully even when
|
||||
* tests fail - it only throws when Cypress itself fails to start (e.g., invalid
|
||||
* config). We convert test failures into thrown errors for consistent handling.
|
||||
*
|
||||
* @param {Partial<CypressCommandLine.CypressRunOptions> | Partial<CypressCommandLine.CypressOpenOptions>} options - Cypress configuration options
|
||||
* @returns {Promise<CypressCommandLine.CypressRunResult>} - Resolves with results if all tests pass
|
||||
* @throws {Error} When Cypress fails to run (e.g., config error) or any tests fail
|
||||
*/
|
||||
const runCypress = async (options, exitFunction) => {
|
||||
const runCypress = async (options) => {
|
||||
const openMode = process.env.OPEN_UI === "true";
|
||||
|
||||
try {
|
||||
const { status, message, totalFailed, failures } = openMode
|
||||
? await cypress.open(options)
|
||||
: await cypress.run(options);
|
||||
|
||||
// At least one test failed
|
||||
if (totalFailed > 0) {
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Something went wrong and Cypress failed to even run tests
|
||||
if (status === "failed" && failures) {
|
||||
console.error(message);
|
||||
|
||||
await exitFunction(failures);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to run Cypress!\n", e);
|
||||
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
// Open mode is interactive - no results to validate
|
||||
if (openMode) {
|
||||
await cypress.open(options);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await cypress.run(options);
|
||||
|
||||
// Cypress failed to even run tests (e.g., config error, browser crash)
|
||||
if (result.status === "failed") {
|
||||
throw new Error(`Cypress failed to run: ${result.message}`);
|
||||
}
|
||||
|
||||
// Tests ran but at least one failed
|
||||
if (result.totalFailed > 0) {
|
||||
throw new Error(`${result.totalFailed} test(s) failed`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = runCypress;
|
||||
|
||||
@@ -22,7 +22,7 @@ const CypressBackend = {
|
||||
|
||||
this.server = server;
|
||||
},
|
||||
async start() {
|
||||
async start(jarPath = "target/uberjar/metabase.jar") {
|
||||
if (!this.server) {
|
||||
this.createServer();
|
||||
}
|
||||
@@ -33,6 +33,11 @@ const CypressBackend = {
|
||||
"-Djava.awt.headless=true", // when running on macOS prevent little Java icon from popping up in Dock
|
||||
"-Dmail.smtps.ssl.trust=*", // trust self signed certs for testing
|
||||
"-Duser.timezone=US/Pacific",
|
||||
// FIXME @nemanjaglumac 2025-11-28
|
||||
// This is a bit convoluted because this particular log4j config outputs all logs to a file (logs/test.log)
|
||||
// See: https://github.com/metabase/metabase/pull/21360
|
||||
// If we want them in the console, we'd have to unset the log4j config, to let stdio inherit the logs.
|
||||
// Override in your .env if you need to see the logs in the console locally.
|
||||
process.env.SHOW_BACKEND_LOGS === "true"
|
||||
? null
|
||||
: `-Dlog4j.configurationFile=file:${__dirname}/../../frontend/test/__runner__/log4j2.xml`,
|
||||
@@ -50,24 +55,22 @@ const CypressBackend = {
|
||||
MB_CONFIG_FILE_PATH: "__cypress__", // ignore config.yml
|
||||
MB_HTTP_CHANNEL_HOST_STRATEGY: "allow-all", // we use a local webhook service for testing
|
||||
MB_IS_CYPRESS: "true", // custom flag so we can detect we're running in Cypress, used in tests.
|
||||
MB_SNOWPLOW_AVAILABLE: true,
|
||||
MB_SNOWPLOW_URL: "http://localhost:9090",
|
||||
};
|
||||
|
||||
this.server.process = spawn(
|
||||
"java",
|
||||
[...javaFlags, "-jar", "target/uberjar/metabase.jar"],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...metabaseConfig,
|
||||
},
|
||||
stdio:
|
||||
process.env["DISABLE_LOGGING"] ||
|
||||
process.env["DISABLE_LOGGING_BACKEND"]
|
||||
? "ignore"
|
||||
: "inherit",
|
||||
detached: true,
|
||||
this.server.process = spawn("java", [...javaFlags, "-jar", jarPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
...metabaseConfig,
|
||||
},
|
||||
);
|
||||
stdio:
|
||||
process.env["DISABLE_LOGGING"] ||
|
||||
process.env["DISABLE_LOGGING_BACKEND"]
|
||||
? "ignore"
|
||||
: "inherit",
|
||||
detached: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isReady(this.server.host))) {
|
||||
|
||||
@@ -19,7 +19,7 @@ const userOptions = {
|
||||
...process.env,
|
||||
} as const;
|
||||
|
||||
printBold(`Running Cypress Sample App Tests with options:
|
||||
printBold(`Setting up the Sample App environment with options:
|
||||
- EMBEDDING_SDK_VERSION : ${userOptions.EMBEDDING_SDK_VERSION}
|
||||
- SAMPLE_APP_BRANCH_NAME : ${userOptions.SAMPLE_APP_BRANCH_NAME}
|
||||
`);
|
||||
|
||||
@@ -37,7 +37,7 @@ const startServer = async () => {
|
||||
|
||||
const runTests = async (config, cliArguments = []) => {
|
||||
const userOverrides = await parseArguments(cliArguments);
|
||||
await runCypress({ ...config, ...userOverrides }, process.exit);
|
||||
await runCypress({ ...config, ...userOverrides });
|
||||
};
|
||||
|
||||
// Custom "modes"
|
||||
@@ -46,7 +46,10 @@ if (command === "start") {
|
||||
}
|
||||
|
||||
if (command === "snapshot") {
|
||||
runTests({ configFile: "e2e/support/cypress-snapshots.config.js" });
|
||||
runTests(
|
||||
{ configFile: "e2e/support/cypress-snapshots.config.js" },
|
||||
cliArguments,
|
||||
);
|
||||
}
|
||||
|
||||
// Metabase component or e2e tests
|
||||
|
||||
46
e2e/runner/run_cypress_host_sample_apps.ts
Normal file
46
e2e/runner/run_cypress_host_sample_apps.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { FAILURE_EXIT_CODE, SUCCESS_EXIT_CODE } from "./constants/exit-code";
|
||||
import runCypress from "./cypress-node-js-runner";
|
||||
import { booleanify, printBold } from "./cypress-runner-utils";
|
||||
import { startHostAppContainers } from "./embedding-sdk/host-apps/start-host-app-containers";
|
||||
import { startSampleAppContainers } from "./embedding-sdk/sample-apps/start-sample-app-containers";
|
||||
import { resolveSdkE2EConfig } from "./resolve-sdk-e2e-config";
|
||||
|
||||
// if you want to change these, set them as environment variables in your shell
|
||||
const options = {
|
||||
SDK_TEST_SUITE: "vite-6-host-app-e2e", // one of the many sample-app, or host-app Embedding SDK suites
|
||||
OPEN_UI: false,
|
||||
...booleanify(process.env),
|
||||
};
|
||||
|
||||
printBold(`Running Cypress with options:
|
||||
- SDK_TEST_SUITE : ${options.SDK_TEST_SUITE}
|
||||
- OPEN_UI : ${options.OPEN_UI}
|
||||
`);
|
||||
|
||||
const init = async () => {
|
||||
switch (options.SDK_TEST_SUITE) {
|
||||
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
|
||||
case "metabase-nextjs-sdk-embedding-sample-e2e":
|
||||
case "shoppy-e2e":
|
||||
await startSampleAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
|
||||
case "vite-6-host-app-e2e":
|
||||
case "next-15-app-router-host-app-e2e":
|
||||
case "next-15-pages-router-host-app-e2e":
|
||||
case "angular-20-host-app-e2e":
|
||||
await startHostAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
}
|
||||
|
||||
printBold("⏳ Starting Sample/Host App Cypress Tests");
|
||||
const config = resolveSdkE2EConfig(options.SDK_TEST_SUITE);
|
||||
await runCypress(config);
|
||||
};
|
||||
|
||||
init()
|
||||
.then(() => process.exit(SUCCESS_EXIT_CODE))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(FAILURE_EXIT_CODE);
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
import { BACKEND_PORT } from "./constants/backend-port";
|
||||
@@ -13,109 +11,73 @@ import {
|
||||
shell,
|
||||
unBooleanify,
|
||||
} from "./cypress-runner-utils";
|
||||
import { startHostAppContainers } from "./embedding-sdk/host-apps/start-host-app-containers";
|
||||
import { startSampleAppContainers } from "./embedding-sdk/sample-apps/start-sample-app-containers";
|
||||
import { resolveSdkE2EConfig } from "./resolve-sdk-e2e-config";
|
||||
|
||||
let tempSampleDBDir: string | null = null;
|
||||
|
||||
// if you want to change these, set them as environment variables in your shell
|
||||
const userOptions = {
|
||||
const options = {
|
||||
CYPRESS_TESTING_TYPE: "e2e", // e2e | component
|
||||
SDK_TEST_SUITE: undefined, // one of the many sample-app, or host-app Embedding SDK suites
|
||||
MB_EDITION: "ee", // ee | oss
|
||||
START_CONTAINERS: true,
|
||||
STOP_CONTAINERS: false,
|
||||
BUILD_BACKEND: true,
|
||||
BACKEND_PORT: BACKEND_PORT, // override with MB_JETTY_PORT in your env
|
||||
OPEN_UI: true,
|
||||
SHOW_BACKEND_LOGS: false,
|
||||
GENERATE_SNAPSHOTS: true,
|
||||
QUIET: false,
|
||||
TZ: "UTC",
|
||||
...booleanify(process.env),
|
||||
};
|
||||
|
||||
const derivedOptions = {
|
||||
QA_DB_ENABLED: userOptions.START_CONTAINERS,
|
||||
BUILD_JAR: userOptions.BACKEND_PORT === 4000,
|
||||
START_BACKEND: userOptions.BACKEND_PORT === 4000,
|
||||
MB_SNOWPLOW_AVAILABLE: true,
|
||||
MB_SNOWPLOW_URL: "http://localhost:9090",
|
||||
};
|
||||
|
||||
const options = {
|
||||
...derivedOptions,
|
||||
...userOptions,
|
||||
};
|
||||
|
||||
process.env = unBooleanify(options);
|
||||
|
||||
const missingTokens = [
|
||||
"MB_ALL_FEATURES_TOKEN",
|
||||
"MB_STARTER_CLOUD_TOKEN",
|
||||
"MB_PRO_CLOUD_TOKEN",
|
||||
"MB_PRO_SELF_HOSTED_TOKEN",
|
||||
"CYPRESS_MB_ALL_FEATURES_TOKEN",
|
||||
"CYPRESS_MB_STARTER_CLOUD_TOKEN",
|
||||
"CYPRESS_MB_PRO_CLOUD_TOKEN",
|
||||
"CYPRESS_MB_PRO_SELF_HOSTED_TOKEN",
|
||||
].filter((token) => !process.env[token]);
|
||||
|
||||
if (options.MB_EDITION === "ee" && missingTokens.length > 0) {
|
||||
printBold(
|
||||
`⚠️ Missing tokens: ${missingTokens.join(", ")}. Either set them or run with MB_EDITION=oss`,
|
||||
);
|
||||
process.exit(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
printBold(`Running Cypress with options:
|
||||
- CYPRESS_TESTING_TYPE : ${options.CYPRESS_TESTING_TYPE}
|
||||
- SDK_TEST_SUITE : ${options.SDK_TEST_SUITE}
|
||||
- MB_EDITION : ${options.MB_EDITION}
|
||||
- START_CONTAINERS : ${options.START_CONTAINERS}
|
||||
- STOP_CONTAINERS : ${options.STOP_CONTAINERS}
|
||||
- BUILD_JAR : ${options.BUILD_JAR}
|
||||
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
|
||||
- BACKEND_PORT : ${options.BACKEND_PORT}
|
||||
- START_BACKEND : ${options.START_BACKEND}
|
||||
- BUILD_BACKEND : ${options.BUILD_BACKEND}
|
||||
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
|
||||
- OPEN_UI : ${options.OPEN_UI}
|
||||
- SHOW_BACKEND_LOGS : ${options.SHOW_BACKEND_LOGS}
|
||||
- TZ : ${options.TZ}
|
||||
`);
|
||||
|
||||
const init = async () => {
|
||||
const cliArguments = process.argv.slice(2);
|
||||
const userOverrides = await parseArguments(cliArguments);
|
||||
|
||||
if (options.START_CONTAINERS) {
|
||||
printBold("⏳ Starting containers");
|
||||
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml up -d");
|
||||
}
|
||||
printBold("⏳ Starting containers");
|
||||
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml up -d");
|
||||
|
||||
if (options.BUILD_JAR) {
|
||||
if (options.BUILD_BACKEND) {
|
||||
printBold("⏳ Building backend");
|
||||
shell("./bin/build-for-test");
|
||||
shell("./bin/build-backend-for-test");
|
||||
|
||||
if (options.START_BACKEND) {
|
||||
const isBackendRunning = shell(
|
||||
`lsof -ti:${options.BACKEND_PORT} || echo ""`,
|
||||
{ quiet: true },
|
||||
const isBackendRunning = shell(
|
||||
`lsof -ti:${options.BACKEND_PORT} || echo ""`,
|
||||
{ quiet: true },
|
||||
);
|
||||
if (isBackendRunning) {
|
||||
printBold(
|
||||
"⚠️ Your backend is already running, you may want to kill pid " +
|
||||
isBackendRunning,
|
||||
);
|
||||
if (isBackendRunning) {
|
||||
printBold(
|
||||
"⚠️ Your backend is already running, you may want to kill pid " +
|
||||
isBackendRunning,
|
||||
);
|
||||
process.exit(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Use a temporary copy of the sample db so it won't use and lock the db used for local development
|
||||
tempSampleDBDir = path.join(
|
||||
os.tmpdir(),
|
||||
`metabase-sample-db-e2e-${process.pid}`,
|
||||
);
|
||||
fs.mkdirSync(tempSampleDBDir, { recursive: true });
|
||||
process.env.MB_INTERNAL_DO_NOT_USE_SAMPLE_DB_DIR = tempSampleDBDir;
|
||||
|
||||
printBold("⏳ Starting backend");
|
||||
await CypressBackend.start();
|
||||
process.exit(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Use a temporary copy of the sample db so it won't use and lock the db used for local development
|
||||
process.env.MB_INTERNAL_DO_NOT_USE_SAMPLE_DB_DIR = path.resolve(
|
||||
__dirname,
|
||||
"../../e2e/tmp", // already exists and is .gitignored
|
||||
);
|
||||
|
||||
printBold("⏳ Starting backend");
|
||||
await CypressBackend.start("target/uberjar/metabase-backend.jar");
|
||||
} else {
|
||||
printBold(
|
||||
`Not building a jar, expecting metabase to be running on port ${options.BACKEND_PORT}. Make sure your metabase instance is running with an h2 app db and the following environment variables:
|
||||
@@ -131,10 +93,12 @@ const init = async () => {
|
||||
|
||||
printBold("⏳ Generating app db snapshots");
|
||||
process.env.OPEN_UI = "false";
|
||||
await runCypress(
|
||||
{ configFile: "e2e/support/cypress-snapshots.config.js" },
|
||||
cleanup,
|
||||
);
|
||||
await runCypress({
|
||||
configFile: "e2e/support/cypress-snapshots.config.js",
|
||||
...(options.CYPRESS_TESTING_TYPE === "component" && {
|
||||
env: { grepTags: "-@external" }, // component tests do not need QA DB snapshots for now
|
||||
}),
|
||||
});
|
||||
process.env.OPEN_UI = `${options.OPEN_UI}`;
|
||||
} else {
|
||||
printBold("Skipping snapshot generation, beware of stale snapshot caches");
|
||||
@@ -148,71 +112,31 @@ const init = async () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (options.SDK_TEST_SUITE) {
|
||||
switch (options.SDK_TEST_SUITE) {
|
||||
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
|
||||
case "metabase-nextjs-sdk-embedding-sample-e2e":
|
||||
case "shoppy-e2e":
|
||||
await startSampleAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
|
||||
case "vite-6-host-app-e2e":
|
||||
case "next-15-app-router-host-app-e2e":
|
||||
case "next-15-pages-router-host-app-e2e":
|
||||
case "angular-20-host-app-e2e":
|
||||
await startHostAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
}
|
||||
|
||||
printBold("⏳ Starting Sample/Host App Cypress Tests");
|
||||
const config = resolveSdkE2EConfig(options.SDK_TEST_SUITE);
|
||||
await runCypress(config, cleanup);
|
||||
}
|
||||
|
||||
if (options.CYPRESS_TESTING_TYPE === "component") {
|
||||
printBold("⏳ Starting Cypress SDK component tests");
|
||||
await runCypress(
|
||||
{
|
||||
configFile:
|
||||
"e2e/support/cypress-embedding-sdk-component-test.config.js",
|
||||
testingType: "component",
|
||||
},
|
||||
cleanup,
|
||||
);
|
||||
await runCypress({
|
||||
configFile: "e2e/support/cypress-embedding-sdk-component-test.config.js",
|
||||
testingType: "component",
|
||||
});
|
||||
}
|
||||
|
||||
if (options.CYPRESS_TESTING_TYPE === "e2e") {
|
||||
const config = { configFile: "e2e/support/cypress.config.js" };
|
||||
|
||||
printBold("⏳ Starting Cypress");
|
||||
await runCypress({ ...config, ...userOverrides }, cleanup);
|
||||
await runCypress({ ...config, ...userOverrides });
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = async (exitCode: string | number = SUCCESS_EXIT_CODE) => {
|
||||
if (options.BUILD_JAR) {
|
||||
if (options.BUILD_BACKEND) {
|
||||
printBold("⏳ Cleaning up...");
|
||||
await CypressBackend.stop();
|
||||
}
|
||||
|
||||
// Add cleanup for the temporary sample database directory
|
||||
if (tempSampleDBDir) {
|
||||
try {
|
||||
fs.rmSync(tempSampleDBDir, { recursive: true, force: true });
|
||||
printBold(
|
||||
`🗑️ Cleaned up temporary sample database directory: ${tempSampleDBDir}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Error cleaning up temporary sample database directory: ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.STOP_CONTAINERS) {
|
||||
printBold("⏳ Stopping containers");
|
||||
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml down");
|
||||
}
|
||||
printBold(
|
||||
"🧹 Containers are running in background. If you wish to stop them, run:\n`docker compose -f ./e2e/test/scenarios/docker-compose.yml down`",
|
||||
);
|
||||
|
||||
typeof exitCode === "number"
|
||||
? process.exit(exitCode)
|
||||
|
||||
@@ -9,29 +9,29 @@ import {
|
||||
|
||||
describe("qa databases snapshots", { tags: "@external" }, () => {
|
||||
it("creates snapshots for supported qa databases", () => {
|
||||
if (Cypress.env("QA_DB_MONGO") === true) {
|
||||
restoreAndAuthenticate();
|
||||
addMongoDatabase();
|
||||
snapshot("mongo-5");
|
||||
} else {
|
||||
// Postgres
|
||||
restoreAndAuthenticate();
|
||||
// Mongo
|
||||
restoreAndAuthenticate();
|
||||
|
||||
addPostgresDatabase();
|
||||
snapshot("postgres-12");
|
||||
addMongoDatabase();
|
||||
snapshot("mongo-5");
|
||||
|
||||
convertToWritable("postgres");
|
||||
snapshot("postgres-writable");
|
||||
// Postgres
|
||||
restoreAndAuthenticate();
|
||||
|
||||
// MySQL
|
||||
restoreAndAuthenticate();
|
||||
addPostgresDatabase();
|
||||
snapshot("postgres-12");
|
||||
|
||||
addMySQLDatabase({});
|
||||
snapshot("mysql-8");
|
||||
convertToWritable("postgres");
|
||||
snapshot("postgres-writable");
|
||||
|
||||
convertToWritable("mysql");
|
||||
snapshot("mysql-writable");
|
||||
}
|
||||
// MySQL
|
||||
restoreAndAuthenticate();
|
||||
|
||||
addMySQLDatabase({});
|
||||
snapshot("mysql-8");
|
||||
|
||||
convertToWritable("mysql");
|
||||
snapshot("mysql-writable");
|
||||
|
||||
restore("blank");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ slug: synced_collection
|
||||
archive_operation_id: null
|
||||
name: Synced Collection
|
||||
personal_owner_id: null
|
||||
type: remote-synced
|
||||
type: null
|
||||
is_sample: false
|
||||
parent_id: null
|
||||
serdes/meta:
|
||||
@@ -16,3 +16,4 @@ archived_directly: null
|
||||
entity_id: AF_m-cEw4pFjXYd90_tnj
|
||||
namespace: null
|
||||
created_at: '2025-10-23T09:38:19.057632-07:00'
|
||||
is_remote_synced: true
|
||||
|
||||
@@ -2,15 +2,10 @@ const { defineConfig } = require("cypress");
|
||||
|
||||
const { defaultConfig } = require("./config");
|
||||
|
||||
const isQaDatabase = process.env["QA_DB_ENABLED"] === "true";
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
...defaultConfig,
|
||||
specPattern: "e2e/snapshot-creators/**/*.cy.snap.js",
|
||||
excludeSpecPattern: !isQaDatabase
|
||||
? "e2e/snapshot-creators/qa-db.cy.snap.js"
|
||||
: undefined,
|
||||
video: false,
|
||||
},
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -226,33 +248,37 @@ function getTableDescriptionInput() {
|
||||
}
|
||||
|
||||
function getTableSortButton() {
|
||||
return getTableSection().button(/Sorting/);
|
||||
return getTableSection().findByRole("button", { name: "Sorting" });
|
||||
}
|
||||
|
||||
function getTableSortDoneButton() {
|
||||
return getTableSection().button(/Done/);
|
||||
return getTableSection().findByRole("button", { name: "Done" });
|
||||
}
|
||||
|
||||
function getTableSortOrderInput() {
|
||||
return getTableSection().findByLabelText("Column order");
|
||||
return getTableSection().findByRole("radiogroup", { name: "Column order" });
|
||||
}
|
||||
|
||||
function getTableSyncOptionsButton() {
|
||||
return getTableSection().button(/Sync options/);
|
||||
return getTableSection().findByRole("button", { name: /Sync/ });
|
||||
}
|
||||
|
||||
function getTableSectionField(name: string) {
|
||||
return getTableSection().findByLabelText(name);
|
||||
return getTableSection().findByRole("listitem", { name });
|
||||
}
|
||||
|
||||
function getTableSectionSortableField(name: string) {
|
||||
return getTableSection().findByLabelText(name);
|
||||
return getTableSection().findByRole("listitem", { name });
|
||||
}
|
||||
|
||||
function getTableSectionSortableFields() {
|
||||
return getTableSection().findAllByRole("listitem");
|
||||
}
|
||||
|
||||
function getTableSectionVisibilityTypeInput() {
|
||||
return getTableSection().findByRole("textbox", { name: "Visibility type" });
|
||||
}
|
||||
|
||||
function getTableSectionFieldNameInput(name: string) {
|
||||
return getTableSectionField(name).findByPlaceholderText(
|
||||
"Give this field a name",
|
||||
|
||||
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) {
|
||||
|
||||
@@ -96,7 +96,7 @@ export const wrapSyncedCollection = (alias = "syncedCollection", n = 0) => {
|
||||
|
||||
cy.request("/api/collection").then(({ body: collections }) => {
|
||||
const syncedCollection = collections.find(
|
||||
(c: Collection) => c.type === "remote-synced" && c.location === "/",
|
||||
(c: Collection) => c.is_remote_synced && c.location === "/",
|
||||
);
|
||||
|
||||
if (syncedCollection) {
|
||||
|
||||
@@ -19,8 +19,6 @@ export function snapshot(name) {
|
||||
* } name
|
||||
*/
|
||||
export function restore(name = "default") {
|
||||
cy.skipOn(name.includes("mongo") && Cypress.env("QA_DB_MONGO") !== true);
|
||||
|
||||
cy.log("Restore Data Set");
|
||||
|
||||
// automatically reset the data db if this is a test that uses a writable db
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -288,6 +300,10 @@ export const moveColumnDown = (column, distance) => {
|
||||
.trigger("mouseup", 0, distance * 50, { force: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `moveDnDKitElementByAlias` instead.
|
||||
* Otherwise, the chain will be broken due to "element was removed from the DOM" error
|
||||
*/
|
||||
export const moveDnDKitElement = (
|
||||
element,
|
||||
{ horizontal = 0, vertical = 0, onBeforeDragEnd = () => {} } = {},
|
||||
@@ -407,6 +423,10 @@ export const undoToastList = () => {
|
||||
return cy.findAllByTestId("toast-undo");
|
||||
};
|
||||
|
||||
export const undoToastListContainer = () => {
|
||||
return cy.findByTestId("undo-list");
|
||||
};
|
||||
|
||||
export function dashboardCards() {
|
||||
return cy.get("[data-element-id=dashboard-cards-container]");
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
93
e2e/support/test-library-data.ts
Normal file
93
e2e/support/test-library-data.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
const { H } = cy;
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import type { StructuredQuestionDetails } from "e2e/support/helpers/api";
|
||||
import type { Collection } from "metabase-types/api";
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
type StructuredQuestionDetailsWithName = StructuredQuestionDetails & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type LibraryResponse = Collection & {
|
||||
effective_children?: Collection[];
|
||||
};
|
||||
|
||||
export const TRUSTED_ORDERS_MODEL: StructuredQuestionDetailsWithName = {
|
||||
name: "Trusted Orders Model",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
},
|
||||
};
|
||||
|
||||
export const TRUSTED_ORDERS_METRIC: StructuredQuestionDetailsWithName = {
|
||||
name: "Trusted Orders Metric",
|
||||
query: {
|
||||
"source-table": ORDERS_ID,
|
||||
aggregation: [["count"]],
|
||||
},
|
||||
};
|
||||
|
||||
export function createLibraryWithItems() {
|
||||
return H.createLibrary().then(
|
||||
(response: Cypress.Response<LibraryResponse>) => {
|
||||
const body = response.body;
|
||||
const modelsCollection = body.effective_children?.find(
|
||||
(child) => child.name === "Data",
|
||||
);
|
||||
const metricsCollection = body.effective_children?.find(
|
||||
(child) => child.name === "Metrics",
|
||||
);
|
||||
|
||||
return H.createQuestion(TRUSTED_ORDERS_MODEL, {
|
||||
wrapId: true,
|
||||
idAlias: "trustedOrdersModelId",
|
||||
}).then(() =>
|
||||
cy.get("@trustedOrdersModelId").then((modelId) =>
|
||||
cy
|
||||
.request("PUT", `/api/card/${modelId}`, {
|
||||
type: "model",
|
||||
collection_id: modelsCollection?.id,
|
||||
})
|
||||
.then(() =>
|
||||
H.createQuestion(TRUSTED_ORDERS_METRIC, {
|
||||
wrapId: true,
|
||||
idAlias: "trustedOrdersMetricId",
|
||||
}),
|
||||
)
|
||||
.then(() =>
|
||||
cy.get("@trustedOrdersMetricId").then((metricId) =>
|
||||
cy.request("PUT", `/api/card/${metricId}`, {
|
||||
type: "metric",
|
||||
collection_id: metricsCollection?.id,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function createLibraryWithModel() {
|
||||
return H.createLibrary().then(
|
||||
(response: Cypress.Response<LibraryResponse>) => {
|
||||
const body = response.body;
|
||||
const modelsCollection = body.effective_children?.find(
|
||||
(child) => child.name === "Data",
|
||||
);
|
||||
|
||||
return H.createQuestion(TRUSTED_ORDERS_MODEL, {
|
||||
wrapId: true,
|
||||
idAlias: "trustedOrdersModelId",
|
||||
}).then(() =>
|
||||
cy.get("@trustedOrdersModelId").then((modelId) =>
|
||||
cy.request("PUT", `/api/card/${modelId}`, {
|
||||
type: "model",
|
||||
collection_id: modelsCollection?.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -512,6 +512,31 @@ describe("Remote Sync", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable 'Set up Remote Sync' button if git url is not set (#65653)", () => {
|
||||
cy.visit("/admin/settings/remote-sync");
|
||||
cy.button("Set up Remote Sync").should("be.disabled");
|
||||
|
||||
cy.findByRole("switch", { name: "Auto-sync with git" }).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
// Trivial dirty state should not be enough to enable the button
|
||||
cy.button("Set up Remote Sync").should("be.disabled");
|
||||
|
||||
cy.findByLabelText(/Access Token/i)
|
||||
.clear()
|
||||
.type("SecretToken");
|
||||
// Still disabled - url is not set
|
||||
cy.button("Set up Remote Sync").should("be.disabled");
|
||||
|
||||
cy.findByLabelText(/repository url/i)
|
||||
.clear()
|
||||
.type(LOCAL_GIT_URL);
|
||||
|
||||
// Enabled now - url is set
|
||||
cy.button("Set up Remote Sync").should("be.enabled");
|
||||
});
|
||||
|
||||
it("shows an error if git settings are invalid", () => {
|
||||
cy.intercept("PUT", "/api/ee/remote-sync/settings").as("saveSettings");
|
||||
cy.visit("/admin/settings/remote-sync");
|
||||
|
||||
@@ -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 in the database").should("have.text", "ORDERS");
|
||||
cy.findByLabelText("Last updated at").should("have.text", expectedDate);
|
||||
cy.findByLabelText("View count").should("have.text", viewCount);
|
||||
cy.findByLabelText("Est. row count").should("not.exist");
|
||||
cy.findByLabelText("Dependencies").should("have.text", "0");
|
||||
cy.findByLabelText("Dependents").should("have.text", "0");
|
||||
|
||||
cy.findByRole("link", { name: "Dependency graph" }).click();
|
||||
cy.findByRole("heading", { name: "Dependency graph" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should publish single table to a collection",
|
||||
{ tags: ["@external"] },
|
||||
() => {
|
||||
H.restore("mysql-8");
|
||||
H.activateToken("bleeding-edge");
|
||||
H.DataModel.visitDataStudio();
|
||||
TablePicker.getDatabase("QA MySQL8").click();
|
||||
TablePicker.getTable("Orders").click();
|
||||
|
||||
// Shows publish model information modal
|
||||
cy.findByRole("button", { name: /Publish/ }).click();
|
||||
cy.findByRole("button", { name: /Got it/ }).click();
|
||||
H.modal().within(() => {
|
||||
cy.findByRole("button", { name: /Cancel/ }).click();
|
||||
});
|
||||
|
||||
// Don't show this again, info should not be shown later
|
||||
cy.findByRole("button", { name: /Publish/ }).click();
|
||||
cy.findByLabelText("Don’t show this to me again").check();
|
||||
cy.findByRole("button", { name: /Got it/ }).click();
|
||||
H.modal().within(() => {
|
||||
cy.findByRole("button", { name: /Cancel/ }).click();
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: /Publish/ }).click();
|
||||
H.pickEntity({
|
||||
tab: "Collections",
|
||||
path: ["Our analytics"],
|
||||
});
|
||||
cy.findByRole("button", { name: /Publish here/ }).click();
|
||||
|
||||
cy.wait<PublishModelResponse>("@publishModel").then(() => {
|
||||
H.undoToast().within(() => {
|
||||
cy.findByText("Published").should("be.visible");
|
||||
cy.findByRole("button", { name: /See it/ }).click();
|
||||
});
|
||||
cy.findByTestId("head-crumbs-container").within(() => {
|
||||
cy.findByText("Our analytics").should("be.visible");
|
||||
cy.findByText("Model based on ORDERS").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
// Should not show info modal again
|
||||
cy.go("back");
|
||||
|
||||
cy.findByRole("button", { name: /Publish/ }).click();
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Pick the collection to publish this table in").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should allow to edit attributes", { tags: ["@external"] }, () => {
|
||||
H.restore("postgres-12");
|
||||
H.activateToken("bleeding-edge");
|
||||
H.DataModel.visitDataStudio();
|
||||
TablePicker.getDatabase("QA Postgres12").click();
|
||||
TablePicker.getTable("Orders").click();
|
||||
|
||||
H.selectHasValue("Owner", "Unspecified").click();
|
||||
H.selectDropdown().contains("Bobby Tables").click();
|
||||
H.undoToastListContainer()
|
||||
.findByText("Table owner updated")
|
||||
.should("be.visible");
|
||||
|
||||
H.selectHasValue("Visibility type", "Bronze").click();
|
||||
H.selectDropdown().contains("Gold").click();
|
||||
H.undoToastListContainer()
|
||||
.findByText("Table visibility type updated")
|
||||
.should("be.visible");
|
||||
|
||||
H.selectHasValue("Entity type", "Transaction").click();
|
||||
H.selectDropdown().contains("Person").click();
|
||||
H.undoToastListContainer()
|
||||
.findByText("Entity type updated")
|
||||
.should("be.visible");
|
||||
|
||||
H.selectHasValue("Source", "Unspecified").click();
|
||||
H.selectDropdown().contains("Ingested").click();
|
||||
H.undoToastListContainer()
|
||||
.findByText("Table data source updated")
|
||||
.should("be.visible");
|
||||
|
||||
// navigate away and back
|
||||
TablePicker.getTable("Products").click();
|
||||
TablePicker.getTable("Orders").click();
|
||||
|
||||
H.selectHasValue("Owner", "Bobby Tables");
|
||||
H.selectHasValue("Visibility type", "Gold");
|
||||
H.selectHasValue("Entity type", "Person");
|
||||
H.selectHasValue("Source", "Ingested");
|
||||
});
|
||||
|
||||
it(
|
||||
"transform-created table should have link and disabled source edit",
|
||||
{ tags: ["@external"] },
|
||||
() => {
|
||||
H.restore("postgres-writable");
|
||||
H.activateToken("bleeding-edge");
|
||||
H.resetTestTable({ type: "postgres", table: "many_schemas" });
|
||||
|
||||
const SOURCE_TABLE = "Animals";
|
||||
const TARGET_TABLE = "transform_table";
|
||||
const TARGET_SCHEMA = "Schema A";
|
||||
const TRANSFORM_TABLE_DISPLAY_NAME = "Transform Table";
|
||||
const TRANSFORM_NAME = "Test transform for animals";
|
||||
|
||||
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: SOURCE_TABLE });
|
||||
|
||||
// Create and run a transform to create a table
|
||||
H.createAndRunMbqlTransform({
|
||||
sourceTable: SOURCE_TABLE,
|
||||
targetTable: TARGET_TABLE,
|
||||
targetSchema: TARGET_SCHEMA,
|
||||
name: TRANSFORM_NAME,
|
||||
}).then(() => {
|
||||
H.DataModel.visitDataStudio();
|
||||
TablePicker.getDatabase("Writable Postgres12").click();
|
||||
TablePicker.getSchema(TARGET_SCHEMA).click();
|
||||
TablePicker.getTable(TRANSFORM_TABLE_DISPLAY_NAME).click();
|
||||
|
||||
cy.findByRole("link", { name: new RegExp(TRANSFORM_NAME) }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
H.selectIsDisabled("Source");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
165
e2e/test/scenarios/data-studio/modeling-library.cy.spec.ts
Normal file
165
e2e/test/scenarios/data-studio/modeling-library.cy.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
ALL_USERS_GROUP_ID,
|
||||
ORDERS_MODEL_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import {
|
||||
createLibraryWithItems,
|
||||
createLibraryWithModel,
|
||||
} from "e2e/support/test-library-data";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > data studio > modeling > library", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
});
|
||||
|
||||
it("should create library via UI and verify collections", () => {
|
||||
cy.intercept("POST", "/api/ee/library").as("createLibrary");
|
||||
cy.intercept("GET", "/api/collection/tree*").as("getCollectionTree");
|
||||
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Click Library sidebar section to open modal");
|
||||
H.DataStudio.ModelingSidebar.root()
|
||||
.findByText("Library")
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.log("Create library via modal");
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Create your Library").should("be.visible");
|
||||
cy.button("Create my Library").click();
|
||||
});
|
||||
|
||||
cy.wait("@createLibrary");
|
||||
cy.wait("@getCollectionTree");
|
||||
|
||||
cy.log("Verify library collections appear in sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().within(() => {
|
||||
cy.findByText("Data").should("be.visible");
|
||||
cy.findByText("Metrics").should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("Verify empty state shows on library root");
|
||||
H.DataStudio.Modeling.collectionPage().within(() => {
|
||||
cy.findByText("No models or metrics yet").should("be.visible");
|
||||
cy.findByText(
|
||||
"Models and metrics in this collection will appear here.",
|
||||
).should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("Select Data collection and verify it's empty");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Verify no models yet message");
|
||||
H.DataStudio.Modeling.collectionPage().within(() => {
|
||||
cy.findByText("No models yet").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should be available in the data picker", () => {
|
||||
createLibraryWithItems();
|
||||
|
||||
H.startNewQuestion();
|
||||
H.miniPickerBrowseAll().click();
|
||||
|
||||
H.entityPickerModalItem(0, "Library").click();
|
||||
H.entityPickerModalItem(1, "Data").click();
|
||||
H.entityPickerModalItem(2, "Trusted Orders Model").click();
|
||||
|
||||
cy.log("Ensure that the we can build the path from a value");
|
||||
|
||||
cy.button(/Trusted Orders Model/).click();
|
||||
H.miniPickerHeader().click();
|
||||
H.miniPickerBrowseAll().click();
|
||||
|
||||
H.entityPickerModalItem(0, "Library").should(
|
||||
"have.attr",
|
||||
"data-active",
|
||||
"true",
|
||||
);
|
||||
H.entityPickerModalItem(1, "Data").should(
|
||||
"have.attr",
|
||||
"data-active",
|
||||
"true",
|
||||
);
|
||||
H.entityPickerModalItem(2, "Trusted Orders Model").should(
|
||||
"have.attr",
|
||||
"data-active",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should let you move models and metrics into the library, even when empty", () => {
|
||||
H.createLibrary();
|
||||
|
||||
H.visitModel(ORDERS_MODEL_ID);
|
||||
H.openQuestionActions("Duplicate");
|
||||
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
|
||||
|
||||
H.entityPickerModalTab("Collections").click();
|
||||
H.entityPickerModalItem(0, "Library").click();
|
||||
H.entityPickerModalItem(1, "Metrics").should(
|
||||
"have.attr",
|
||||
"data-disabled",
|
||||
"true",
|
||||
);
|
||||
H.entityPickerModalItem(1, "Data").click();
|
||||
H.entityPickerModal().button("Select this collection").click();
|
||||
H.modal().button("Duplicate").click();
|
||||
});
|
||||
|
||||
it("should show the library collection even if only 1 child collection has items", () => {
|
||||
createLibraryWithModel();
|
||||
|
||||
H.startNewQuestion();
|
||||
H.miniPickerBrowseAll().click();
|
||||
|
||||
H.entityPickerModalItem(0, "Library").click();
|
||||
H.entityPickerModalItem(1, "Data").click();
|
||||
H.entityPickerModalItem(2, "Trusted Orders Model").should("exist");
|
||||
});
|
||||
|
||||
it("should be available in the question picker modal", () => {
|
||||
createLibraryWithItems();
|
||||
|
||||
const library = () => H.entityPickerModalLevel(0).findByText("Library");
|
||||
|
||||
cy.visit(
|
||||
`/admin/permissions/data/group/${ALL_USERS_GROUP_ID}/database/${SAMPLE_DB_ID}/schema/PUBLIC/${SAMPLE_DATABASE.ACCOUNTS_ID}/segmented`,
|
||||
);
|
||||
|
||||
H.modal()
|
||||
.findByText(/Use a saved question/)
|
||||
.click();
|
||||
H.modal()
|
||||
.button(/Select a question/)
|
||||
.click();
|
||||
|
||||
H.entityPickerModal().within(() => {
|
||||
// Questions tab should not show the library, because the library can only contain models and metrics
|
||||
H.entityPickerModalTab("Questions").click();
|
||||
library().should("not.exist");
|
||||
H.entityPickerModalItem(0, "Our analytics").should("exist");
|
||||
|
||||
// Question picker should show library in models tab
|
||||
H.entityPickerModalTab("Models").click();
|
||||
H.entityPickerModalItem(0, "Our analytics").should("exist");
|
||||
library().click();
|
||||
H.entityPickerModalItem(1, "Metrics").should(
|
||||
"have.attr",
|
||||
"data-disabled",
|
||||
"true",
|
||||
);
|
||||
H.entityPickerModalItem(1, "Data").click();
|
||||
H.entityPickerModalItem(2, "Trusted Orders Model").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
414
e2e/test/scenarios/data-studio/modeling-metrics.cy.spec.ts
Normal file
414
e2e/test/scenarios/data-studio/modeling-metrics.cy.spec.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
const { H } = cy;
|
||||
import { createLibraryWithItems } from "e2e/support/test-library-data";
|
||||
|
||||
describe("scenarios > data studio > modeling > metrics", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
|
||||
cy.intercept("POST", "/api/card").as("createCard");
|
||||
cy.intercept("PUT", "/api/card/*").as("updateCard");
|
||||
cy.intercept("POST", "/api/collection").as("createCollection");
|
||||
cy.intercept("PUT", "/api/collection/*").as("updateCollection");
|
||||
|
||||
createLibraryWithItems();
|
||||
});
|
||||
|
||||
describe("empty state", () => {
|
||||
it("should show empty state when no collections are selected", () => {
|
||||
visitModelingPage();
|
||||
H.DataStudio.Modeling.emptyPage().should("be.visible");
|
||||
H.DataStudio.Modeling.emptyPage()
|
||||
.parent()
|
||||
.findByText("Build your semantic layer with models and metrics.")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a new metric with proper validation and save to collection", () => {
|
||||
visitModelingPage();
|
||||
|
||||
cy.log("Create a new metric");
|
||||
H.DataStudio.ModelingSidebar.createCardMenuButton().click();
|
||||
H.popover().findByText("Metric").click();
|
||||
|
||||
H.DataStudio.Metrics.queryEditor().should("be.visible");
|
||||
H.DataStudio.Metrics.saveButton().should("be.disabled");
|
||||
|
||||
H.miniPickerBrowseAll().click();
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Databases").click();
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
|
||||
H.getNotebookStep("summarize")
|
||||
.findByText("Pick a column to group by")
|
||||
.click();
|
||||
|
||||
H.popover().findByText("Created At").click();
|
||||
|
||||
H.DataStudio.Metrics.saveButton().should("be.enabled").click();
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Save your metric").should("be.visible");
|
||||
cy.findByLabelText("Name").type("Total Revenue");
|
||||
cy.findByLabelText("Description").type(
|
||||
"Sum of all order totals across the store",
|
||||
);
|
||||
cy.findByText("Where do you want to save this?").should("be.visible");
|
||||
cy.button("Save").click();
|
||||
});
|
||||
|
||||
cy.wait("@createCard");
|
||||
|
||||
cy.log("Verify metric overview page");
|
||||
cy.url().should("match", /\/data-studio\/modeling\/metrics\/\d+$/);
|
||||
|
||||
H.DataStudio.Metrics.overviewPage().within(() => {
|
||||
cy.findByText("Total Revenue").should("be.visible");
|
||||
cy.findByText("Sum of all order totals across the store").should(
|
||||
"be.visible",
|
||||
);
|
||||
cy.findByText("Sample Database").should("be.visible");
|
||||
cy.findByText("PUBLIC").should("be.visible");
|
||||
cy.findByText("Orders").should("be.visible");
|
||||
cy.findByText("Creator and last editor").should("be.visible");
|
||||
cy.findAllByText(/by Bobby Tables/).should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("Ensure chart is visible");
|
||||
H.echartsContainer().findByText("Count").should("be.visible");
|
||||
|
||||
cy.log("Verify metric definition page");
|
||||
H.DataStudio.Metrics.definitionTab().click();
|
||||
|
||||
cy.log("Verify notebook state");
|
||||
H.DataStudio.Metrics.queryEditor().should("be.visible");
|
||||
H.getNotebookStep("data").findByText("Orders").should("be.visible");
|
||||
H.getNotebookStep("summarize")
|
||||
.findByText("Created At: Month")
|
||||
.should("be.visible");
|
||||
|
||||
H.runButtonInOverlay().click();
|
||||
cy.log("Ensure chart is visible");
|
||||
H.echartsContainer().findByText("Count").should("be.visible");
|
||||
|
||||
cy.log("Verify metric dependencies page");
|
||||
H.DataStudio.Metrics.dependenciesTab().click();
|
||||
H.DependencyGraph.graph().within(() => {
|
||||
cy.findByText("Orders").should("be.visible");
|
||||
cy.findByText("Total Revenue").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should edit metric definition and save changes", function () {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Verify metric overview page displays correct data");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Update the metric name");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.clear()
|
||||
.type("Updated Orders Metric{enter}");
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify updated name appears in overview");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Updated Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify updated name appears in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.metricItem("Updated Orders Metric").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
|
||||
it("should cancel editing and revert changes", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Navigate to definition tab");
|
||||
H.DataStudio.Metrics.definitionTab().click();
|
||||
|
||||
cy.log("Change aggregation");
|
||||
H.getNotebookStep("summarize").findByText("Count").click();
|
||||
H.popover().findByText("Sum of ...").click();
|
||||
H.popover().findByText("Total").click();
|
||||
|
||||
cy.log("Verify save button is enabled, then cancel");
|
||||
H.DataStudio.Metrics.saveButton().should("be.enabled");
|
||||
H.DataStudio.Metrics.cancelButton().click();
|
||||
|
||||
cy.log("Verify changes were reverted");
|
||||
H.getNotebookStep("summarize").findByText("Count").should("be.visible");
|
||||
});
|
||||
|
||||
it("should show unsaved changes warning when navigating away", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Navigate to definition tab");
|
||||
H.DataStudio.Metrics.definitionTab().click();
|
||||
|
||||
cy.log("Change aggregation from Count to Sum");
|
||||
H.DataStudio.Metrics.queryEditor().should("be.visible");
|
||||
H.getNotebookStep("summarize").findByText("Count").click();
|
||||
H.popover().findByText("Sum of ...").click();
|
||||
H.popover().findByText("Total").click();
|
||||
|
||||
cy.log("Try to navigate away");
|
||||
H.DataStudio.ModelingSidebar.glossaryLink().click();
|
||||
|
||||
cy.log("Verify unsaved changes modal appears");
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Discard your changes?").should("be.visible");
|
||||
cy.button("Cancel").click();
|
||||
});
|
||||
|
||||
cy.log("Verify we're still on the definition tab");
|
||||
H.DataStudio.Metrics.queryEditor().should("be.visible");
|
||||
});
|
||||
|
||||
it("should archive and restore a metric", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Wait for collection page to load");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Verify metric is loaded before archiving");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Archive the metric");
|
||||
H.DataStudio.Metrics.moreMenu().click();
|
||||
H.popover().findByText("Move to trash").click();
|
||||
|
||||
cy.log("Confirm archiving in modal");
|
||||
H.modal().button("Move to trash").click();
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify redirected to modeling page");
|
||||
cy.url().should("include", "/data-studio/modeling");
|
||||
|
||||
cy.log("Verify metric is removed from collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.collectionPage()
|
||||
.findByText("No metrics yet")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Navigate to trash");
|
||||
cy.visit("/trash");
|
||||
|
||||
cy.log("Verify metric appears in trash");
|
||||
cy.findByRole("table")
|
||||
.findByText("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Restore the metric");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
cy.findByTestId("archive-banner").should("be.visible");
|
||||
cy.findByTestId("archive-banner").findByText("Restore").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify metric is restored in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table")
|
||||
.findByText("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("should view metric in question view via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Verify metric is loaded");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify View link opens in new tab");
|
||||
H.DataStudio.Metrics.moreMenu().click();
|
||||
H.popover()
|
||||
.findByText("View")
|
||||
.closest("a")
|
||||
.should("have.attr", "target", "_blank")
|
||||
.should("have.attr", "href")
|
||||
.and("match", /\/metric\/\d+/);
|
||||
});
|
||||
|
||||
it("should duplicate metric via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Verify metric is loaded");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Open more menu and click Duplicate");
|
||||
H.DataStudio.Metrics.moreMenu().click();
|
||||
H.popover().findByText("Duplicate").click();
|
||||
|
||||
cy.log("Save duplicate metric");
|
||||
H.modal()
|
||||
.findByText('Duplicate "Trusted Orders Metric"')
|
||||
.should("be.visible");
|
||||
H.modal()
|
||||
.findByLabelText("Name")
|
||||
.should("have.value", "Trusted Orders Metric - Duplicate");
|
||||
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
|
||||
|
||||
H.entityPickerModalTab("Collections").click();
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Our analytics").click();
|
||||
cy.findByText("Library").click();
|
||||
cy.findByText("Metrics").click();
|
||||
cy.button("Select this collection").click();
|
||||
});
|
||||
|
||||
H.modal().button("Duplicate").click();
|
||||
|
||||
cy.wait("@createCard");
|
||||
H.modal().should("not.exist");
|
||||
|
||||
cy.log("Verify duplicate metric is created");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByText("Trusted Orders Metric - Duplicate")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify both metrics appear in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").within(() => {
|
||||
cy.findByText("Trusted Orders Metric").should("be.visible");
|
||||
cy.findByText("Trusted Orders Metric - Duplicate").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should move metric to different collection via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Metrics collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
|
||||
cy.log("Click on the metric from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Metric").click();
|
||||
|
||||
cy.log("Verify metric is loaded");
|
||||
H.DataStudio.Metrics.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Metric")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Open more menu and click Move");
|
||||
H.DataStudio.Metrics.moreMenu().click();
|
||||
H.popover().findByText("Move").click();
|
||||
|
||||
cy.log("Select First collection as destination");
|
||||
H.entityPickerModal().findByText("First collection").click();
|
||||
H.entityPickerModal().button("Move").click();
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify metric is in First collection");
|
||||
cy.findByTestId("move-card-toast").findByText("First collection").click();
|
||||
|
||||
cy.log("Verify metric is no longer in Metrics collection");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree()
|
||||
.findByText("Metrics")
|
||||
.click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.collectionPage()
|
||||
.findByText("No metrics yet")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
function visitModelingPage() {
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.root().should("be.visible");
|
||||
}
|
||||
281
e2e/test/scenarios/data-studio/modeling-models.cy.spec.ts
Normal file
281
e2e/test/scenarios/data-studio/modeling-models.cy.spec.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
const { H } = cy;
|
||||
import { createLibraryWithItems } from "e2e/support/test-library-data";
|
||||
|
||||
describe("scenarios > data studio > modeling > models", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
|
||||
cy.intercept("POST", "/api/card").as("createCard");
|
||||
cy.intercept("PUT", "/api/card/*").as("updateCard");
|
||||
cy.intercept("POST", "/api/collection").as("createCollection");
|
||||
cy.intercept("PUT", "/api/collection/*").as("updateCollection");
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
createLibraryWithItems();
|
||||
});
|
||||
|
||||
it("should edit model name and description", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify model overview page displays correct data");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Update the model name");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.clear()
|
||||
.type("Updated Orders Model{enter}");
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify updated name appears in overview");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Updated Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify updated name appears in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.modelItem("Updated Orders Model").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
|
||||
it("should navigate between tabs", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify Overview tab displays model name");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Navigate to Definition tab");
|
||||
H.DataStudio.Models.definitionTab().click();
|
||||
H.DataStudio.Models.queryEditor().findByText("Orders").should("be.visible");
|
||||
|
||||
cy.log("Navigate to Fields tab");
|
||||
H.DataStudio.Models.fieldsTab().click();
|
||||
H.DataStudio.Models.fieldsPage().within(() => {
|
||||
cy.findByText("ID").should("be.visible");
|
||||
cy.findByText("User ID").should("be.visible");
|
||||
cy.findByText("Product ID").should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("Navigate to Dependencies tab");
|
||||
H.DataStudio.Models.dependenciesTab().click();
|
||||
H.DependencyGraph.graph().within(() => {
|
||||
cy.findByText("Orders").should("be.visible");
|
||||
cy.findByText("Trusted Orders Model").should("be.visible");
|
||||
});
|
||||
|
||||
cy.log("Navigate back to Overview tab");
|
||||
H.DataStudio.Models.overviewTab().click();
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("should archive and restore a model", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Wait for collection page to load");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify model is loaded before archiving");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Archive the model");
|
||||
H.DataStudio.Models.moreMenu().click();
|
||||
H.popover().findByText("Move to trash").click();
|
||||
|
||||
cy.log("Confirm archiving in modal");
|
||||
H.modal().button("Move to trash").click();
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify redirected to modeling page");
|
||||
cy.url().should("include", "/data-studio/modeling");
|
||||
|
||||
cy.log("Verify model is removed from collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.collectionPage()
|
||||
.findByText("No models yet")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Navigate to trash");
|
||||
cy.visit("/trash");
|
||||
|
||||
cy.log("Verify model appears in trash");
|
||||
cy.findByRole("table")
|
||||
.findByText("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Restore the model");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
cy.findByTestId("archive-banner").should("be.visible");
|
||||
cy.findByTestId("archive-banner").findByText("Restore").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify model is restored in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table")
|
||||
.findByText("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("should view model in question view via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify model is loaded");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify View link opens in new tab");
|
||||
H.DataStudio.Models.moreMenu().click();
|
||||
H.popover()
|
||||
.findByText("View")
|
||||
.closest("a")
|
||||
.should("have.attr", "target", "_blank")
|
||||
.should("have.attr", "href")
|
||||
.and("match", /\/model\/\d+/);
|
||||
});
|
||||
|
||||
it("should duplicate model via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify model is loaded");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Open more menu and click Duplicate");
|
||||
H.DataStudio.Models.moreMenu().click();
|
||||
H.popover().findByText("Duplicate").click();
|
||||
|
||||
cy.log("Save duplicate model");
|
||||
H.modal()
|
||||
.findByText('Duplicate "Trusted Orders Model"')
|
||||
.should("be.visible");
|
||||
H.modal()
|
||||
.findByLabelText("Name")
|
||||
.should("have.value", "Trusted Orders Model - Duplicate");
|
||||
H.modal().findByTestId("dashboard-and-collection-picker-button").click();
|
||||
|
||||
H.entityPickerModalTab("Collections").click();
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Our analytics").click();
|
||||
cy.findByText("Library").click();
|
||||
cy.findByText("Data").click();
|
||||
cy.button("Select this collection").click();
|
||||
});
|
||||
|
||||
H.modal().button("Duplicate").click();
|
||||
|
||||
cy.wait("@createCard");
|
||||
H.modal().should("not.exist");
|
||||
|
||||
cy.log("Verify duplicate model is created");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByText("Trusted Orders Model - Duplicate")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Verify both models appear in collection view");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").within(() => {
|
||||
cy.findByText("Trusted Orders Model").should("be.visible");
|
||||
cy.findByText("Trusted Orders Model - Duplicate").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should move model to different collection via more menu", () => {
|
||||
cy.log("Navigate to Data Studio Modeling");
|
||||
cy.visit("/data-studio/modeling");
|
||||
|
||||
cy.log("Select Models collection from sidebar");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
|
||||
cy.log("Click on the model from the collection view");
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
cy.findByRole("table").findByText("Trusted Orders Model").click();
|
||||
|
||||
cy.log("Verify model is loaded");
|
||||
H.DataStudio.Models.overviewPage()
|
||||
.findByDisplayValue("Trusted Orders Model")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Open more menu and click Move");
|
||||
H.DataStudio.Models.moreMenu().click();
|
||||
H.popover().findByText("Move").click();
|
||||
|
||||
cy.log("Select First collection as destination");
|
||||
H.entityPickerModal().findByText("First collection").click();
|
||||
H.entityPickerModal().button("Move").click();
|
||||
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.log("Verify model is in First collection");
|
||||
cy.findByTestId("move-card-toast").findByText("First collection").click();
|
||||
|
||||
cy.log("Verify model is no longer in Models collection");
|
||||
cy.visit("/data-studio/modeling");
|
||||
H.DataStudio.ModelingSidebar.collectionsTree().findByText("Data").click();
|
||||
H.DataStudio.Modeling.collectionPage().should("be.visible");
|
||||
H.DataStudio.Modeling.collectionPage()
|
||||
.findByText("No models yet")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
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");
|
||||
|
||||
@@ -1892,6 +1892,14 @@ describe("scenarios > embedding > full app - jwt sso integration", () => {
|
||||
H.updateSetting("jwt-shared-secret", jwtSecret);
|
||||
H.updateSetting("jwt-enabled", true);
|
||||
|
||||
cy.intercept("POST", `/api/card/${ORDERS_QUESTION_ID}/query`).as(
|
||||
"getCardQuery",
|
||||
);
|
||||
addLinkClickBehavior({
|
||||
dashboardId,
|
||||
linkTemplate: "/question/{{test_attribute}}",
|
||||
});
|
||||
|
||||
cy.signOut(); // we *need* to sign out, otherwise the SSO process won't kick in
|
||||
});
|
||||
|
||||
@@ -1937,6 +1945,43 @@ describe("scenarios > embedding > full app - jwt sso integration", () => {
|
||||
cy.url().should("equal", `${baseUrl}/dashboard/${dashboardId}`);
|
||||
H.main().findByText("Orders in a dashboard").should("be.visible");
|
||||
});
|
||||
|
||||
it("should pass JWT user attributes to click behavior custom destinations (metabase#65942)", () => {
|
||||
const jwtAttributeValue = ORDERS_QUESTION_ID;
|
||||
|
||||
// 1) set up a click behavior that uses a user attribute in the URL
|
||||
// Added in beforeeach so it uses the admin user
|
||||
|
||||
// 2) sign a jwt for the user with a custom attribute
|
||||
cy.task("signJwt", {
|
||||
payload: {
|
||||
email: USERS.normal.email,
|
||||
exp: Math.round(Date.now() / 1000) + 10 * 60,
|
||||
test_attribute: jwtAttributeValue,
|
||||
},
|
||||
secret: jwtSecret,
|
||||
}).then((jwtToken) => {
|
||||
// 3) mock the JWT provider to redirect to the auth/sso endpoint with the JWT
|
||||
cy.intercept(/http:\/\/localhost:8888\/.*/, (req) => {
|
||||
const redirectUrl = `${baseUrl}/auth/sso?jwt=${jwtToken}&return_to=/dashboard/${dashboardId}`;
|
||||
req.redirect(redirectUrl);
|
||||
}).as("jwt-provider");
|
||||
});
|
||||
|
||||
// 4) visit the dashboard as embedded
|
||||
H.visitFullAppEmbeddingUrl({ url: `/dashboard/${dashboardId}` });
|
||||
|
||||
cy.wait("@jwt-provider");
|
||||
|
||||
// 5) verify user is on dashboard
|
||||
cy.url().should("equal", `${baseUrl}/dashboard/${dashboardId}`);
|
||||
|
||||
cy.findAllByRole("gridcell").first().click();
|
||||
cy.wait("@getCardQuery");
|
||||
|
||||
cy.findByTestId("question-filter-header").realHover();
|
||||
cy.findByTestId("main-logo").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
const visitQuestionUrl = (urlOptions) => {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user