Compare commits
133 Commits
adm-759-de
...
watcher-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38883ff7cd | ||
|
|
5cbba5d817 | ||
|
|
ff4a57e933 | ||
|
|
93515ebe31 | ||
|
|
d53a7a680b | ||
|
|
8edafd083c | ||
|
|
557e7d0f34 | ||
|
|
8b2a8b4d42 | ||
|
|
0e53e2bd21 | ||
|
|
8fa039f6dc | ||
|
|
1c5bc70adb | ||
|
|
07235bbde0 | ||
|
|
7c0ca3369b | ||
|
|
be86f32a0f | ||
|
|
d9147bd92c | ||
|
|
d55b0e247a | ||
|
|
61b7eeda25 | ||
|
|
3a8479078c | ||
|
|
a743c27bcd | ||
|
|
139d1605fa | ||
|
|
a6f9bb8167 | ||
|
|
b405045f94 | ||
|
|
a669530a55 | ||
|
|
a6ef327bdd | ||
|
|
dafbd09482 | ||
|
|
87ca8ab2d3 | ||
|
|
8827164777 | ||
|
|
8285908e3a | ||
|
|
58f4e0a9dd | ||
|
|
431184652f | ||
|
|
3cfcb03cf5 | ||
|
|
76a5231a6f | ||
|
|
ab296e8d6c | ||
|
|
fcb6e8415b | ||
|
|
38762f1b74 | ||
|
|
d415393100 | ||
|
|
7db950f952 | ||
|
|
c9caad50a4 | ||
|
|
be81440458 | ||
|
|
fc0a1777ec | ||
|
|
4c6c580964 | ||
|
|
d3fafe9cf2 | ||
|
|
8fc63096b8 | ||
|
|
f53785e010 | ||
|
|
ee5d61525c | ||
|
|
2f4a3d35c2 | ||
|
|
c1763bd9d7 | ||
|
|
22e028af3a | ||
|
|
e58ecd7290 | ||
|
|
a69c66d0e1 | ||
|
|
29efdb4287 | ||
|
|
655907c7bb | ||
|
|
8c57a1b48a | ||
|
|
770207290a | ||
|
|
802890a8a3 | ||
|
|
3709509a67 | ||
|
|
0b91af3d7c | ||
|
|
c0d38f98a7 | ||
|
|
6a34d8b120 | ||
|
|
319b45efb0 | ||
|
|
ce33f7c3b9 | ||
|
|
251fb3d05c | ||
|
|
a59d8af558 | ||
|
|
e8ff64fc77 | ||
|
|
0beb4fd1ec | ||
|
|
f508020b18 | ||
|
|
61b06cb9ca | ||
|
|
c21a754c1a | ||
|
|
cebcccd81a | ||
|
|
479f6ec15e | ||
|
|
e42004d8bd | ||
|
|
3b9ba47ce5 | ||
|
|
6376201ee2 | ||
|
|
7f92780e9e | ||
|
|
9e13274e72 | ||
|
|
92fac6cabb | ||
|
|
aca9b98060 | ||
|
|
d25b43a3cb | ||
|
|
fd2f465639 | ||
|
|
b7f5e23707 | ||
|
|
9e7f0b903a | ||
|
|
232ce151c3 | ||
|
|
5d6b82d6ee | ||
|
|
1b0ceb2c48 | ||
|
|
c42252c802 | ||
|
|
dd9c170d8d | ||
|
|
c88a5b5bc5 | ||
|
|
aa0bd12ed3 | ||
|
|
a0bee28c26 | ||
|
|
ea279ca19b | ||
|
|
a8656deb25 | ||
|
|
8ebc12b933 | ||
|
|
edb69d6dbb | ||
|
|
8aac6aac61 | ||
|
|
691d1430f8 | ||
|
|
2adc027178 | ||
|
|
e3e035ac6e | ||
|
|
c7ee8b29d2 | ||
|
|
a23481188c | ||
|
|
0a4625fdbe | ||
|
|
ac81ee5c25 | ||
|
|
99613cfe0d | ||
|
|
e515a2d6d0 | ||
|
|
0c0aaf5490 | ||
|
|
0bc9646ca0 | ||
|
|
40507a0e21 | ||
|
|
562617890f | ||
|
|
9e4fb1181c | ||
|
|
6422238118 | ||
|
|
c4cb9263f6 | ||
|
|
003777b77d | ||
|
|
76133ade49 | ||
|
|
f29902368b | ||
|
|
2c106817a4 | ||
|
|
da5cd4e7cf | ||
|
|
04bf793795 | ||
|
|
fa9ea2a5a9 | ||
|
|
a04e9bd9b0 | ||
|
|
85b3ab5d5f | ||
|
|
41e3110f20 | ||
|
|
213535773b | ||
|
|
c1e301eda6 | ||
|
|
d82bf686b6 | ||
|
|
fe3792cda9 | ||
|
|
8278732860 | ||
|
|
0fb515cbc6 | ||
|
|
2d038e518e | ||
|
|
fc56c810f9 | ||
|
|
fd18b7708f | ||
|
|
1c113a608e | ||
|
|
5fabb3e1ec | ||
|
|
6da77fce6f | ||
|
|
f947b70c35 |
@@ -91,7 +91,6 @@
|
||||
metabase.test.data.users/with-group-for-user
|
||||
metabase.test.data/with-empty-h2-app-db
|
||||
metabase.test.util.log/with-log-level
|
||||
metabase.test.util.misc/with-single-admin-user
|
||||
metabase.test.util/with-all-users-permission
|
||||
metabase.test.util/with-discarded-collections-perms-changes
|
||||
metabase.test.util/with-env-keys-renamed-by
|
||||
@@ -123,7 +122,6 @@
|
||||
metabase.test/with-model-cleanup
|
||||
metabase.test/with-non-admin-groups-no-root-collection-for-namespace-perms
|
||||
metabase.test/with-non-admin-groups-no-root-collection-perms
|
||||
metabase.test/with-single-admin-user
|
||||
metabase.test/with-temp-env-var-value
|
||||
metabase.test/with-temp-vals-in-db
|
||||
metabase.test/with-temporary-raw-setting-values
|
||||
@@ -579,13 +577,13 @@
|
||||
metabase.test/with-actions clojure.core/let
|
||||
metabase.test/with-grouper-batches! clojure.core/fn
|
||||
metabase.test/with-open-channels clojure.core/let
|
||||
metabase.test/with-single-admin-user clojure.core/fn
|
||||
metabase.test/with-single-admin-user! clojure.core/fn
|
||||
metabase.test/with-temp-dir clojure.core/let
|
||||
metabase.test/with-temp-empty-app-db clojure.core/let
|
||||
metabase.test/with-temp-file clojure.core/let
|
||||
metabase.test/with-user-in-groups clojure.core/let
|
||||
metabase.test.gentest/defgentest clojure.test/deftest
|
||||
metabase.upload-test/with-upload-table! clojure.core/let
|
||||
metabase.upload.impl-test/with-upload-table! clojure.core/let
|
||||
metabase.util/with-timer-ms clojure.core/fn
|
||||
metabase.util.files/with-open-path-to-resource clojure.core/let
|
||||
metabase.util.malli.defn/defn schema.core/defn
|
||||
@@ -636,6 +634,7 @@
|
||||
metabase.api.dashboard-test/with-simple-dashboard-with-tabs hooks.common/with-one-binding
|
||||
metabase.api.macros/defendpoint hooks.metabase.api.macros/defendpoint
|
||||
metabase.api.user-test/with-temp-user-email! hooks.common/with-one-binding
|
||||
metabase.channel.render.js.svg/with-static-viz-context hooks.common/with-one-top-level-binding
|
||||
metabase.channel.template.handlebars-helper/defhelper hooks.metabase.channel.template.handlebars-helper/defhelper
|
||||
metabase.channel.template.handlebars-test/with-temp-template! hooks.common/with-vec-first-binding
|
||||
metabase.dashboard-subscription-test/with-link-card-fixture-for-dashboard hooks.common/let-second
|
||||
@@ -682,7 +681,7 @@
|
||||
metabase.request.core/with-current-request hooks.common/with-used-first-arg
|
||||
metabase.request.core/with-current-user hooks.common/with-used-first-arg
|
||||
metabase.request.core/with-limit-and-offset hooks.common/with-used-first-two-args
|
||||
metabase.sample-data-test/with-temp-sample-database-db hooks.common/with-one-binding
|
||||
metabase.sample-data.impl-test/with-temp-sample-database-db hooks.common/with-one-binding
|
||||
metabase.search.api-test/with-search-items-in-collection hooks.metabase.search.api-test/with-search-items-in-collection
|
||||
metabase.search.api-test/with-search-items-in-root-collection hooks.common/do*
|
||||
metabase.server.streaming-response-test/with-start-execution-chan hooks.common/with-one-binding
|
||||
@@ -772,7 +771,7 @@
|
||||
metabase.lib.filter/deffilter macros.metabase.lib.filter/deffilter
|
||||
metabase.models.params.chain-filter-test/chain-filter macros.metabase.models.params.chain-filter-test/chain-filter
|
||||
metabase.models.params.chain-filter-test/chain-filter-search macros.metabase.models.params.chain-filter-test/chain-filter
|
||||
metabase.models.user-test/with-groups macros.metabase.models.user-test/with-groups
|
||||
metabase.models.user-test/with-groups! macros.metabase.models.user-test/with-groups!
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-card-referencing!
|
||||
metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing! macros.metabase.public-sharing.api-test/with-sharing-enabled-and-temp-dashcard-referencing!
|
||||
metabase.query-analysis.models.query-analysis-test/with-test-setup macros.metabase.query-analysis.models.query-analysis-test/with-test-setup
|
||||
|
||||
@@ -42,19 +42,22 @@
|
||||
:uses #{api config db events models util}}
|
||||
|
||||
analytics
|
||||
{:api #{metabase.analytics.core
|
||||
{:api #{metabase.analytics.api
|
||||
metabase.analytics.core
|
||||
metabase.analytics.init
|
||||
metabase.analytics.prometheus
|
||||
metabase.analytics.snowplow}
|
||||
:uses #{api
|
||||
channel
|
||||
config
|
||||
db
|
||||
driver
|
||||
eid-translation
|
||||
integrations
|
||||
internal-stats
|
||||
models
|
||||
premium-features
|
||||
settings
|
||||
task
|
||||
util}}
|
||||
analyze
|
||||
{:api #{metabase.analyze.core metabase.analyze.query-results}
|
||||
@@ -63,21 +66,69 @@
|
||||
api
|
||||
{:api #{metabase.api.auth
|
||||
metabase.api.card
|
||||
metabase.api.cards
|
||||
metabase.api.collection
|
||||
metabase.api.common
|
||||
metabase.api.common.validation
|
||||
metabase.api.dashboard
|
||||
metabase.api.database
|
||||
metabase.api.dataset
|
||||
metabase.api.docs
|
||||
metabase.api.field
|
||||
metabase.api.geojson
|
||||
metabase.api.logger
|
||||
metabase.api.macros
|
||||
metabase.api.open-api
|
||||
metabase.api.query-metadata
|
||||
metabase.api.routes
|
||||
metabase.api.routes.common
|
||||
metabase.api.table
|
||||
metabase.api.user
|
||||
metabase.api.util
|
||||
metabase.api.util.handlers}
|
||||
:uses #{actions
|
||||
analytics
|
||||
analyze
|
||||
api-routes
|
||||
channel
|
||||
config
|
||||
database-routing
|
||||
db
|
||||
driver
|
||||
eid-translation
|
||||
embedding
|
||||
events
|
||||
legacy-mbql
|
||||
lib
|
||||
logger
|
||||
model-persistence
|
||||
models
|
||||
permissions
|
||||
plugins
|
||||
premium-features
|
||||
pulse
|
||||
query-processor
|
||||
request
|
||||
revisions
|
||||
sample-data
|
||||
search
|
||||
server
|
||||
session
|
||||
settings
|
||||
sso
|
||||
sync
|
||||
types
|
||||
upload
|
||||
util
|
||||
xrays
|
||||
enterprise/advanced-permissions}}
|
||||
|
||||
api-keys
|
||||
{:api #{metabase.api-keys.api
|
||||
metabase.api-keys.core}
|
||||
:uses #{api db events models permissions util}}
|
||||
|
||||
api-routes
|
||||
{:api #{metabase.api-routes.core}
|
||||
:uses :any}
|
||||
|
||||
audit
|
||||
@@ -85,7 +136,7 @@
|
||||
:uses #{db settings}}
|
||||
|
||||
auth-provider
|
||||
{:api #{metabase.auth-provider}
|
||||
{:api #{metabase.auth-provider.core}
|
||||
:uses #{premium-features}}
|
||||
|
||||
bookmarks
|
||||
@@ -98,6 +149,7 @@
|
||||
metabase.cache.init}
|
||||
:uses #{api config db events models premium-features settings util}}
|
||||
|
||||
;; TODO -- way too many API namespaces.
|
||||
channel
|
||||
{:api #{metabase.channel.api
|
||||
metabase.channel.core
|
||||
@@ -106,6 +158,8 @@
|
||||
metabase.channel.init
|
||||
metabase.channel.models.channel
|
||||
metabase.channel.render.core
|
||||
metabase.channel.settings
|
||||
metabase.channel.slack
|
||||
metabase.channel.template.core}
|
||||
:uses #{analytics
|
||||
api
|
||||
@@ -114,7 +168,6 @@
|
||||
driver
|
||||
events
|
||||
formatter
|
||||
integrations
|
||||
lib
|
||||
models
|
||||
notification
|
||||
@@ -122,6 +175,7 @@
|
||||
premium-features
|
||||
settings
|
||||
query-processor
|
||||
task
|
||||
timeline
|
||||
types
|
||||
util}}
|
||||
@@ -136,7 +190,7 @@
|
||||
{:api #{metabase.cmd
|
||||
metabase.cmd.copy
|
||||
metabase.cmd.dump-to-h2}
|
||||
:uses #{api config db legacy-mbql models plugins query-processor search settings util}}
|
||||
:uses #{api api-routes config db legacy-mbql models plugins query-processor search settings util}}
|
||||
|
||||
;; technically this 'uses' `enterprise/core` and `test` since it tries to load them to see if they exists so we know
|
||||
;; if EE/test code is available; however we can ignore them since they're not real usages.
|
||||
@@ -231,8 +285,9 @@
|
||||
util}}
|
||||
|
||||
events
|
||||
{:api #{metabase.events metabase.events.init metabase.events.notification}
|
||||
:uses #{api channel driver models notification premium-features task-history util}}
|
||||
{:api #{metabase.events.core
|
||||
metabase.events.init}
|
||||
:uses #{channel driver models util view-log}}
|
||||
|
||||
formatter
|
||||
{:api #{metabase.formatter}
|
||||
@@ -242,10 +297,6 @@
|
||||
{:api #{metabase.indexed-entities.api metabase.indexed-entities.init}
|
||||
:uses #{analytics api driver legacy-mbql lib models query-processor search sync task util}}
|
||||
|
||||
integrations
|
||||
{:api #{metabase.integrations.slack}
|
||||
:uses #{events settings util}}
|
||||
|
||||
internal-stats
|
||||
{:api #{metabase.internal-stats.core}
|
||||
:uses #{db models}}
|
||||
@@ -318,8 +369,7 @@
|
||||
:uses #{api db driver events lib models premium-features settings query-processor request task task-history util}}
|
||||
|
||||
models
|
||||
{:api #{metabase.models.api-key
|
||||
metabase.models.application-permissions-revision
|
||||
{:api #{metabase.models.application-permissions-revision
|
||||
metabase.models.audit-log
|
||||
metabase.models.card
|
||||
metabase.models.card.metadata
|
||||
@@ -334,14 +384,11 @@
|
||||
metabase.models.database
|
||||
metabase.models.dispatch
|
||||
metabase.models.field
|
||||
metabase.models.field-usage
|
||||
metabase.models.field-values
|
||||
metabase.models.humanization
|
||||
metabase.models.init
|
||||
metabase.models.interface
|
||||
metabase.models.moderation-review
|
||||
metabase.models.native-query-snippet
|
||||
metabase.models.native-query-snippet.permissions
|
||||
metabase.models.params
|
||||
metabase.models.params.chain-filter
|
||||
metabase.models.params.custom-values
|
||||
@@ -356,11 +403,11 @@
|
||||
metabase.models.user
|
||||
metabase.models.user-parameter-value
|
||||
metabase.models.util.spec-update
|
||||
metabase.models.view-log-impl
|
||||
metabase.models.visualization-settings}
|
||||
:uses #{analytics
|
||||
analyze
|
||||
api
|
||||
api-keys
|
||||
audit
|
||||
cache
|
||||
channel
|
||||
@@ -391,14 +438,17 @@
|
||||
{:api #{metabase.moderation}
|
||||
:uses #{models util}}
|
||||
|
||||
native-query-snippets
|
||||
{:api #{metabase.native-query-snippets.api
|
||||
metabase.native-query-snippets.core}
|
||||
:uses #{api models permissions premium-features util}}
|
||||
|
||||
notification
|
||||
{:api #{metabase.notification.api
|
||||
metabase.notification.core
|
||||
metabase.notification.init
|
||||
metabase.notification.models
|
||||
metabase.notification.api.notification
|
||||
metabase.notification.payload.core
|
||||
metabase.notification.send}
|
||||
metabase.notification.payload.core}
|
||||
:uses #{analytics
|
||||
api
|
||||
channel
|
||||
@@ -450,9 +500,9 @@
|
||||
metabase.pulse.core
|
||||
metabase.pulse.init
|
||||
metabase.pulse.task.send-pulses}
|
||||
:uses #{api channel config db driver events integrations models notification permissions plugins premium-features
|
||||
:uses #{api channel config db driver events models notification permissions plugins premium-features
|
||||
query-processor request task task-history util
|
||||
enterprise/advanced-config enterprise/advanced-permissions enterprise/sandbox}}
|
||||
enterprise/advanced-permissions enterprise/sandbox}}
|
||||
|
||||
query-analysis
|
||||
{:api #{metabase.query-analysis.core metabase.query-analysis.init}
|
||||
@@ -470,7 +520,6 @@
|
||||
metabase.query-processor.middleware.annotate
|
||||
metabase.query-processor.middleware.cache-backend.db
|
||||
metabase.query-processor.middleware.constraints
|
||||
metabase.query-processor.middleware.fetch-source-query
|
||||
metabase.query-processor.middleware.fetch-source-query-legacy
|
||||
metabase.query-processor.middleware.limit
|
||||
metabase.query-processor.middleware.permissions
|
||||
@@ -524,8 +573,9 @@
|
||||
:uses #{api config events models util}}
|
||||
|
||||
sample-data
|
||||
{:api #{metabase.sample-data}
|
||||
:uses #{plugins sync util}}
|
||||
{:api #{metabase.sample-data.core
|
||||
metabase.sample-data.init}
|
||||
:uses #{plugins settings sync util}}
|
||||
|
||||
search
|
||||
{:api #{metabase.search.api
|
||||
@@ -536,7 +586,7 @@
|
||||
metabase.search.ingestion
|
||||
metabase.search.init
|
||||
metabase.search.spec}
|
||||
:uses #{analytics api audit config db driver models permissions premium-features settings request task util}}
|
||||
:uses #{analytics api audit config db driver models permissions premium-features settings request startup task util}}
|
||||
|
||||
segments
|
||||
{:api #{metabase.segments.api}
|
||||
@@ -547,7 +597,7 @@
|
||||
metabase.server.middleware.json
|
||||
metabase.server.middleware.session
|
||||
metabase.server.streaming-response}
|
||||
:uses #{analytics api config core db driver embedding models premium-features settings session request util enterprise/sso}}
|
||||
:uses #{analytics api api-keys api-routes config core db driver embedding models premium-features settings session request util enterprise/sso}}
|
||||
|
||||
session
|
||||
{:api #{metabase.session.core
|
||||
@@ -584,6 +634,10 @@
|
||||
metabase.sso.init}
|
||||
:uses #{api models permissions premium-features settings util}}
|
||||
|
||||
startup
|
||||
{:api #{metabase.startup.core}
|
||||
:uses #{util}}
|
||||
|
||||
sync
|
||||
{:api #{metabase.sync.api
|
||||
metabase.sync.core
|
||||
@@ -602,7 +656,6 @@
|
||||
config
|
||||
db
|
||||
driver
|
||||
integrations
|
||||
plugins
|
||||
premium-features
|
||||
settings
|
||||
@@ -622,6 +675,12 @@
|
||||
task
|
||||
util}}
|
||||
|
||||
testing-api
|
||||
{:api #{metabase.testing-api.api
|
||||
metabase.testing-api.core
|
||||
metabase.testing-api.init}
|
||||
:uses #{analytics api config db premium-features search util}}
|
||||
|
||||
tiles
|
||||
{:api #{metabase.tiles.api
|
||||
metabase.tiles.init}
|
||||
@@ -636,7 +695,9 @@
|
||||
:uses #{util}}
|
||||
|
||||
upload
|
||||
{:api #{metabase.upload}
|
||||
{:api #{metabase.upload.api
|
||||
metabase.upload.core
|
||||
metabase.upload.init}
|
||||
:uses #{analytics api driver events legacy-mbql lib model-persistence models permissions settings sync util}}
|
||||
|
||||
user-key-value
|
||||
@@ -647,6 +708,11 @@
|
||||
{:api :any
|
||||
:uses #{config db driver legacy-mbql lib models plugins settings}}
|
||||
|
||||
view-log
|
||||
{:api #{metabase.view-log.core
|
||||
metabase.view-log.init}
|
||||
:uses #{analytics api events models premium-features util}}
|
||||
|
||||
xrays
|
||||
{:api #{metabase.xrays.api
|
||||
metabase.xrays.core
|
||||
@@ -655,9 +721,10 @@
|
||||
|
||||
enterprise/advanced-config
|
||||
{:api #{metabase-enterprise.advanced-config.api.logs
|
||||
metabase-enterprise.advanced-config.models.pulse-channel
|
||||
metabase-enterprise.advanced-config.models.notification
|
||||
metabase-enterprise.advanced-config.init
|
||||
metabase-enterprise.advanced-config.file}
|
||||
:uses #{api db driver models permissions premium-features setup settings sync util}}
|
||||
:uses #{api api-keys db driver models permissions premium-features setup settings sync util}}
|
||||
|
||||
enterprise/advanced-permissions
|
||||
{:api #{metabase-enterprise.advanced-permissions.api.routes
|
||||
@@ -665,9 +732,21 @@
|
||||
metabase-enterprise.advanced-permissions.models.permissions.group-manager}
|
||||
:uses #{api enterprise/impersonation lib models permissions premium-features query-processor util}}
|
||||
|
||||
enterprise/airgap
|
||||
enterprise/ai-entity-analysis
|
||||
{:api #{metabase-enterprise.ai-entity-analysis.api}
|
||||
:uses #{api enterprise/metabot-v3 premium-features util}}
|
||||
|
||||
enterprise/ai-sql-fixer
|
||||
{:api #{metabase-enterprise.ai-sql-fixer.api}
|
||||
:uses #{api driver enterprise/metabot-v3 query-analysis query-processor util}}
|
||||
|
||||
enterprise/ai-sql-generation
|
||||
{:api #{metabase-enterprise.ai-sql-generation.api}
|
||||
:uses #{api driver enterprise/metabot-v3 models util}}
|
||||
|
||||
enterprise/analytics
|
||||
{:api #{}
|
||||
:uses #{premium-features util}}
|
||||
:uses #{driver premium-features enterprise/advanced-config enterprise/scim enterprise/sso}}
|
||||
|
||||
enterprise/api
|
||||
{:api #{metabase-enterprise.api.routes
|
||||
@@ -747,6 +826,15 @@
|
||||
{:api #{metabase-enterprise.llm.api}
|
||||
:uses #{analytics analyze api query-processor settings util}}
|
||||
|
||||
enterprise/metabot-v3
|
||||
{:api #{metabase-enterprise.metabot-v3.api metabase-enterprise.metabot-v3.core
|
||||
metabase-enterprise.metabot-v3.tools.api}
|
||||
:uses #{api config db legacy-mbql lib models premium-features settings pulse query-processor request util}}
|
||||
|
||||
enterprise/premium-features
|
||||
{:api #{}
|
||||
:uses #{premium-features util}}
|
||||
|
||||
enterprise/query-reference-validation
|
||||
{:api #{metabase-enterprise.query-reference-validation.api}
|
||||
:uses #{api models query-analysis request util}}
|
||||
@@ -757,6 +845,7 @@
|
||||
:uses #{api
|
||||
audit
|
||||
db
|
||||
driver
|
||||
legacy-mbql
|
||||
lib
|
||||
models
|
||||
@@ -770,7 +859,7 @@
|
||||
|
||||
enterprise/scim
|
||||
{:api #{metabase-enterprise.scim.api metabase-enterprise.scim.routes}
|
||||
:uses #{analytics api models permissions settings server util enterprise/serialization}}
|
||||
:uses #{analytics api api-keys models permissions settings server util enterprise/serialization}}
|
||||
|
||||
enterprise/search
|
||||
{:api #{}
|
||||
@@ -784,22 +873,19 @@
|
||||
|
||||
enterprise/snippet-collections
|
||||
{:api #{}
|
||||
:uses #{models permissions premium-features util}}
|
||||
:uses #{models native-query-snippets permissions premium-features util}}
|
||||
|
||||
enterprise/sso
|
||||
{:api #{metabase-enterprise.sso.api.routes
|
||||
metabase-enterprise.sso.integrations.sso-settings
|
||||
metabase-enterprise.sso.integrations.sso-utils}
|
||||
:uses #{api channel embedding events premium-features request session settings sso util enterprise/scim}}
|
||||
:uses #{api channel embedding events notification premium-features request session settings sso util enterprise/scim}}
|
||||
|
||||
enterprise/stale
|
||||
{:api #{metabase-enterprise.stale.routes}
|
||||
{:api #{metabase-enterprise.stale.api
|
||||
metabase-enterprise.stale.init}
|
||||
:uses #{analytics api embedding models premium-features request settings util}}
|
||||
|
||||
enterprise/stats
|
||||
{:api #{}
|
||||
:uses #{driver premium-features enterprise/advanced-config enterprise/scim enterprise/sso}}
|
||||
|
||||
enterprise/task
|
||||
{:api #{metabase-enterprise.task.truncate-audit-tables}
|
||||
:uses #{premium-features}}
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
metabase.query-processor.streaming.interface/begin!
|
||||
metabase.query-processor.streaming.interface/finish!
|
||||
metabase.query-processor.streaming.interface/write-row!
|
||||
metabase.sample-data/try-to-extract-sample-database!
|
||||
metabase.sample-data.impl/try-to-extract-sample-database!
|
||||
metabase.settings.models.setting-test/test-user-local-allowed-setting!
|
||||
metabase.settings.models.setting-test/test-user-local-only-setting!
|
||||
metabase.settings.models.setting.cache/restore-cache!
|
||||
@@ -126,7 +126,7 @@
|
||||
metabase.test/initialize-if-needed!
|
||||
metabase.test/test-helpers-set-global-values!
|
||||
metabase.test/with-temp-env-var-value!
|
||||
metabase.upload-test/set-local-infile!
|
||||
metabase.upload.impl-test/set-local-infile!
|
||||
metabase.util.files/create-dir-if-not-exists!
|
||||
metabase.util.ssh-test/start-mock-servers!
|
||||
metabase.util.ssh-test/stop-mock-servers!}}}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns macros.metabase.models.user-test)
|
||||
|
||||
(defmacro with-groups [bindings & body]
|
||||
(defmacro with-groups! [bindings & body]
|
||||
`(let ~(into []
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
(comp (partition-all 3)
|
||||
(mapcat (fn [[group-binding properties members]]
|
||||
[group-binding [properties members]])))
|
||||
bindings)
|
||||
~@body))
|
||||
|
||||
34
.clj-kondo/test/hooks/metabase/models/setting_test.clj
Normal file
@@ -0,0 +1,34 @@
|
||||
(ns hooks.metabase.models.setting-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clojure.test :refer :all]
|
||||
[hooks.metabase.settings.models.setting]))
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/validate-deftest]}
|
||||
(deftest ^:parallel defsetting-test
|
||||
(let [node (-> '(defsetting active-users-count
|
||||
(deferred-tru "Cached number of active users. Refreshed every 5 minutes.")
|
||||
:visibility :admin
|
||||
:type :integer
|
||||
:default 0
|
||||
:getter (fn []
|
||||
(if-not ((requiring-resolve 'metabase.db/db-is-set-up?))
|
||||
0
|
||||
(cached-active-users-count))))
|
||||
pr-str
|
||||
hooks/parse-string)]
|
||||
(is (= '(let [_ (deferred-tru "Cached number of active users. Refreshed every 5 minutes.")
|
||||
_ {:default 0
|
||||
:type :integer
|
||||
:getter (fn []
|
||||
(if-not ((requiring-resolve 'metabase.db/db-is-set-up?))
|
||||
0
|
||||
(cached-active-users-count)))
|
||||
:visibility :admin}]
|
||||
:active-users-count
|
||||
(defn active-users-count "Docstring." [])
|
||||
(defn active-users-count! "Docstring." [_value-or-nil]))
|
||||
(-> {:node node}
|
||||
hooks.metabase.settings.models.setting/defsetting
|
||||
:node
|
||||
hooks/sexpr)))))
|
||||
@@ -190,7 +190,7 @@
|
||||
metabase.request.core/with-limit-and-offset [[:inner 0]]
|
||||
metabase.request.session/as-admin [[:block 0]]
|
||||
metabase.request.session/with-current-user [[:inner 0]]
|
||||
metabase.sample-data-test/with-temp-sample-database-db [[:block 1]]
|
||||
metabase.sample-data.impl-test/with-temp-sample-database-db [[:block 1]]
|
||||
metabase.search.api-test/do-test-users [[:block 1]]
|
||||
metabase.server.middleware.session/as-admin [[:block 0]]
|
||||
metabase.server.middleware.session/with-current-user [[:inner 0]]
|
||||
@@ -274,7 +274,7 @@
|
||||
metabase.test/with-user-in-groups [[:inner 0]]
|
||||
metabase.timeseries-query-processor-test.util/test-timeseries-drivers [[:block 0]]
|
||||
metabase.timeseries-query-processor-test/run-mbql-query [[:inner 0]]
|
||||
metabase.upload-test/with-upload-table! [[:inner 0]]
|
||||
metabase.upload.impl-test/with-upload-table! [[:inner 0]]
|
||||
metabase.upload.types/with-currency [[:default]]
|
||||
metabase.upload.types/with-parens [[:default]]
|
||||
metabase.util.async/cancelable-thread [[:block 0]]
|
||||
|
||||
15
.eslintrc.js
@@ -6,7 +6,7 @@
|
||||
// can use this flag to enable it. This is set to true in CI
|
||||
const shouldLintCssModules =
|
||||
process.env.LINT_CSS_MODULES === "true" || process.env.CI;
|
||||
const plugins = ["react", "no-only-tests","ttag"];
|
||||
const plugins = ["react", "no-only-tests", "ttag"];
|
||||
if (shouldLintCssModules) {
|
||||
plugins.push("postcss-modules");
|
||||
}
|
||||
@@ -118,7 +118,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
complexity: ["error", { max: 54 }],
|
||||
complexity: ["error", { max: 55 }],
|
||||
...(shouldLintCssModules
|
||||
? {
|
||||
"postcss-modules/no-undef-class": "error",
|
||||
@@ -195,12 +195,15 @@ module.exports = {
|
||||
"*.unit.spec.*",
|
||||
"frontend/src/metabase/admin/**/*",
|
||||
"frontend/src/metabase/setup/**/*",
|
||||
"enterprise/frontend/src/metabase-enterprise/whitelabel/**/*",
|
||||
"frontend/lint/**/*",
|
||||
"*.stories.*",
|
||||
"stories-data.*",
|
||||
"e2e/**/*",
|
||||
"**/tests/*",
|
||||
"release/**/*",
|
||||
"webpack.config.js",
|
||||
"rspack.config.js",
|
||||
],
|
||||
rules: {
|
||||
"no-color-literals": "off",
|
||||
@@ -258,9 +261,9 @@ module.exports = {
|
||||
"jest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": ["expect*", "assert*"],
|
||||
"additionalTestBlockFunctions": []
|
||||
}
|
||||
assertFunctionNames: ["expect*", "assert*"],
|
||||
additionalTestBlockFunctions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -291,7 +294,7 @@ module.exports = {
|
||||
selector: "Literal[value=/mb-base-color-/]",
|
||||
message:
|
||||
"You may not use base colors in the application, use semantic colors instead. (see colors.module.css)",
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
3
.github/workflows/containerize-jar.yml
vendored
@@ -16,6 +16,8 @@ on:
|
||||
release:
|
||||
type: string
|
||||
default: 'true'
|
||||
build-args:
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
artifact-name:
|
||||
@@ -87,6 +89,7 @@ jobs:
|
||||
tags: ${{ inputs.repo }}:${{ inputs.tag }}
|
||||
no-cache: true
|
||||
push: true
|
||||
build-args: ${{ inputs.build-args }}
|
||||
|
||||
# we need to do this to get the nice fallback behavior of the legacy manifest file
|
||||
- name: Build container (x86 only)
|
||||
|
||||
3
.github/workflows/uberjar.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- metabot-v3-main
|
||||
- feature-visualizer
|
||||
- internal-tools
|
||||
paths-ignore:
|
||||
@@ -141,6 +142,8 @@ jobs:
|
||||
repo: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).repo }}
|
||||
tag: ${{ fromJson(needs.get-container-info.outputs[matrix.edition]).tag }}
|
||||
release: "false"
|
||||
build-args: |
|
||||
GIT_COMMIT_SHA=${{ github.event.inputs.commit || github.sha }}
|
||||
|
||||
run-trivy:
|
||||
needs: [get-container-info, containerize]
|
||||
|
||||
5
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
*.po~
|
||||
*.sqlite
|
||||
*.trace.db
|
||||
*.trace.db.old
|
||||
*.xcworkspacedata
|
||||
.DS_Store
|
||||
.\#*
|
||||
@@ -133,6 +134,10 @@ modules/drivers/*/.lsp/*
|
||||
|
||||
# jar downloader
|
||||
/jars/
|
||||
|
||||
# metabot
|
||||
llm-payloads
|
||||
|
||||
/bin/bb
|
||||
|
||||
.aider*
|
||||
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
.loki/reference/chrome_laptop_Viz_Static_Viz_Gauge_Watermark.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 33 KiB |
BIN
.loki/reference/chrome_laptop_viz_BarChart_Watermark.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
.loki/reference/chrome_laptop_viz_Funnel_Watermark.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
.loki/reference/chrome_laptop_viz_PieChart_Watermark.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
@@ -11,7 +11,7 @@ const {
|
||||
|
||||
module.exports = {
|
||||
stories: ["../enterprise/frontend/src/embedding-sdk/**/*.stories.tsx"],
|
||||
staticDirs: ["../resources/frontend_client"],
|
||||
staticDirs: ["../resources/frontend_client", "./msw-public"],
|
||||
addons: [
|
||||
"@storybook/addon-webpack5-compiler-babel",
|
||||
"@storybook/addon-interactions",
|
||||
@@ -25,9 +25,9 @@ module.exports = {
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
reactDocgen: "react-docgen-typescript",
|
||||
reactDocgen: false,
|
||||
},
|
||||
webpackFinal: storybookConfig => ({
|
||||
webpackFinal: (storybookConfig) => ({
|
||||
...storybookConfig,
|
||||
plugins: [
|
||||
...storybookConfig.plugins,
|
||||
@@ -44,10 +44,10 @@ module.exports = {
|
||||
...storybookConfig.module,
|
||||
rules: [
|
||||
...storybookConfig.module.rules.filter(
|
||||
rule => !isCSSRule(rule) && !isSvgRule(rule),
|
||||
(rule) => !isCSSRule(rule) && !isSvgRule(rule),
|
||||
),
|
||||
...appConfig.module.rules.filter(
|
||||
rule => isCSSRule(rule) || isSvgRule(rule),
|
||||
(rule) => isCSSRule(rule) || isSvgRule(rule),
|
||||
),
|
||||
],
|
||||
},
|
||||
@@ -65,8 +65,8 @@ module.exports = {
|
||||
}),
|
||||
};
|
||||
|
||||
const isCSSRule = rule => rule.test?.toString() === "/\\.css$/";
|
||||
const isSvgRule = rule => rule.test && rule.test?.test(".svg");
|
||||
const isCSSRule = (rule) => rule.test?.toString() === "/\\.css$/";
|
||||
const isSvgRule = (rule) => rule.test && rule.test?.test(".svg");
|
||||
|
||||
function resolveEmbeddingSdkPackage() {
|
||||
let isEmbeddingSdkPackageInstalled = false;
|
||||
|
||||
307
.storybook-sdk/msw-public/mockServiceWorker.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.7.5'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId))
|
||||
})
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
body: responseClone.body,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
},
|
||||
},
|
||||
[responseClone.body],
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const requestBuffer = await request.arrayBuffer()
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: requestBuffer,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
},
|
||||
[requestBuffer],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(
|
||||
message,
|
||||
[channel.port2].concat(transferrables.filter(Boolean)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GlobalTypes } from "@storybook/types";
|
||||
import { initialize, mswLoader } from "msw-storybook-addon";
|
||||
|
||||
// @ts-expect-error: See metabase/lib/delay
|
||||
// This will skip the skippable delays in stories
|
||||
@@ -7,7 +8,7 @@ window.METABASE_REMOVE_DELAYS = true;
|
||||
import { storybookThemeOptions } from "embedding-sdk/test/storybook-themes";
|
||||
import { availableLocales } from "./constants";
|
||||
|
||||
export const parameters = {
|
||||
const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
@@ -17,9 +18,9 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = []; // No decorators for Embedding SDK stories, as we want to simulate real use cases
|
||||
const decorators = []; // No decorators for Embedding SDK stories, as we want to simulate real use cases
|
||||
|
||||
export const globalTypes: GlobalTypes = {
|
||||
const globalTypes: GlobalTypes = {
|
||||
sdkTheme: {
|
||||
name: "SDK Theme",
|
||||
description: "Global theme for sdk components",
|
||||
@@ -54,3 +55,4 @@ export const globalTypes: GlobalTypes = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-va
|
||||
import { css, Global, useTheme } from "@emotion/react";
|
||||
import { baseStyle, rootStyle } from "metabase/css/core/base.styled";
|
||||
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
|
||||
import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image";
|
||||
import { saveDomImageStyles } from "metabase/visualizations/lib/image-exports";
|
||||
|
||||
// Note: Changing the names of the stories may impact loki visual testing. Please ensure that
|
||||
// Any story name changes are also reflected in the loki.config.js storiesFilter array.
|
||||
@@ -72,7 +72,7 @@ const globalStyles = css`
|
||||
`;
|
||||
|
||||
const decorators = [
|
||||
Story => {
|
||||
(Story) => {
|
||||
if (!document.body.classList.contains("mb-wrapper")) {
|
||||
document.body.classList.add("mb-wrapper");
|
||||
}
|
||||
|
||||
10
CLAUDE.md
@@ -74,10 +74,10 @@ The `./bin/mage -check-readable <file> <optional: line-number>` command checks i
|
||||
This ensures your changes maintain valid syntax and structure.
|
||||
|
||||
- Edit Clojure files one step at a time.
|
||||
- After EVERY change to a Clojure form, call `mage -check-readable src/metabase/thefile.clj <line-number>` with the line number.
|
||||
- After EVERY change to a Clojure form, call `mage -check-readable src/metabase/thefile.clj <line-number>` with the line number.
|
||||
- If it's readable then call `mage -check-readable dev/src/dev.clj` without the line number to check the entire file.
|
||||
- If the change results in unreadable code, try again until it is readable.
|
||||
- To overcome errors about parens, pay close attention to them. Count opening/closing parens you add/remove.
|
||||
- To overcome errors about parens, pay close attention to them. Count opening/closing parens you add/remove.
|
||||
|
||||
#### Bottom-up dev loop
|
||||
|
||||
@@ -104,9 +104,9 @@ mage -repl --namespace metabase.db.connection
|
||||
|
||||
To call `your.namespace/your-function` on `arg1` and `arg2`:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
|
||||
```
|
||||
```
|
||||
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
|
||||
```
|
||||
|
||||
DO NOT use "require", "load-file" etc in the code string argument.
|
||||
|
||||
|
||||
64
deps.edn
@@ -8,7 +8,7 @@
|
||||
:exclusions [org.clojure/clojure
|
||||
org.clojure/clojurescript]}
|
||||
amalloy/ring-gzip-middleware {:mvn/version "0.1.4"} ; Ring middleware to GZIP responses if client can handle it
|
||||
babashka/fs {:mvn/version "0.5.24"} ; Portable filesystem operations
|
||||
babashka/fs {:mvn/version "0.5.25"} ; Portable filesystem operations
|
||||
bigml/histogram {:mvn/version "5.0.0" ; Histogram data structure
|
||||
:exclusions [junit/junit]}
|
||||
buddy/buddy-core {:mvn/version "1.12.0-430" ; various cryptographic functions
|
||||
@@ -51,19 +51,19 @@
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"} ; Talk to JDBC DBs
|
||||
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.7"} ; 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.6-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.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.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 "76.1"} ; Used to transliterate Unicode characters into Latin characters
|
||||
{:mvn/version "2.1.214"} ; embedded SQL database
|
||||
com.ibm.icu/icu4j {:mvn/version "77.1"} ; Used to transliterate Unicode characters into Latin characters
|
||||
com.mchange/c3p0 {:mvn/version "0.10.1"} ; DB connection pool
|
||||
com.snowplowanalytics/snowplow-java-tracker ^:antq/exclude ; 2+ depend on apache httpclient 5.x, but saml and clj-http depend on 4.x
|
||||
{:mvn/version "1.0.1"} ; Snowplow analytics
|
||||
com.taoensso/nippy {:mvn/version "3.4.2"} ; Fast serialization (i.e., GZIP) library for Clojure
|
||||
com.taoensso/nippy {:mvn/version "3.5.0"} ; Fast serialization (i.e., GZIP) library for Clojure
|
||||
com.vladsch.flexmark/flexmark {:mvn/version "0.64.8"} ; Markdown parsing
|
||||
com.vladsch.flexmark/flexmark-ext-autolink
|
||||
{:mvn/version "0.64.8"} ; Flexmark extension for auto-linking bare URLs
|
||||
commons-codec/commons-codec {:mvn/version "1.18.0"} ; Apache Commons -- useful codec util fns
|
||||
commons-io/commons-io {:mvn/version "2.18.0"} ; Apache Commons -- useful IO util fns
|
||||
commons-io/commons-io {:mvn/version "2.19.0"} ; Apache Commons -- useful IO util fns
|
||||
commons-net/commons-net {:mvn/version "3.11.1"} ; Apache Commons -- useful network utils. Transitive dep of Snowplow, pinned due to CVE-2021-37533
|
||||
commons-validator/commons-validator {:mvn/version "1.9.0" ; Apache Commons -- useful validation util fns
|
||||
:exclusions [commons-beanutils/commons-beanutils
|
||||
@@ -82,14 +82,12 @@
|
||||
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.561"}
|
||||
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
|
||||
:git/sha "9598f4c72b528480a85ea41651c45b08d1717428"
|
||||
:git/sha "4998a1fdd9d3713d7034d04509e9212204773bf2"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
io.github.metabase/macaw {:mvn/version "0.2.25"} ; Parse native SQL queries
|
||||
io.github.resilience4j/resilience4j-retry ^:antq/exclude ; 2.x do not support Java 8
|
||||
{:mvn/version "1.7.1"} ; Support for retrying operations
|
||||
{:mvn/version "1.7.1"} ; Support for retrying operations
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} ; prometheus jvm collector
|
||||
io.prometheus/simpleclient_jetty {:mvn/version "0.16.0"} ; prometheus jetty collector
|
||||
javax.servlet/servlet-api {:mvn/version "2.5"} ; used by ring's multipart-params (file upload)
|
||||
junegunn/grouper {:mvn/version "0.1.1"} ; Batch processing helper
|
||||
kixi/stats {:mvn/version "0.5.7" ; Various statistic measures implemented as transducers
|
||||
:exclusions [org.clojure/data.avl]}
|
||||
@@ -126,11 +124,11 @@
|
||||
{:mvn/version "2.24.3"} ; allows the custom json logging format
|
||||
org.apache.logging.log4j/log4j-slf4j2-impl
|
||||
{:mvn/version "2.24.3"} ; allows the slf4j2 API to work with log4j 2
|
||||
org.apache.poi/poi {:mvn/version "5.4.0"} ; Work with Office documents (e.g. Excel spreadsheets) -- newer version than one specified by Docjure
|
||||
org.apache.poi/poi-ooxml {:mvn/version "5.4.0"
|
||||
org.apache.poi/poi {:mvn/version "5.4.1"} ; Work with Office documents (e.g. Excel spreadsheets) -- newer version than one specified by Docjure
|
||||
org.apache.poi/poi-ooxml {:mvn/version "5.4.1"
|
||||
:exclusions [org.bouncycastle/bcpkix-jdk15on
|
||||
org.bouncycastle/bcprov-jdk15on]}
|
||||
org.apache.poi/poi-ooxml-full {:mvn/version "5.4.0"}
|
||||
org.apache.poi/poi-ooxml-full {:mvn/version "5.4.1"}
|
||||
org.apache.sshd/sshd-core {:mvn/version "2.15.0" ; ssh tunneling and test server
|
||||
:exclusions [org.slf4j/slf4j-api
|
||||
org.slf4j/jcl-over-slf4j]}
|
||||
@@ -139,6 +137,7 @@
|
||||
org.bouncycastle/bcpkix-jdk18on {:mvn/version "1.80"} ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors
|
||||
org.bouncycastle/bcprov-jdk18on {:mvn/version "1.80"}
|
||||
org.clj-commons/claypoole {:mvn/version "1.2.2"} ; Threadpool tools for Clojure
|
||||
org.clj-commons/dirigiste {:mvn/version "1.0.4"} ; Instrumented object pooling
|
||||
org.clj-commons/hickory {:mvn/version "0.7.7" ; Parse HTML into Clojure data structures
|
||||
:exclusions [org.jsoup/jsoup]}
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} ; LDAP client
|
||||
@@ -161,14 +160,14 @@
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
org.clojure/tools.reader {:mvn/version "1.5.2"}
|
||||
org.clojure/tools.trace {:mvn/version "0.8.0"} ; function tracing
|
||||
org.eclipse.jetty/jetty-server {:mvn/version "12.0.19"}
|
||||
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.0.19"}
|
||||
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.0.19"}
|
||||
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.0.19"}
|
||||
org.eclipse.jetty/jetty-server {:mvn/version "12.0.20"}
|
||||
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.0.20"}
|
||||
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.0.20"}
|
||||
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.0.20"}
|
||||
org.flatland/ordered {:mvn/version "1.15.12"} ; ordered maps & sets
|
||||
org.graalvm.polyglot/js-community {:mvn/version "24.2.0" :extension "pom"} ; JavaScript engine
|
||||
org.graalvm.polyglot/polyglot {:mvn/version "24.2.0"} ; GraalVM platform, required by JS engine
|
||||
org.jsoup/jsoup {:mvn/version "1.19.1"} ; required by hickory
|
||||
org.jsoup/jsoup {:mvn/version "1.20.1"} ; required by hickory
|
||||
org.liquibase/liquibase-core ^:antq/exclude ; FIXME: no idea why it fails
|
||||
{:mvn/version "4.26.0" ; migration management (Java lib)
|
||||
:exclusions [ch.qos.logback/logback-classic]}
|
||||
@@ -176,7 +175,7 @@
|
||||
{:mvn/version "2.7.10"} ; MySQL/MariaDB driver
|
||||
org.mindrot/jbcrypt {:mvn/version "0.4"} ; Crypto library
|
||||
; NOTE: Postgres fix in 42.7.5 breaks liquibase on case-sensitive databases until https://github.com/liquibase/liquibase/pull/6755 is released
|
||||
org.postgresql/postgresql {:mvn/version "42.7.4"} ; Postgres driver
|
||||
org.postgresql/postgresql {:mvn/version "42.7.5"} ; Postgres driver
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.17"} ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
|
||||
org.tcrawley/dynapath {:mvn/version "1.1.0"} ; Dynamically add Jars (e.g. Oracle or Vertica) to classpath
|
||||
org.threeten/threeten-extra {:mvn/version "1.8.0"} ; extra Java 8 java.time classes like DayOfMonth and Quarter
|
||||
@@ -241,18 +240,20 @@
|
||||
{:extra-deps
|
||||
{clj-http-fake/clj-http-fake {:mvn/version "1.0.4"
|
||||
:exclusions [slingshot/slingshot]}
|
||||
clj-kondo/clj-kondo {:mvn/version "2025.02.20"} ; included here mainly to facilitate working on the stuff in `.clj-kondo/src`
|
||||
clj-kondo/clj-kondo {:mvn/version "2025.04.07"} ; included here mainly to facilitate working on the stuff in `.clj-kondo/src`
|
||||
cloverage/cloverage {:mvn/version "1.2.4"}
|
||||
com.clojure-goes-fast/clj-async-profiler
|
||||
{:mvn/version "1.6.1"} ; Enables local profiling and heat map generation
|
||||
com.clojure-goes-fast/clj-memory-meter
|
||||
{:mvn/version "0.4.0"} ; Enables easy memory measurement
|
||||
com.gfredericks/test.chuck {:mvn/version "0.2.15"} ; generating strings from regexes (useful with malli)
|
||||
com.nextjournal/beholder {:mvn/version "1.0.2"} ; watcher
|
||||
criterium/criterium {:mvn/version "0.4.6"} ; benchmarking library
|
||||
dev.weavejester/hashp {:mvn/version "0.4.0"} ; debugging/spying utility
|
||||
djblue/portal {:mvn/version "0.59.0"} ; ui for inspecting values
|
||||
io.github.camsaul/humane-are {:mvn/version "1.0.2"}
|
||||
io.github.metabase/hawk {:mvn/version "1.0.7"}
|
||||
io.github.camsaul/humane-are {:mvn/version "1.0.2"} ; cleaner test output
|
||||
io.github.metabase/hawk {:mvn/version "1.0.7"} ; test runner
|
||||
io.github.tonsky/clj-reload {:mvn/version "0.9.5"} ; reloader
|
||||
jonase/eastwood {:mvn/version "1.4.3" ; inspects namespaces and reports possible problems using tools.analyzer
|
||||
:exclusions
|
||||
[org.ow2.asm/asm-all]}
|
||||
@@ -307,6 +308,9 @@
|
||||
;; 1. Runs (dev) (start!) for you
|
||||
;; 2. This starts the repl too: just like the :nrepl alias below. (see [[user/-main]])
|
||||
;; clj -M:dev:dev-start:drivers:drivers-dev:ee:ee-dev
|
||||
;;
|
||||
;; To start it with hot code reloading, use:
|
||||
;; clj -M:dev:dev-start:drivers:drivers-dev:ee:ee-dev --watch
|
||||
:dev-start {:main-opts ["-m" "user"]
|
||||
:jvm-opts ["-Dmb.run.mode=dev"
|
||||
"-Dmacaw.run.mode=dev"
|
||||
@@ -316,6 +320,7 @@
|
||||
:test
|
||||
{:extra-paths ["test_config"]
|
||||
:exec-fn metabase.test-runner/find-and-run-tests-cli
|
||||
:exec-args {:exclude-tags [:metabot-v3/e2e]}
|
||||
:jvm-opts ["-Dmb.run.mode=test"
|
||||
"-Dmacaw.run.mode=test"
|
||||
"-Dmb.db.in.memory=true"
|
||||
@@ -397,15 +402,15 @@
|
||||
{:extra-paths ["test"]
|
||||
:extra-deps
|
||||
{binaryage/devtools {:mvn/version "1.0.7"}
|
||||
cider/cider-nrepl {:mvn/version "0.55.4"}
|
||||
cider/cider-nrepl {:mvn/version "0.55.7"}
|
||||
cider/piggieback {:mvn/version "0.6.0"}
|
||||
cljs-bean/cljs-bean {:mvn/version "1.9.0"}
|
||||
com.lambdaisland/glogi {:mvn/version "1.3.169"}
|
||||
io.github.metabase/hawk {:mvn/version "1.0.7"}
|
||||
org.clojars.mmb90/cljs-cache {:mvn/version "0.1.4"}
|
||||
refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}
|
||||
refactor-nrepl/refactor-nrepl {:mvn/version "3.11.0"}
|
||||
;; keep it in sync with package.json
|
||||
thheller/shadow-cljs {:mvn/version "2.28.21"}}}
|
||||
thheller/shadow-cljs {:mvn/version "3.0.4"}}}
|
||||
|
||||
;; for local dev -- include the drivers locally with :dev:drivers
|
||||
:drivers
|
||||
@@ -453,7 +458,7 @@
|
||||
;; Use this to only run Kondo against specific files.
|
||||
:kondo
|
||||
{:replace-deps
|
||||
{clj-kondo/clj-kondo {:mvn/version "2025.02.20"}}
|
||||
{clj-kondo/clj-kondo {:mvn/version "2025.04.07"}}
|
||||
|
||||
:main-opts
|
||||
["-m" "clj-kondo.main"]}
|
||||
@@ -868,13 +873,6 @@
|
||||
:test/kondo
|
||||
{:exec-args {:only [".clj-kondo/test"]}}
|
||||
|
||||
;; watch and reload clojure namespaces
|
||||
:watch {:extra-deps
|
||||
{com.nextjournal/beholder {:mvn/version "1.0.2"} ;; watcher
|
||||
io.github.tonsky/clj-reload {:mvn/version "0.9.4"}} ;; reloader
|
||||
:extra-paths "dev/src/watch"
|
||||
:main-opts ["-m" "watch.watcher"]}
|
||||
|
||||
;; DB configs -- start DBs with:
|
||||
|
||||
;; ./bin/mage start-db postgres oldest
|
||||
|
||||
@@ -51,11 +51,19 @@
|
||||
(in-ns 'dev)
|
||||
:loaded)
|
||||
|
||||
(defn watch? []
|
||||
(contains? (set *command-line-args*) "--watch"))
|
||||
|
||||
(defn -main
|
||||
"This is called by the `:dev-start` cli alias.
|
||||
|
||||
Try it out: `clj -M:dev:dev-start:drivers:drivers-dev:ee:ee-dev`"
|
||||
Try it out: `clj -M:dev:dev-start:drivers:drivers-dev:ee:ee-dev`
|
||||
|
||||
Command line options:
|
||||
- `--watch` - start the hot-reload clojure code watcher"
|
||||
[& _args]
|
||||
(future (nrepl.cmdline/-main "-p" "50605" "-b" "0.0.0.0"))
|
||||
(when (watch?)
|
||||
(future (requiring-resolve 'watch.watcher/watcher)))
|
||||
((requiring-resolve 'dev/start!))
|
||||
(deref (promise)))
|
||||
|
||||
@@ -1,51 +1,29 @@
|
||||
(ns watch.watcher
|
||||
(:require [clj-reload.core :as reload]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.string :as str]
|
||||
[dev :as dev]
|
||||
[metabase.util.log :as log]
|
||||
[nextjournal.beholder :as beholder]
|
||||
[user :as user])
|
||||
(ns ^:clj-reload/no-unload
|
||||
^:clj-reload/no-reload
|
||||
watch.watcher
|
||||
(:require
|
||||
[clj-reload.core :as reload]
|
||||
[dev :as dev]
|
||||
[metabase.util.log :as log]
|
||||
[nextjournal.beholder :as beholder]
|
||||
[user :as user])
|
||||
(:gen-class))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(def all-paths-to-watch
|
||||
"Returns all paths that contain code. These are the paths we should watch."
|
||||
(memoize
|
||||
(fn all-paths-to-watch* []
|
||||
(->> (str (System/getProperty "user.dir") "/deps.edn")
|
||||
slurp
|
||||
edn/read-string
|
||||
:aliases
|
||||
vals
|
||||
(keep :extra-paths)
|
||||
(mapv #(if (coll? %) % [%]))
|
||||
(apply concat)
|
||||
(map (comp first #(str/split % #"/")))
|
||||
(concat ["dev" "src" "test"])
|
||||
distinct
|
||||
vec))))
|
||||
|
||||
(defn watch-fn
|
||||
"Reloads the system when a file changes."
|
||||
[{:keys [type path]}]
|
||||
(log/warnf "type: %s | path: %s" type path)
|
||||
(let [before (System/currentTimeMillis)
|
||||
_ (log/warn
|
||||
(-> (reload/reload {:throw false
|
||||
:no-reload '#{watch.watcher user dev}})
|
||||
(update :unloaded count)
|
||||
(update :loaded count)))
|
||||
after (System/currentTimeMillis)
|
||||
_ (log/warn (str "Reloaded in " (- after before) " ms"))]))
|
||||
[change]
|
||||
(log/warn "Change detected. Reloading system. " (pr-str change) " ...")
|
||||
(reload/reload {:throw :false}))
|
||||
|
||||
(def ^:private paths
|
||||
["src" "test" "enterprise/backend" "modules"])
|
||||
|
||||
#_:clj-kondo/ignore
|
||||
(defonce watcher
|
||||
(do
|
||||
(log/warn "Watching paths:" (all-paths-to-watch))
|
||||
(reload/init {:dirs (all-paths-to-watch)})
|
||||
(apply (partial beholder/watch watch-fn) (all-paths-to-watch))))
|
||||
(log/warn "Watching paths:" paths)
|
||||
(apply beholder/watch watch-fn paths)))
|
||||
|
||||
(defn -main "Watcher main function"
|
||||
[& _args]
|
||||
@@ -54,6 +32,6 @@
|
||||
|
||||
(comment
|
||||
|
||||
(beholder/stop watcher)
|
||||
(reload/reload)
|
||||
|
||||
)
|
||||
(beholder/stop watcher))
|
||||
|
||||
@@ -12,7 +12,6 @@ clojure -M:doc:ee config-template
|
||||
|
||||
The template lists example `database`, `user`, and `settings` sections for the [config file](./config-file.md).
|
||||
|
||||
|
||||
```yaml
|
||||
# A config file template for Metabase.
|
||||
# You'll need to update (or remove) the `users` and `databases` sections.
|
||||
@@ -27,45 +26,45 @@ The template lists example `database`, `user`, and `settings` sections for the [
|
||||
version: 1
|
||||
config:
|
||||
users:
|
||||
- first_name: First
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
email: first@example.com
|
||||
- first_name: Normal
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
email: normal@example.com
|
||||
- first_name: Admin
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
is_superuser: true
|
||||
email: admin@example.com
|
||||
- first_name: First
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
email: first@example.com
|
||||
- first_name: Normal
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
email: normal@example.com
|
||||
- first_name: Admin
|
||||
last_name: Person
|
||||
password: metabot1
|
||||
is_superuser: true
|
||||
email: admin@example.com
|
||||
databases:
|
||||
- name: Sample PostgreSQL
|
||||
engine: postgres
|
||||
details:
|
||||
host: postgres-data
|
||||
port: 5432
|
||||
user: metabase
|
||||
password: metasample123
|
||||
dbname: sample
|
||||
- name: Sample MySQL
|
||||
engine: mysql
|
||||
details:
|
||||
host: mysql-data
|
||||
port: 3306
|
||||
user: metabase
|
||||
password: metasample123
|
||||
dbname: sample
|
||||
- name: Sample PostgreSQL
|
||||
engine: postgres
|
||||
details:
|
||||
host: postgres-data
|
||||
port: 5432
|
||||
user: metabase
|
||||
password: metasample123
|
||||
dbname: sample
|
||||
- name: Sample MySQL
|
||||
engine: mysql
|
||||
details:
|
||||
host: mysql-data
|
||||
port: 3306
|
||||
user: metabase
|
||||
password: metasample123
|
||||
dbname: sample
|
||||
api-keys:
|
||||
- name: Admin API key
|
||||
group: admin
|
||||
creator: first@example.com
|
||||
key: mb_firsttestapikey123
|
||||
- name: All Users API key
|
||||
group: all-users
|
||||
creator: first@example.com
|
||||
key: mb_secondtestapikey456
|
||||
- name: Admin API key
|
||||
group: admin
|
||||
creator: first@example.com
|
||||
key: mb_firsttestapikey123
|
||||
- name: All Users API key
|
||||
group: all-users
|
||||
creator: first@example.com
|
||||
key: mb_secondtestapikey456
|
||||
settings:
|
||||
admin-email: null
|
||||
aggregated-query-row-limit: null
|
||||
@@ -136,7 +135,6 @@ config:
|
||||
enable-embedding-interactive: false
|
||||
enable-embedding-sdk: false
|
||||
enable-embedding-static: false
|
||||
enable-field-usage-analysis: false
|
||||
enable-password-login: true
|
||||
enable-pivoted-exports: true
|
||||
enable-public-sharing: true
|
||||
@@ -149,7 +147,7 @@ config:
|
||||
gsheets: null
|
||||
health-check-logging-enabled: true
|
||||
help-link: metabase
|
||||
help-link-custom-destination: https://www.metabase.com/help/premium
|
||||
help-link-custom-destination: https://www.metabase.com/help-premium
|
||||
humanization-strategy: simple
|
||||
jdbc-data-warehouse-max-connection-pool-size: 15
|
||||
jwt-attribute-email: email
|
||||
@@ -162,7 +160,7 @@ config:
|
||||
jwt-identity-provider-uri: null
|
||||
jwt-shared-secret: null
|
||||
jwt-user-provisioning-enabled: true
|
||||
landing-page: ''
|
||||
landing-page: ""
|
||||
landing-page-illustration: default
|
||||
landing-page-illustration-custom: null
|
||||
ldap-attribute-email: mail
|
||||
|
||||
@@ -45,7 +45,7 @@ docker run -d -p 3000:3000 -e MB_SITE_NAME="Awesome Company" --name metabase met
|
||||
|
||||
## Environment variables on Metabase Cloud
|
||||
|
||||
If you're running Metabase Cloud, you can [contact support](https://www.metabase.com/help/premium) to adjust environment variables for your Metabase.
|
||||
If you're running Metabase Cloud, you can [contact support](https://www.metabase.com/help-premium) to adjust environment variables for your Metabase.
|
||||
|
||||
---
|
||||
|
||||
@@ -573,17 +573,6 @@ Allow admins to embed Metabase via the SDK?
|
||||
|
||||
Allow admins to embed Metabase via static embedding?
|
||||
|
||||
### `MB_ENABLE_FIELD_USAGE_ANALYSIS`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
- [Configuration file name](./config-file.md): `enable-field-usage-analysis`
|
||||
|
||||
Enable field usage analysis for queries. This will analyze the fields used in queries and store them in the
|
||||
application database.
|
||||
|
||||
Turn off by default since we haven't had an user-facing feature that uses this data yet.
|
||||
|
||||
### `MB_ENABLE_PASSWORD_LOGIN`
|
||||
|
||||
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
|
||||
@@ -690,7 +679,7 @@ Keyword setting to control whitelabeling of the help link. Valid values are `:me
|
||||
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
|
||||
|
||||
- Type: string
|
||||
- Default: `https://www.metabase.com/help/premium`
|
||||
- Default: `https://www.metabase.com/help-premium`
|
||||
- [Configuration file name](./config-file.md): `help-link-custom-destination`
|
||||
|
||||
Custom URL for the help link.
|
||||
@@ -2241,4 +2230,3 @@ Type: string<br>
|
||||
Default: `null`
|
||||
|
||||
Base-64 encoded public key for this sites SSL certificate. Specify this to enable HTTP Public Key Pinning. Using HPKP is no longer recommended. See http://mzl.la/1EnfqBf for more information.
|
||||
|
||||
|
||||
@@ -26,10 +26,9 @@ Supported languages include:
|
||||
| Arabic (Saudi Arabia) | `ar-SA` |
|
||||
| Bulgarian | `bg` |
|
||||
| Catalan | `ca` |
|
||||
| Chinese (Traditional) | `zh` |
|
||||
| Chinese (Taiwanese) | `zh-TW` |
|
||||
| Chinese (Hong Kong) | `zh-HK` |
|
||||
| Chinese (Simplified) | `zh-CN` |
|
||||
| Chinese (Taiwanese) | `zh-TW` |
|
||||
| Czech | `cs` |
|
||||
| Dutch | `nl` |
|
||||
| Farsi/Persian | `fa` |
|
||||
@@ -39,11 +38,11 @@ Supported languages include:
|
||||
| Hebrew | `he` |
|
||||
| Hungarian | `hu` |
|
||||
| Indonesian | `id` |
|
||||
| Malay | `ms` |
|
||||
| Italian | `it` |
|
||||
| Japanese | `ja` |
|
||||
| Korean | `ko` |
|
||||
| Latvian | `lv` |
|
||||
| Malay | `ms` |
|
||||
| Norwegian Bokmål | `nb` |
|
||||
| Polish | `pl` |
|
||||
| Portuguese (Brazilian) | `pt-BR` |
|
||||
|
||||
@@ -80,7 +80,7 @@ If you check this box, Metabase will drop any visualization settings applied to
|
||||
|
||||
Here you can specify which questions Metabase should attach results for.
|
||||
|
||||
The attached files will include up to 2000 rows by default. If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [MB_UNAGGREGATED_QUERY_ROW_LIMIT](../configuring-metabase/environment-variables.md#mb_unaggregated_query_row_limit). To change this row limit on your Metabase Cloud instance, you can [contact us](https://www.metabase.com/help/premium) and request a different row limit.
|
||||
The attached files will include up to 2000 rows by default. If you're self-hosting Metabase, you can adjust this row limit by setting the environment variable [MB_UNAGGREGATED_QUERY_ROW_LIMIT](../configuring-metabase/environment-variables.md#mb_unaggregated_query_row_limit). To change this row limit on your Metabase Cloud instance, you can [contact us](https://www.metabase.com/help-premium) and request a different row limit.
|
||||
|
||||
## Slack subscription options
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ Note that only the `*` wildcard is supported; you can't use other special charac
|
||||
|
||||
## Role (optional)
|
||||
|
||||
> **WARNING:** Metabase gets _all the permissions_ combined from _every_ role you grant the user in Snowflake. Setting the 'Role' here just adds the role to the connection string to tell Snowflake which role to default to – it **won't restrict** access on its own. So **be careful which roles you assign to the connection user**: if down the line you grant that user broader roles in Snowflake, Metabase will automatically gets those new permissions, potentially showing data you didn't mean for it to see. If instead you want Metabase to issue `SET ROLE` commands before querying anything, check out [connection impersonation](../../permissions/impersonation.md).
|
||||
|
||||
Specify a role to override the database user's default role. For example, if the database user `METABASE` has the roles:
|
||||
|
||||
- Default role `APPLICATION`.
|
||||
|
||||
@@ -4,6 +4,12 @@ title: API changelog
|
||||
|
||||
# Breaking changes to the API interface
|
||||
|
||||
## Metabase 0.55.0
|
||||
|
||||
- `POST /api/card/from-csv` has been renamed to `POST /api/upload/csv`.
|
||||
|
||||
- `GET /api/util/stats` has been renamed to `GET /api/analytics/anonymous-stats`.
|
||||
|
||||
## Metabase 0.54.0
|
||||
|
||||
- The alert system has been migrated from the legacy pulse infrastructure to the new notification system. This migration includes the following changes:
|
||||
|
||||
@@ -6,6 +6,12 @@ title: Driver interface changelog
|
||||
|
||||
## Metabase 0.55.0
|
||||
|
||||
- Add the multi-method `->date` that allows the driver to control how to cast strings and temporal types to dates.
|
||||
|
||||
- Add the multi-method `date-dbtype` that allows the driver to control which types dates are cast to.
|
||||
|
||||
- Extend `date()` to accept a DateTime (or DB equivalent) in addition to an ISO string. When given a DateTime, it will truncate it to a date.
|
||||
|
||||
- Added a feature `:expression-literals` for drivers that support expressions consisting of a single string, number, or boolean literal value.
|
||||
|
||||
- Added a feature `:multi-level-schema` for drivers that support hierarchical levels between database and schema. Such as databricks' catalog. Defaults to false.
|
||||
@@ -20,6 +26,11 @@ title: Driver interface changelog
|
||||
|
||||
- Added the multi-method `integer-dbtype` that allows the driver to control which type integers are cast to.
|
||||
|
||||
- The `metabase.upload` namespace has been replaced with `metabase.upload.core`, but upload type keywords e.g.
|
||||
`:metabase.upload/varchar-255` remain unchanged. Make sure you weren't using `::` keywords inside methods like
|
||||
`metabase.driver/upload-type->database-type` or `metabase.driver/allowed-promotions` -- make sure you use
|
||||
`:metabase.upload/varchar-255` rather than something like `::upload/varchar-255`.
|
||||
|
||||
## Metabase 0.54.0
|
||||
|
||||
- Added the multi-method `allowed-promotions` that allows driver control over which column type promotions are supported for uploads.
|
||||
|
||||
@@ -57,7 +57,7 @@ Assuming you've been using a [production application database](../installation-a
|
||||
|
||||
Migrating to the Enterprise Edition will keep all of your questions, dashboards, people, settings — everything in your existing Metabase.
|
||||
|
||||
And don't stress. You won't lose any of your work, and if you get stuck, we're [here to help](https://www.metabase.com/help/premium).
|
||||
And don't stress. You won't lose any of your work, and if you get stuck, we're [here to help](https://www.metabase.com/help-premium).
|
||||
|
||||
## Validating your token
|
||||
|
||||
@@ -99,12 +99,12 @@ If you're hosting Metabase behind a firewall that blocks outgoing connections, *
|
||||
44.212.138.188
|
||||
```
|
||||
|
||||
To verify your license with a token check to `token-check.metabase.com`, your Metabase will make GET HTTP requests to these IP addresses and parse their responses. If you can't allow outbound connections for security reasons, please [contact us](https://www.metabase.com/help/premium).
|
||||
To verify your license with a token check to `token-check.metabase.com`, your Metabase will make GET HTTP requests to these IP addresses and parse their responses. If you can't allow outbound connections for security reasons, please [contact us](https://www.metabase.com/help-premium).
|
||||
|
||||
## Note about Zscaler deployments
|
||||
|
||||
When Metabase is deployed inside infrastructure that uses Zscaler, you should do the following:
|
||||
|
||||
1. Contact your networking team and let them know that Metabase will need to perform token checks in order for paid features to work. If you need an air-gapped version of Metabase, [contact us](https://www.metabase.com/help/premium).
|
||||
1. Contact your networking team and let them know that Metabase will need to perform token checks in order for paid features to work. If you need an air-gapped version of Metabase, [contact us](https://www.metabase.com/help-premium).
|
||||
2. Make sure Zscaler isn't acting as a proxy or DNS for the server where Metabase is running. Metabase needs a direct connection to the token check service without any gateway acting as a proxy.
|
||||
3. Make sure the server where Metabase is running isn't using Zscaler root CA certificates for all websites. Otherwise, the Java virtual machine where Metabase runs will determine that the certificate authority is incorrect.
|
||||
|
||||
@@ -11,7 +11,7 @@ Metabase supports user provisioning via the System for Cross-domain Identity Man
|
||||
- **Decouple authentication from provisioning**. Even though anyone could authenticate with SSO, you may only want some people to be able to create an account in Metabase.
|
||||
- **Support deprovisioning user accounts**. If you deactivate someone from your SSO, SCIM can let Metabase know to deactivate their Metabase account as well.
|
||||
|
||||
> For now, Metabase officially supports SCIM for [Okta](https://help.okta.com/en-us/content/topics/apps/apps_app_integration_wizard_scim.htm?cshid=ext_Apps_App_Integration_Wizard-scim) and [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/configure-automatic-user-provisioning-portal). Other SCIM providers may work, but we haven't tested them. If you're having issues with another identity provider, please [reach out to us](https://www.metabase.com/help/premium).
|
||||
> For now, Metabase officially supports SCIM for [Okta](https://help.okta.com/en-us/content/topics/apps/apps_app_integration_wizard_scim.htm?cshid=ext_Apps_App_Integration_Wizard-scim) and [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/configure-automatic-user-provisioning-portal). Other SCIM providers may work, but we haven't tested them. If you're having issues with another identity provider, please [reach out to us](https://www.metabase.com/help-premium).
|
||||
|
||||
## Setting up user provisioning
|
||||
|
||||
|
||||
@@ -94,6 +94,11 @@ export const SECOND_COLLECTION_ID = _.findWhere(
|
||||
{ name: "Second collection" },
|
||||
).id;
|
||||
|
||||
export const SECOND_COLLECTION_ENTITY_ID = _.findWhere(
|
||||
SAMPLE_INSTANCE_DATA.collections,
|
||||
{ name: "Second collection" },
|
||||
).entity_id;
|
||||
|
||||
export const THIRD_COLLECTION_ID = _.findWhere(
|
||||
SAMPLE_INSTANCE_DATA.collections,
|
||||
{ name: "Third collection" },
|
||||
|
||||
12
e2e/support/helpers/api/createSnippet.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function createSnippet({
|
||||
name,
|
||||
content,
|
||||
}: {
|
||||
name: string;
|
||||
content: string;
|
||||
}): Cypress.Chainable<Cypress.Response<NativeQuerySnippet>> {
|
||||
cy.request("POST", "/api/native-query-snippet", {
|
||||
name,
|
||||
content,
|
||||
});
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export type {
|
||||
} from "./createQuestion";
|
||||
export { createQuestionAndAddToDashboard } from "./createQuestionAndAddToDashboard";
|
||||
export { createQuestionAndDashboard } from "./createQuestionAndDashboard";
|
||||
export { createSnippet } from "./createSnippet";
|
||||
export { createTimeline } from "./createTimeline";
|
||||
export { createTimelineEvent } from "./createTimelineEvent";
|
||||
export { createTimelineWithEvents } from "./createTimelineWithEvents";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { capitalize } from "inflection";
|
||||
|
||||
import {
|
||||
commandPalette,
|
||||
commandPaletteButton,
|
||||
commandPaletteInput,
|
||||
} from "./e2e-command-palette-helpers";
|
||||
import { NativeEditor } from "./e2e-native-editor-helpers";
|
||||
|
||||
export function setActionsEnabledForDB(dbId, enabled = true) {
|
||||
@@ -48,3 +53,13 @@ export function createImplicitActions({ modelId }) {
|
||||
createImplicitAction({ model_id: modelId, kind: "update" });
|
||||
createImplicitAction({ model_id: modelId, kind: "delete" });
|
||||
}
|
||||
|
||||
export function startNewAction() {
|
||||
commandPaletteButton().click();
|
||||
commandPalette().within(() => {
|
||||
commandPaletteInput().type("Ac");
|
||||
cy.findByLabelText("New action").click();
|
||||
});
|
||||
commandPalette().should("not.exist");
|
||||
cy.findByTestId("action-creator").should("be.visible");
|
||||
}
|
||||
|
||||
@@ -3,10 +3,18 @@ import {
|
||||
entityPickerModalLevel,
|
||||
entityPickerModalTab,
|
||||
getFullName,
|
||||
navigationSidebar,
|
||||
popover,
|
||||
} from "e2e/support/helpers";
|
||||
import type { CollectionId } from "metabase-types/api";
|
||||
|
||||
export function startNewCollectionFromSidebar() {
|
||||
return navigationSidebar()
|
||||
.findByLabelText("Create a new collection")
|
||||
.should("be.visible")
|
||||
.click();
|
||||
}
|
||||
|
||||
export function getCollectionActions() {
|
||||
return cy.findByTestId("collection-menu");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export const shortcutModal = () =>
|
||||
cy.findByRole("dialog", { name: "Shortcuts" });
|
||||
|
||||
export const openCommandPalette = () => cy.get("body").type("{ctrl+k}{cmd+k}");
|
||||
export const openShortcutModal = () => cy.get("body").type("?");
|
||||
export const openShortcutModal = () => cy.get("body").type("{shift+?}");
|
||||
|
||||
export const commandPaletteButton = () =>
|
||||
cy.findByTestId("app-bar").findByRole("button", { name: /Search/ });
|
||||
|
||||
78
e2e/support/helpers/e2e-metabot-helpers.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { StaticResponse } from "cypress/types/net-stubbing";
|
||||
|
||||
import { newButton } from "./e2e-ui-elements-helpers";
|
||||
|
||||
export function assertChatVisibility(visibility: "visible" | "not.visible") {
|
||||
cy.findByTestId("metabot-chat").should(
|
||||
visibility === "visible" ? "be.visible" : "not.exist",
|
||||
);
|
||||
}
|
||||
|
||||
export function openMetabotViaShortcutKey(assertVisibility = true) {
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("not.visible");
|
||||
}
|
||||
|
||||
cy.realPress(["Meta", "b"]);
|
||||
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("visible");
|
||||
}
|
||||
}
|
||||
|
||||
export function closeMetabotViaShortcutKey(assertVisibility = true) {
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("visible");
|
||||
}
|
||||
|
||||
cy.realPress(["Meta", "b"]);
|
||||
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("not.visible");
|
||||
}
|
||||
}
|
||||
|
||||
export function openMetabotViaNewMenu(assertVisibility = true) {
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("not.visible");
|
||||
}
|
||||
|
||||
newButton("Metabot request").click();
|
||||
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("visible");
|
||||
}
|
||||
}
|
||||
|
||||
export function closeMetabotViaCloseButton(assertVisibility = true) {
|
||||
cy.findByTestId("metabot-close-chat").click();
|
||||
|
||||
if (assertVisibility) {
|
||||
assertChatVisibility("not.visible");
|
||||
}
|
||||
}
|
||||
|
||||
export function metabotChatInput() {
|
||||
return cy.findByTestId("metabot-chat-input");
|
||||
}
|
||||
|
||||
export function sendMetabotMessage(input: string) {
|
||||
metabotChatInput()
|
||||
.should("not.be.disabled")
|
||||
.click()
|
||||
.should("be.focused")
|
||||
.type(input)
|
||||
.type("{Enter}");
|
||||
}
|
||||
|
||||
export function userMessages() {
|
||||
return cy.findAllByTestId("metabot-chat-message");
|
||||
}
|
||||
|
||||
export const mockMetabotResponse = (response: StaticResponse) => {
|
||||
return cy
|
||||
.intercept("POST", "/api/ee/metabot-v3/agent", (req) => {
|
||||
req.reply(response);
|
||||
})
|
||||
.as("metabotAgent");
|
||||
};
|
||||
@@ -36,7 +36,8 @@ export const expectGoodSnowplowEvent = (eventData, count = 1) => {
|
||||
retrySnowplowRequest(
|
||||
"micro/good",
|
||||
({ body }) => {
|
||||
lastReceivedEvent = body?.[0].event?.unstruct_event?.data?.data;
|
||||
lastReceivedEvent =
|
||||
body?.[0]?.event?.unstruct_event?.data?.data ?? body?.[0];
|
||||
lastFoundEventCount = body.filter((snowplowEvent) =>
|
||||
isDeepMatch(
|
||||
snowplowEvent?.event?.unstruct_event?.data?.data,
|
||||
@@ -101,10 +102,45 @@ function isArrayDeepMatch(array, partialArray) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const getEventNames = (body) => {
|
||||
return body?.map((event) => {
|
||||
// get the event name if it exists
|
||||
const eventName = event?.event?.unstruct_event?.data?.data?.event;
|
||||
if (eventName) {
|
||||
return eventName;
|
||||
}
|
||||
// get the eventType if it exists, print it as object so we can tell
|
||||
if (event.eventType) {
|
||||
return {
|
||||
eventType: event.eventType,
|
||||
};
|
||||
}
|
||||
// fallback to logging it
|
||||
console.log("[not able to parse event name]", event);
|
||||
return "[not able to parse event name]";
|
||||
});
|
||||
};
|
||||
|
||||
export const expectGoodSnowplowEvents = (count) => {
|
||||
retrySnowplowRequest("micro/good", ({ body }) => body.length >= count)
|
||||
.its("body")
|
||||
.should("have.length", count);
|
||||
return retrySnowplowRequest(
|
||||
"micro/good",
|
||||
({ body }) => body.length >= count,
|
||||
({ body }) => {
|
||||
// this gets printed when the timeout is reached, meaning that we never reach the count we expect
|
||||
const eventNamesReceived = getEventNames(body);
|
||||
return `Timeout while waiting for ${count} good Snowplow events, reached ${eventNamesReceived.length} events. ${eventNamesReceived ? `Events names received: ${JSON.stringify(eventNamesReceived, null, 2)}` : ""}`;
|
||||
},
|
||||
).then(({ body }) => {
|
||||
const eventNamesReceived = getEventNames(body);
|
||||
|
||||
if (body.length !== count) {
|
||||
// this gets called when the number of events is greater than what we expected
|
||||
throw new Error(
|
||||
`Expected ${count} good Snowplow events, but got ${eventNamesReceived.length}. ${eventNamesReceived ? `Events names received: ${JSON.stringify(eventNamesReceived, null, 2)}` : ""}`,
|
||||
);
|
||||
}
|
||||
return cy.wrap(body);
|
||||
});
|
||||
};
|
||||
|
||||
export const expectNoBadSnowplowEvents = () => {
|
||||
@@ -138,7 +174,7 @@ const retrySnowplowRequest = (
|
||||
} else {
|
||||
const message =
|
||||
typeof messageOrMessageFn === "function"
|
||||
? messageOrMessageFn()
|
||||
? messageOrMessageFn(response)
|
||||
: messageOrMessageFn;
|
||||
throw new Error("Snowplow retry timeout " + message);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function uploadFile(
|
||||
testFile,
|
||||
uploadMode = "upload",
|
||||
) {
|
||||
cy.intercept("POST", "/api/card/from-csv").as("uploadCSV");
|
||||
cy.intercept("POST", "/api/upload/csv").as("uploadCSV");
|
||||
cy.intercept("POST", "/api/table/*/append-csv").as("appendCSV");
|
||||
cy.intercept("POST", "/api/table/*/replace-csv").as("replaceCSV");
|
||||
|
||||
@@ -104,7 +104,7 @@ export function headlessUpload(collectionId, file) {
|
||||
formData.append("collection_id", collectionId);
|
||||
|
||||
cy.request({
|
||||
url: "/api/card/from-csv",
|
||||
url: "/api/upload/csv",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "multipart/form-data",
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from "./e2e-filter-helpers";
|
||||
export * from "./e2e-ldap-helpers";
|
||||
export * from "./e2e-misc-helpers";
|
||||
export * from "./e2e-mock-app-settings-helpers";
|
||||
export * from "./e2e-metabot-helpers";
|
||||
export * from "./e2e-models-metadata-helpers";
|
||||
export * from "./e2e-native-editor-helpers";
|
||||
export * from "./e2e-notebook-helpers";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { CollectionBrowser } from "@metabase/embedding-sdk-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
FIRST_COLLECTION_ENTITY_ID,
|
||||
SECOND_COLLECTION_ENTITY_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import { signInAsAdminAndEnableEmbeddingSdk } from "e2e/support/helpers/component-testing-sdk";
|
||||
import {
|
||||
mockAuthProviderAndJwtSignIn,
|
||||
@@ -67,4 +72,49 @@ describe("scenarios > embedding-sdk > collection browser", () => {
|
||||
getSdkRoot().findByText("Our analytics").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection using entity ids", () => {
|
||||
beforeEach(() => {
|
||||
signInAsAdminAndEnableEmbeddingSdk();
|
||||
cy.signOut();
|
||||
mockAuthProviderAndJwtSignIn();
|
||||
});
|
||||
|
||||
it("can change collection to a different entity id without crashing (metabase#57438)", () => {
|
||||
const TestComponent = () => {
|
||||
const [collectionId, setCollectionId] = useState<string | null>(
|
||||
FIRST_COLLECTION_ENTITY_ID!,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>id = {collectionId}</div>
|
||||
<CollectionBrowser collectionId={collectionId} />
|
||||
|
||||
<div onClick={() => setCollectionId(SECOND_COLLECTION_ENTITY_ID!)}>
|
||||
use second collection
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
cy.intercept("GET", "/api/collection/*").as("getCollection");
|
||||
|
||||
mountSdkContent(<TestComponent />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText(`id = ${FIRST_COLLECTION_ENTITY_ID}`).should("exist");
|
||||
cy.findByText("Our analytics").should("not.exist");
|
||||
cy.findByText("Second collection").should("not.exist");
|
||||
cy.findByText("First collection").should("exist");
|
||||
|
||||
cy.findByText("use second collection").click();
|
||||
|
||||
cy.log("ensure that the collection id is updated and does not crash");
|
||||
cy.findByText(`id = ${SECOND_COLLECTION_ENTITY_ID}`).should("exist");
|
||||
cy.findByText("First collection").should("not.exist");
|
||||
cy.findByText("Second collection").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,22 @@ describe("scenarios > embedding-sdk > interactive-dashboard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show a watermark on dashcards in development mode", () => {
|
||||
cy.intercept("/api/session/properties", (req) => {
|
||||
req.continue((res) => {
|
||||
res.body["token-features"]["development-mode"] = true;
|
||||
});
|
||||
});
|
||||
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
mountSdkContent(<InteractiveDashboard dashboardId={dashboardId} />);
|
||||
});
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findAllByTestId("development-watermark").should("have.length", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading behavior for both entity IDs and number IDs (metabase#49581)", () => {
|
||||
const successTestCases = [
|
||||
{
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
InteractiveQuestion,
|
||||
type MetabaseQuestion,
|
||||
} from "@metabase/embedding-sdk-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { type ComponentProps, useState } from "react";
|
||||
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
FIRST_COLLECTION_ENTITY_ID,
|
||||
FIRST_COLLECTION_ID,
|
||||
SECOND_COLLECTION_ENTITY_ID,
|
||||
THIRD_COLLECTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import {
|
||||
@@ -65,6 +67,20 @@ describe("scenarios > embedding-sdk > interactive-question", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show a watermark in development mode", () => {
|
||||
cy.intercept("/api/session/properties", (req) => {
|
||||
req.continue((res) => {
|
||||
res.body["token-features"]["development-mode"] = true;
|
||||
});
|
||||
});
|
||||
|
||||
mountInteractiveQuestion();
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("development-watermark").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fail on aggregated question drill", () => {
|
||||
mountInteractiveQuestion();
|
||||
|
||||
@@ -410,4 +426,43 @@ describe("scenarios > embedding-sdk > interactive-question", () => {
|
||||
H.visualize();
|
||||
H.cartesianChartCircle().should("have.length", LIMIT);
|
||||
});
|
||||
|
||||
it("can change target collection to a different entity id without crashing (metabase#57438)", () => {
|
||||
const TestComponent = () => {
|
||||
const [targetCollection, setTargetCollection] = useState<string | null>(
|
||||
FIRST_COLLECTION_ENTITY_ID!,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>id = {targetCollection}</div>
|
||||
|
||||
<InteractiveQuestion
|
||||
questionId="new"
|
||||
targetCollection={targetCollection}
|
||||
onSave={() => {}}
|
||||
isSaveEnabled
|
||||
/>
|
||||
|
||||
<div
|
||||
onClick={() => setTargetCollection(SECOND_COLLECTION_ENTITY_ID!)}
|
||||
>
|
||||
use second collection
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
mountSdkContent(<TestComponent />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByText(`id = ${FIRST_COLLECTION_ENTITY_ID}`).should("exist");
|
||||
cy.findByText("use second collection").click();
|
||||
cy.findByText(`id = ${SECOND_COLLECTION_ENTITY_ID}`).should("exist");
|
||||
|
||||
cy.log("ensure that the interactive question still works");
|
||||
H.popover().findByRole("link", { name: "Orders" }).click();
|
||||
cy.findByRole("button", { name: "Visualize" }).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,8 +331,7 @@ describe("issue 51020", () => {
|
||||
createTemporaryTable();
|
||||
H.resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: "foo" });
|
||||
|
||||
cy.visit("/");
|
||||
H.newButton("Model").click();
|
||||
cy.visit("/model/new");
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use the notebook editor")
|
||||
.click();
|
||||
@@ -455,22 +454,6 @@ describe("issue 32840", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 41831", () => {
|
||||
beforeEach(() => {
|
||||
H.restore("without-models");
|
||||
cy.signInAsAdmin();
|
||||
H.setActionsEnabledForDB(SAMPLE_DB_ID);
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("new action button is hidden without models", () => {
|
||||
cy.findByRole("button", { name: "New" }).click();
|
||||
cy.findByRole("dialog").within(() => {
|
||||
cy.findByText("Action").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 32750", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -480,12 +463,7 @@ describe("issue 32750", () => {
|
||||
});
|
||||
|
||||
it("modal do not dissapear on viewport change", () => {
|
||||
cy.findByRole("button", { name: "New" }).click();
|
||||
cy.findByRole("dialog").within(() => {
|
||||
cy.findByText("Action").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("action-creator").should("be.visible");
|
||||
H.startNewAction();
|
||||
cy.viewport(320, 800);
|
||||
cy.findByTestId("action-creator").should("be.visible");
|
||||
cy.viewport(1440, 800);
|
||||
|
||||
@@ -155,36 +155,6 @@ describe(
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to create an action with the New button", () => {
|
||||
const QUERY = "UPDATE orders SET discount = {{ discount }}";
|
||||
cy.visit("/");
|
||||
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
H.popover().findByText("Action").click();
|
||||
|
||||
cy.wait("@getDatabase");
|
||||
H.fillActionQuery(QUERY);
|
||||
|
||||
cy.findByRole("dialog").within(() => {
|
||||
cy.findByText(/New Action/)
|
||||
.clear()
|
||||
.type("Discount order");
|
||||
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
});
|
||||
|
||||
H.modal().eq(1).findByText("Select a model").click();
|
||||
H.entityPickerModal().within(() => {
|
||||
cy.findByText("Order").click();
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Create" }).click();
|
||||
|
||||
cy.get("@modelId").then((modelId) => {
|
||||
cy.url().should("include", `/model/${modelId}/detail/actions`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect permissions", () => {
|
||||
// Enabling actions for sample database as well
|
||||
// to test database picker behavior in the action editor
|
||||
@@ -266,8 +236,7 @@ describe(
|
||||
|
||||
it("should display parameters for variable template tags only", () => {
|
||||
cy.visit("/");
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
H.popover().findByText("Action").click();
|
||||
H.startNewAction();
|
||||
|
||||
H.fillActionQuery("{{#1-orders-model}}");
|
||||
cy.findByLabelText("#1-orders-model").should("not.exist");
|
||||
|
||||
@@ -436,11 +436,6 @@ describe("scenarios > admin > people", () => {
|
||||
const NEW_USERS = 18;
|
||||
const NEW_TOTAL_USERS = TOTAL_USERS + NEW_USERS;
|
||||
|
||||
const waitForUserRequests = () => {
|
||||
cy.wait("@users");
|
||||
cy.wait("@memberships");
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
generateUsers(NEW_USERS);
|
||||
|
||||
@@ -453,8 +448,6 @@ describe("scenarios > admin > people", () => {
|
||||
|
||||
cy.visit("/admin/people");
|
||||
|
||||
waitForUserRequests();
|
||||
|
||||
// Total
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(`${NEW_TOTAL_USERS} people found`);
|
||||
@@ -465,11 +458,8 @@ describe("scenarios > admin > people", () => {
|
||||
assertTableRowsCount(PAGE_SIZE);
|
||||
cy.findByLabelText("Previous page").should("be.disabled");
|
||||
|
||||
// cy.findByLabelText("Next page").click();
|
||||
cy.findByTestId("next-page-btn").click();
|
||||
waitForUserRequests();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Loading...").should("not.exist");
|
||||
cy.wait("@users");
|
||||
|
||||
// Page 2
|
||||
cy.findByTestId("people-list-footer")
|
||||
@@ -479,9 +469,6 @@ describe("scenarios > admin > people", () => {
|
||||
cy.findByLabelText("Next page").should("be.disabled");
|
||||
|
||||
cy.findByLabelText("Previous page").click();
|
||||
cy.wait("@users");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Loading...").should("not.exist");
|
||||
|
||||
// Page 1
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
@@ -504,7 +491,6 @@ describe("scenarios > admin > people", () => {
|
||||
cy.findByLabelText("Previous page").should("be.disabled");
|
||||
|
||||
cy.findByLabelText("Next page").click();
|
||||
waitForUserRequests();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Loading...").should("not.exist");
|
||||
|
||||
@@ -514,7 +500,6 @@ describe("scenarios > admin > people", () => {
|
||||
cy.findByLabelText("Next page").should("be.disabled");
|
||||
|
||||
cy.findByLabelText("Previous page").click();
|
||||
cy.wait("@users");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Loading...").should("not.exist");
|
||||
|
||||
|
||||
@@ -548,9 +548,12 @@ describe("scenarios > admin > settings > email settings", () => {
|
||||
Cypress.config().baseUrl + "/admin/settings/email",
|
||||
);
|
||||
cy.findByTestId("smtp-connection-card").should("exist");
|
||||
|
||||
// Non SMTP-settings should save automatically
|
||||
cy.findByLabelText("From Address").type("mailer@metabase.test").blur();
|
||||
cy.findByLabelText("From Address")
|
||||
.clear()
|
||||
.type("mailer@metabase.test")
|
||||
.blur();
|
||||
|
||||
cy.findByLabelText("From Name").type("Sender Name").blur();
|
||||
cy.findByLabelText("Reply-To Address")
|
||||
.type("reply-to@metabase.test")
|
||||
@@ -1286,13 +1289,16 @@ describe("admin > settings > updates", () => {
|
||||
cy.signInAsAdmin();
|
||||
cy.visit("/admin/settings/updates");
|
||||
|
||||
cy.intercept("GET", "/api/session/properties", (req, res) => {
|
||||
cy.intercept("GET", "/api/session/properties", (req) => {
|
||||
req.continue((res) => {
|
||||
res.body["version-info"] = versionInfo;
|
||||
res.body.version.tag = currentVersion;
|
||||
return res.body;
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept("GET", "/api/setting/version-info", (req) => {
|
||||
req.reply(versionInfo);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the updates page", () => {
|
||||
|
||||
@@ -32,8 +32,7 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
.should("have.text", "Appearance")
|
||||
.and("not.have.descendants", ".Icon-gem");
|
||||
|
||||
cy.log("By default redirects to the branding tab");
|
||||
cy.location("pathname").should("eq", "/admin/settings/whitelabel/branding");
|
||||
cy.log("By default shows the branding tab");
|
||||
cy.findByRole("tab", { name: "Branding" }).should(
|
||||
"have.attr",
|
||||
"aria-selected",
|
||||
@@ -45,8 +44,7 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
|
||||
cy.log("Should show the upsell if the feature is missing");
|
||||
H.deleteToken();
|
||||
cy.visit("/admin/settings/whitelabel");
|
||||
cy.location("pathname").should("eq", "/admin/settings/whitelabel");
|
||||
cy.visit("/admin/settings/appearance");
|
||||
cy.findByRole("heading", { name: "Make Metabase look like you" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
@@ -70,34 +68,32 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
});
|
||||
|
||||
describe("company name", () => {
|
||||
const COMPANY_NAME = "Test Co";
|
||||
const NEW_COMPANY_NAME = "New Test Co";
|
||||
|
||||
beforeEach(() => {
|
||||
cy.log("Change company name");
|
||||
cy.visit("/admin/settings/whitelabel");
|
||||
cy.findByLabelText("Application Name").clear().type(COMPANY_NAME);
|
||||
// Helps scroll the page up in order to see "Saved" notification
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Application Name").click();
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
cy.findByLabelText("Application Name")
|
||||
.clear()
|
||||
.type(NEW_COMPANY_NAME)
|
||||
.blur();
|
||||
H.undoToast().findByText("Changes saved").should("be.visible");
|
||||
cy.findByDisplayValue(COMPANY_NAME);
|
||||
cy.log("Company name has been updated!");
|
||||
cy.findByDisplayValue(NEW_COMPANY_NAME);
|
||||
});
|
||||
|
||||
it.skip("should not show the old name in the admin panel (metabase#17043)", () => {
|
||||
cy.reload();
|
||||
|
||||
cy.findByDisplayValue(COMPANY_NAME);
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.contains(
|
||||
`These are the primary colors used in charts and throughout ${COMPANY_NAME}.`,
|
||||
);
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.contains(`The top nav bar of ${COMPANY_NAME}.`);
|
||||
|
||||
it("should not show the old name in the admin panel (metabase#17043)", () => {
|
||||
cy.visit("/admin/settings/general");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.contains(`The name used for this instance of ${COMPANY_NAME}.`);
|
||||
cy.findByTestId("site-name-setting")
|
||||
.findByText(`The name used for this instance of ${NEW_COMPANY_NAME}.`)
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("should show the new name in the main app", () => {
|
||||
cy.visit("/");
|
||||
cy.icon("gear").click();
|
||||
H.popover().findByText(`About ${NEW_COMPANY_NAME}`).click();
|
||||
H.modal()
|
||||
.findByText(`Thanks for using ${NEW_COMPANY_NAME}!`)
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -326,24 +322,22 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
it("should allow display the selected illustration on the landing page", () => {
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByRole("textbox", { name: "Landing page" }).should(
|
||||
"have.value",
|
||||
"Lighthouse",
|
||||
);
|
||||
cy.findByTestId("landing-page-illustration-setting")
|
||||
.findByDisplayValue("Lighthouse")
|
||||
.click();
|
||||
|
||||
cy.findByRole("textbox", { name: "Landing page" }).click();
|
||||
H.selectDropdown().findByText("Custom").click();
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByTestId("landing-page-illustration-setting")
|
||||
.findByText("logo.jpeg")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByTestId("landing-page-illustration-setting").within(() => {
|
||||
cy.findByTestId("file-input").selectFile(
|
||||
{
|
||||
contents: "e2e/support/assets/logo.jpeg",
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{ force: true },
|
||||
);
|
||||
cy.findByText("logo.jpeg").should("be.visible");
|
||||
});
|
||||
H.undoToast().findByText("Changes saved").should("be.visible");
|
||||
|
||||
cy.readFile("e2e/support/assets/logo.jpeg", "base64").then(
|
||||
@@ -361,7 +355,9 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
cy.log("test no illustration");
|
||||
cy.visit("/admin/settings/whitelabel/conceal-metabase");
|
||||
|
||||
cy.findByLabelText("Landing page").click();
|
||||
cy.findByTestId("landing-page-illustration-setting")
|
||||
.findByDisplayValue("Custom")
|
||||
.click();
|
||||
H.selectDropdown().findByText("No illustration").click();
|
||||
|
||||
cy.visit("/");
|
||||
@@ -532,21 +528,21 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
|
||||
describe("loading message", () => {
|
||||
it("should update loading message", () => {
|
||||
cy.visit("/question/" + ORDERS_QUESTION_ID);
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Doing science...");
|
||||
cy.intercept("PUT", "/api/setting/loading-message").as(
|
||||
"putLoadingMessage",
|
||||
);
|
||||
const messages = [
|
||||
"Loading results...",
|
||||
"Doing science...",
|
||||
"Running query...",
|
||||
];
|
||||
|
||||
const runningQueryMessage = "Running query...";
|
||||
changeLoadingMessage(runningQueryMessage);
|
||||
cy.visit("/question/" + ORDERS_QUESTION_ID);
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(runningQueryMessage);
|
||||
|
||||
const loadingResultsMessage = "Loading results...";
|
||||
changeLoadingMessage(loadingResultsMessage);
|
||||
cy.visit("/question/" + ORDERS_QUESTION_ID);
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(loadingResultsMessage);
|
||||
messages.forEach((message) => {
|
||||
changeLoadingMessage(message);
|
||||
// can't use visitQuestion helper because it waits for loading to be finished
|
||||
cy.visit(`/question/${ORDERS_QUESTION_ID}`);
|
||||
cy.findByTestId("query-builder-main").findByText(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,14 +569,31 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
cy.log("Change Application Font");
|
||||
cy.signInAsAdmin();
|
||||
setApplicationFontTo(font);
|
||||
});
|
||||
|
||||
it("should apply correct font", () => {
|
||||
setApplicationFontTo(font);
|
||||
cy.signInAsNormalUser();
|
||||
cy.visit("/");
|
||||
cy.get("body").should("have.css", "font-family", `"${font}", sans-serif`);
|
||||
});
|
||||
|
||||
it("should be able to make multiple font changes (metabase#45486)", () => {
|
||||
cy.intercept("PUT", "/api/setting/application-font").as("saveFont");
|
||||
const fonts = ["Lora", "Merriweather", "Montserrat", "Lato"];
|
||||
cy.visit("/admin/settings/whitelabel/branding");
|
||||
|
||||
fonts.forEach((newFont) => {
|
||||
cy.findByLabelText("Font").click();
|
||||
H.selectDropdown().findByText(newFont).click();
|
||||
cy.wait("@saveFont");
|
||||
cy.get("body").should(
|
||||
"have.css",
|
||||
"font-family",
|
||||
`${newFont}, sans-serif`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Help link", () => {
|
||||
@@ -619,6 +632,7 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
|
||||
getHelpLinkCustomDestinationInput()
|
||||
.should("have.focus")
|
||||
.clear()
|
||||
.type("https://example.org/custom-destination")
|
||||
.blur();
|
||||
|
||||
@@ -701,7 +715,7 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
.findByText("Please make sure this is a valid URL")
|
||||
.should("exist");
|
||||
|
||||
getHelpLinkCustomDestinationInput().type("example");
|
||||
getHelpLinkCustomDestinationInput().type("example").blur();
|
||||
|
||||
H.main()
|
||||
.findByText("Please make sure this is a valid URL")
|
||||
@@ -761,8 +775,9 @@ describe("formatting > whitelabel", { tags: "@EE" }, () => {
|
||||
|
||||
function changeLoadingMessage(message) {
|
||||
cy.visit("/admin/settings/whitelabel");
|
||||
cy.findByLabelText("Loading message").click();
|
||||
cy.findByText(message).click();
|
||||
cy.findByLabelText("Loading Message").click();
|
||||
H.selectDropdown().findByText(message).click();
|
||||
cy.wait("@putLoadingMessage");
|
||||
}
|
||||
|
||||
function setApplicationFontTo(font) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FIRST_COLLECTION_ENTITY_ID,
|
||||
FIRST_COLLECTION_ID,
|
||||
ORDERS_QUESTION_ID,
|
||||
READ_ONLY_PERSONAL_COLLECTION_ID,
|
||||
SECOND_COLLECTION_ID,
|
||||
THIRD_COLLECTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
@@ -29,6 +30,94 @@ describe("scenarios > collection defaults", () => {
|
||||
cy.intercept("GET", "/api/collection/*/items?**").as("getCollectionItems");
|
||||
});
|
||||
|
||||
H.describeWithSnowplow("new collection button", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.enableTracking();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("should show the new collection button in the root collection", () => {
|
||||
cy.intercept("POST", "/api/collection").as("createCollection");
|
||||
|
||||
visitRootCollection();
|
||||
cy.findByTestId("collection-menu")
|
||||
.findByLabelText("Create a new collection")
|
||||
.click();
|
||||
|
||||
cy.log("Track the collection initiation from the header");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "plus_button_clicked",
|
||||
triggered_from: "collection-header",
|
||||
});
|
||||
cy.findByTestId("new-collection-modal").within(() => {
|
||||
cy.findByLabelText("Name").type("MCL");
|
||||
cy.findByTestId("collection-picker-button").should(
|
||||
"contain",
|
||||
"Our analytics",
|
||||
);
|
||||
cy.button("Create").click();
|
||||
cy.wait("@createCollection");
|
||||
});
|
||||
cy.location("pathname").should("match", /^\/collection\/\d+-mcl/);
|
||||
cy.findByTestId("collection-empty-state").should("be.visible");
|
||||
|
||||
cy.log(
|
||||
"Newly created collection should also have the new collection button",
|
||||
);
|
||||
cy.findByTestId("collection-menu")
|
||||
.findByLabelText("Create a new collection")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("Track the collection initiation from the main navbar");
|
||||
H.navigationSidebar().findByLabelText("Create a new collection").click();
|
||||
cy.findByTestId("new-collection-modal").should("be.visible");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "plus_button_clicked",
|
||||
triggered_from: "collection-nav",
|
||||
});
|
||||
});
|
||||
|
||||
it("user without curate permissions should only be allowed to create a new collection inside their personal collection scope", () => {
|
||||
cy.intercept("POST", "/api/collection").as("createCollection");
|
||||
|
||||
cy.signIn("readonly");
|
||||
visitRootCollection();
|
||||
cy.findByTestId("collection-menu")
|
||||
.findByLabelText("Create a new collection")
|
||||
.should("not.exist");
|
||||
|
||||
H.visitCollection(READ_ONLY_PERSONAL_COLLECTION_ID);
|
||||
|
||||
cy.findByTestId("collection-menu")
|
||||
.findByLabelText("Create a new collection")
|
||||
.click();
|
||||
cy.findByTestId("new-collection-modal").within(() => {
|
||||
cy.findByLabelText("Name").type("sub");
|
||||
cy.findByTestId("collection-picker-button").should(
|
||||
"contain",
|
||||
"Read Only Tableton's Personal Collection",
|
||||
);
|
||||
cy.button("Create").click();
|
||||
cy.wait("@createCollection");
|
||||
});
|
||||
cy.location("pathname").should("match", /^\/collection\/\d+-sub/);
|
||||
cy.findByTestId("collection-empty-state").should("be.visible");
|
||||
|
||||
cy.log(
|
||||
"Newly created personal sub-collection should also have the new collection button",
|
||||
);
|
||||
cy.findByTestId("collection-menu")
|
||||
.findByLabelText("Create a new collection")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("new collection modal", () => {
|
||||
it("should be usable on small screens", () => {
|
||||
const COLLECTIONS_COUNT = 5;
|
||||
@@ -43,11 +132,7 @@ describe("scenarios > collection defaults", () => {
|
||||
|
||||
cy.viewport(800, 500);
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("New").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Collection").click();
|
||||
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
cy.findByPlaceholderText("My new fantastic collection").type(
|
||||
"Test collection",
|
||||
@@ -854,11 +939,7 @@ describe("scenarios > collection defaults", () => {
|
||||
|
||||
it("should create new collections within the current collection", () => {
|
||||
H.visitCollection(THIRD_COLLECTION_ID);
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Collection").click();
|
||||
});
|
||||
H.startNewCollectionFromSidebar();
|
||||
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
cy.findByText("Collection it's saved in").should("be.visible");
|
||||
|
||||
@@ -416,6 +416,31 @@ describe("collection permissions", () => {
|
||||
);
|
||||
cy.findByTestId("permission-table");
|
||||
});
|
||||
|
||||
it("should show the new collection button in a sidebar even to users without collection access", () => {
|
||||
cy.intercept("POST", "/api/collection").as("createCollection");
|
||||
|
||||
cy.signIn("nocollection");
|
||||
cy.visit("/");
|
||||
H.navigationSidebar()
|
||||
.findByLabelText("Create a new collection")
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.findByTestId("new-collection-modal").within(() => {
|
||||
cy.findByLabelText("Name").type("Foo");
|
||||
cy.log(
|
||||
"The only possible location to save the new collection is this user's personal collection",
|
||||
);
|
||||
cy.findByTestId("collection-picker-button").should(
|
||||
"contain",
|
||||
"No Collection Tableton's Personal Collection",
|
||||
);
|
||||
cy.button("Create").click();
|
||||
cy.wait("@createCollection");
|
||||
});
|
||||
cy.location("pathname").should("match", /^\/collection\/\d+-foo/);
|
||||
});
|
||||
});
|
||||
|
||||
function clickButton(name) {
|
||||
|
||||
@@ -56,10 +56,9 @@ describe("personal collections", () => {
|
||||
cy.findAllByRole("tree")
|
||||
.contains("Your personal collection")
|
||||
.should("be.visible");
|
||||
H.navigationSidebar().within(() => {
|
||||
cy.icon("ellipsis").click();
|
||||
});
|
||||
H.popover().findByText("Other users' personal collections").click();
|
||||
H.navigationSidebar()
|
||||
.findByLabelText("Other users' personal collections")
|
||||
.click();
|
||||
cy.location("pathname").should("eq", "/collection/users");
|
||||
cy.findByTestId("browsercrumbs").findByText(/All personal collections/i);
|
||||
Object.values(USERS).forEach((user) => {
|
||||
@@ -75,12 +74,13 @@ describe("personal collections", () => {
|
||||
parent_id: ADMIN_PERSONAL_COLLECTION_ID,
|
||||
});
|
||||
|
||||
// Go to admin's personal collection
|
||||
cy.visit("/collection/root");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Your personal collection").click();
|
||||
H.visitCollection(ADMIN_PERSONAL_COLLECTION_ID);
|
||||
|
||||
cy.log(
|
||||
"Make sure it's not possible to edit personal collection's permissions",
|
||||
);
|
||||
H.getCollectionActions().within(() => {
|
||||
cy.icon("info").should("exist");
|
||||
cy.icon("ellipsis").should("not.exist");
|
||||
});
|
||||
|
||||
@@ -96,11 +96,14 @@ describe("personal collections", () => {
|
||||
|
||||
// Go to the newly created sub-collection "Foo"
|
||||
H.navigationSidebar().findByText("Foo").click();
|
||||
|
||||
// It should be possible to edit sub-collections' details, but not its permissions
|
||||
cy.findByDisplayValue("Foo").should("be.enabled");
|
||||
|
||||
cy.log(
|
||||
"Other menu options exist, but editing permissions is not possible",
|
||||
);
|
||||
H.openCollectionMenu();
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Move to trash").should("be.visible");
|
||||
cy.findByText("Edit permissions").should("not.exist");
|
||||
});
|
||||
|
||||
@@ -112,9 +115,10 @@ describe("personal collections", () => {
|
||||
// });
|
||||
|
||||
// Go to random user's personal collection
|
||||
cy.visit(`/collection/${NO_DATA_PERSONAL_COLLECTION_ID}`);
|
||||
H.visitCollection(NO_DATA_PERSONAL_COLLECTION_ID);
|
||||
|
||||
H.getCollectionActions().within(() => {
|
||||
cy.icon("info").should("exist");
|
||||
cy.icon("ellipsis").should("not.exist");
|
||||
});
|
||||
});
|
||||
@@ -138,8 +142,7 @@ describe("personal collections", () => {
|
||||
cy.signIn(user);
|
||||
|
||||
cy.visit("/collection/root");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Your personal collection").click();
|
||||
H.navigationSidebar().findByText("Your personal collection").click();
|
||||
|
||||
// Create initial collection inside the personal collection and navigate to it
|
||||
addNewCollection("Foo");
|
||||
@@ -161,11 +164,9 @@ describe("personal collections", () => {
|
||||
);
|
||||
|
||||
H.openCollectionMenu();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
H.popover().within(() => cy.findByText("Move to trash").click());
|
||||
H.modal().findByRole("button", { name: "Move to trash" }).click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Trashed collection");
|
||||
H.popover().findByText("Move to trash").click();
|
||||
H.modal().button("Move to trash").click();
|
||||
cy.findByTestId("toast-undo").should("contain", "Trashed collection");
|
||||
cy.get("@sidebar").findByText("Foo").should("not.exist");
|
||||
});
|
||||
});
|
||||
@@ -174,7 +175,7 @@ describe("personal collections", () => {
|
||||
});
|
||||
|
||||
function addNewCollection(name) {
|
||||
H.newButton("Collection").click();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByPlaceholderText("My new fantastic collection").type(name, {
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,10 @@ type CastTestCase = {
|
||||
filterOperator: string;
|
||||
filterValue: string;
|
||||
expectedRowCount: number;
|
||||
expectedTableData?: {
|
||||
columns: string[];
|
||||
firstRows: string[][];
|
||||
};
|
||||
};
|
||||
|
||||
const TEXT_TEST_CASES: CastTestCase[] = [
|
||||
@@ -113,6 +117,28 @@ const DATE_TEST_CASES: CastTestCase[] = [
|
||||
filterValue: "March 15, 2025",
|
||||
expectedRowCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Datetime",
|
||||
expression: "date([Created At])",
|
||||
filterOperator: "Before",
|
||||
filterValue: "April 27, 2016",
|
||||
expectedRowCount: 1,
|
||||
expectedTableData: {
|
||||
columns: ["ID", "Datetime"],
|
||||
firstRows: [["36", "April 26, 2016"]],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DatetimeExpression",
|
||||
expression: "date(dateTimeAdd([Created At], 1, 'day'))",
|
||||
filterOperator: "Before",
|
||||
filterValue: "April 28, 2016",
|
||||
expectedRowCount: 1,
|
||||
expectedTableData: {
|
||||
columns: ["ID", "DatetimeExpression"],
|
||||
firstRows: [["36", "April 27, 2016"]],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const FLOAT_TEST_CASES: CastTestCase[] = [
|
||||
@@ -219,6 +245,12 @@ function testFilterWithExpressions(
|
||||
addFilter(testCase);
|
||||
H.visualize();
|
||||
H.assertQueryBuilderRowCount(testCase.expectedRowCount);
|
||||
|
||||
if (testCase.expectedTableData) {
|
||||
// @ts-expect-error: assertTableData is not typed
|
||||
H.assertTableData(testCase.expectedTableData);
|
||||
}
|
||||
|
||||
H.openNotebook();
|
||||
removeCustomColumn(testCase);
|
||||
});
|
||||
|
||||
@@ -1797,7 +1797,6 @@ describe("issue 48878", () => {
|
||||
// Create a dummy model so that GET /api/search does not return the model want to test.
|
||||
// If we don't do this, GET /api/search will return and put card object with dataset_query
|
||||
// attribute in the redux store (entity framework) which would prevent the issue from happening.
|
||||
cy.visit("/model/new");
|
||||
createModel({
|
||||
name: "Dummy model",
|
||||
query: "select 1",
|
||||
@@ -1805,8 +1804,6 @@ describe("issue 48878", () => {
|
||||
|
||||
cy.log("create model");
|
||||
|
||||
cy.button("New").click();
|
||||
H.popover().findByText("Model").click();
|
||||
createModel({
|
||||
name: "SQL Model",
|
||||
query: "select * from orders limit 5",
|
||||
@@ -1863,6 +1860,7 @@ describe("issue 48878", () => {
|
||||
}
|
||||
|
||||
function createModel({ name, query }) {
|
||||
cy.visit("/model/new");
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use a native query")
|
||||
.click();
|
||||
|
||||
@@ -187,6 +187,12 @@ describe("scenarios > x-rays", { tags: "@slow" }, () => {
|
||||
cy.findAllByTestId("dashcard").contains("18,760");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("How these transactions are distributed");
|
||||
|
||||
H.openNavigationSidebar();
|
||||
|
||||
H.navigationSidebar()
|
||||
.findByRole("link", { name: /Automatically generated dashboards/i })
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should start loading cards from top to bottom", () => {
|
||||
|
||||
120
e2e/test/scenarios/metabot/metabot.cy.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
assertChatVisibility,
|
||||
closeMetabotViaCloseButton,
|
||||
closeMetabotViaShortcutKey,
|
||||
metabotChatInput,
|
||||
mockMetabotResponse,
|
||||
openMetabotViaNewMenu,
|
||||
openMetabotViaShortcutKey,
|
||||
popover,
|
||||
restore,
|
||||
sendMetabotMessage,
|
||||
setTokenFeatures,
|
||||
userMessages,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
describe("Metabot UI", () => {
|
||||
beforeEach(() => {
|
||||
restore();
|
||||
cy.signInAsAdmin();
|
||||
cy.intercept("POST", "/api/ee/metabot-v3/agent").as("agentReq");
|
||||
cy.intercept("GET", "/api/session/properties").as("sessionProperties");
|
||||
});
|
||||
|
||||
describe("OSS", { tags: "@OSS" }, () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.wait("@sessionProperties");
|
||||
});
|
||||
|
||||
it("should not be available in OSS", () => {
|
||||
openMetabotViaShortcutKey(false);
|
||||
assertChatVisibility("not.visible");
|
||||
cy.findByLabelText("Navigation bar").within(() => {
|
||||
cy.findByText("New").click();
|
||||
});
|
||||
popover().findByText("Metabot request").should("not.exist");
|
||||
assertChatVisibility("not.visible");
|
||||
});
|
||||
});
|
||||
|
||||
// token feature has been disabled so skipping for now - there's currently decent coverage for UI related things in our unit tests
|
||||
describe.skip("EE", () => {
|
||||
beforeEach(() => {
|
||||
setTokenFeatures("all");
|
||||
cy.visit("/");
|
||||
cy.wait("@sessionProperties");
|
||||
});
|
||||
|
||||
// FIXME: shortcut keys aren't working in CI only, but work locally
|
||||
it.skip("should be able to be opened and closed", () => {
|
||||
openMetabotViaNewMenu();
|
||||
closeMetabotViaCloseButton();
|
||||
openMetabotViaShortcutKey();
|
||||
closeMetabotViaShortcutKey();
|
||||
});
|
||||
|
||||
it("should allow a user to send a message to the agent and handle successful or failed responses", () => {
|
||||
openMetabotViaNewMenu();
|
||||
userMessages().should("not.exist");
|
||||
|
||||
mockMetabotResponse({
|
||||
statusCode: 200,
|
||||
delay: 100, // small delay to detect loading state
|
||||
body: whoIsYourFavoriteResponse,
|
||||
});
|
||||
sendMetabotMessage("Who is your favorite?");
|
||||
|
||||
metabotChatInput()
|
||||
.should("have.attr", "placeholder")
|
||||
.and("eq", "Doing science...");
|
||||
userMessages()
|
||||
.should("exist")
|
||||
.should("have.text", "You are... but don't tell anyone!");
|
||||
|
||||
mockMetabotResponse({
|
||||
statusCode: 500,
|
||||
body: whoIsYourFavoriteResponse,
|
||||
});
|
||||
sendMetabotMessage("Who is your favorite?");
|
||||
userMessages()
|
||||
.should("exist")
|
||||
.should("have.text", "I'm currently offline, try again later.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const whoIsYourFavoriteResponse = {
|
||||
reactions: [
|
||||
{
|
||||
type: "metabot.reaction/message",
|
||||
message: "You are... but don't tell anyone!",
|
||||
},
|
||||
],
|
||||
history: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Who is your favorite?",
|
||||
},
|
||||
{
|
||||
content: "",
|
||||
role: "assistant",
|
||||
"tool-calls": [
|
||||
{
|
||||
id: "call_PVmnR8mcnYFF2AmqupSKzJDh",
|
||||
name: "who-is-your-favorite",
|
||||
arguments: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "tool",
|
||||
"tool-call-id": "call_PVmnR8mcnYFF2AmqupSKzJDh",
|
||||
content: "You are... but don't tell anyone!",
|
||||
},
|
||||
{
|
||||
content: "You are... but don't tell anyone!",
|
||||
role: "assistant",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
const { H } = cy;
|
||||
import { USERS } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import {
|
||||
FIRST_COLLECTION_ID,
|
||||
ORDERS_MODEL_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import type { StructuredQuestionDetails } from "e2e/support/helpers";
|
||||
|
||||
const { ORDERS_ID, ORDERS, PRODUCTS_ID, PRODUCTS } = SAMPLE_DATABASE;
|
||||
|
||||
type StructuredQuestionDetailsWithName = H.StructuredQuestionDetails & {
|
||||
type StructuredQuestionDetailsWithName = StructuredQuestionDetails & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
@@ -117,6 +119,35 @@ describe("scenarios > browse > metrics", () => {
|
||||
).should("be.visible");
|
||||
cy.findByText("Create metric").should("not.exist");
|
||||
});
|
||||
|
||||
cy.log("New metric header button should not show either");
|
||||
cy.findByTestId("browse-metrics-header")
|
||||
.findByLabelText("Create a new metric")
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
it("user without a collection access should still be able to create and save a metric in his own personal collection", () => {
|
||||
cy.intercept("POST", "/api/card").as("createMetric");
|
||||
|
||||
cy.signIn("nocollection");
|
||||
cy.visit("/browse/metrics");
|
||||
|
||||
cy.findByTestId("browse-metrics-header")
|
||||
.findByLabelText("Create a new metric")
|
||||
.click();
|
||||
cy.findByTestId("entity-picker-modal").findByText("People").click();
|
||||
cy.findByTestId("edit-bar")
|
||||
.should("contain", "New metric")
|
||||
.button("Save")
|
||||
.click();
|
||||
H.modal()
|
||||
.should("contain", "Save metric")
|
||||
.and("contain", H.getPersonalCollectionName(USERS["nocollection"]))
|
||||
.button("Save")
|
||||
.click();
|
||||
|
||||
cy.wait("@createMetric");
|
||||
cy.location("pathname").should("match", /^\/metric\/\d+-.*$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -73,27 +73,6 @@ describe("scenarios > metrics > editing", () => {
|
||||
});
|
||||
|
||||
describe("organization", () => {
|
||||
it("should be able to create a new metric from the homepage", () => {
|
||||
cy.visit("/");
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
H.popover().findByText("Metric").click();
|
||||
H.entityPickerModal().within(() => {
|
||||
H.entityPickerModalTab("Tables").click();
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
addAggregation({ operatorName: "Count of rows" });
|
||||
saveMetric({ name: "my new metric" });
|
||||
verifyScalarValue("18,760");
|
||||
|
||||
cy.log(
|
||||
"newly created metric should be visible in recents (metabase#44223)",
|
||||
);
|
||||
H.appBar()
|
||||
.findByText(/search/i)
|
||||
.click();
|
||||
H.commandPalette().findByText("my new metric").should("be.visible");
|
||||
});
|
||||
|
||||
it("should be able to rename a metric", () => {
|
||||
const newTitle = "New metric name";
|
||||
H.createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) => {
|
||||
@@ -135,9 +114,11 @@ describe("scenarios > metrics > editing", () => {
|
||||
});
|
||||
|
||||
it("should pin new metrics automatically", () => {
|
||||
cy.visit("/");
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
H.popover().findByText("Metric").click();
|
||||
cy.visit("/browse/metrics");
|
||||
cy.findByTestId("browse-metrics-header")
|
||||
.findByLabelText("Create a new metric")
|
||||
.click();
|
||||
|
||||
H.entityPickerModal().within(() => {
|
||||
H.entityPickerModalTab("Tables").click();
|
||||
cy.findByText("Orders").click();
|
||||
@@ -571,17 +552,6 @@ function addNumberBetweenFilter({ tableName, columnName, minValue, maxValue }) {
|
||||
});
|
||||
}
|
||||
|
||||
function addAggregation({ operatorName, columnName, stageIndex }) {
|
||||
startNewAggregation({ stageIndex });
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText(operatorName).click();
|
||||
if (columnName) {
|
||||
cy.findByText(columnName).click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addBreakout({ tableName, columnName, bucketName, stageIndex }) {
|
||||
startNewBreakout({ stageIndex });
|
||||
if (tableName) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const { H } = cy;
|
||||
import { THIRD_COLLECTION_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const modelName = "A name";
|
||||
import { USERS } from "e2e/support/cypress_data";
|
||||
|
||||
describe("scenarios > models > create", () => {
|
||||
beforeEach(() => {
|
||||
@@ -10,8 +8,8 @@ describe("scenarios > models > create", () => {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
});
|
||||
|
||||
it("creates a native query model via the New button", () => {
|
||||
cy.visit("/");
|
||||
it("creates a native query model", () => {
|
||||
const modelName = "m42";
|
||||
|
||||
navigateToNewModelPage();
|
||||
|
||||
@@ -25,68 +23,81 @@ describe("scenarios > models > create", () => {
|
||||
// Clicking on metadata should not work until we run a query
|
||||
cy.findByTestId("editor-tabs-metadata").should("be.disabled");
|
||||
|
||||
H.NativeEditor.focus().type("select * from ORDERS");
|
||||
|
||||
H.NativeEditor.focus().type("select 42");
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.findByPlaceholderText("What is the name of your model?").type(modelName);
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Save").click();
|
||||
cy.findByTestId("save-question-modal").within(() => {
|
||||
cy.findByLabelText("Name").type(modelName);
|
||||
cy.button("Save").click();
|
||||
});
|
||||
|
||||
// After saving, we land on view mode for the model
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Summarize");
|
||||
cy.location("pathname").should("match", /^\/model\/\d+-.*$/);
|
||||
cy.findByTestId("question-row-count").should("have.text", "Showing 1 row");
|
||||
|
||||
checkIfPinned();
|
||||
checkIfPinned(modelName);
|
||||
});
|
||||
|
||||
it("suggest the currently viewed collection when saving a new native query", () => {
|
||||
H.visitCollection(THIRD_COLLECTION_ID);
|
||||
// This covers creating a GUI model from the browse page + nocollection permissions (2 in 1)
|
||||
it("user without a collection access should still be able to create and save a model in his own personal collection", () => {
|
||||
cy.intercept("POST", "/api/card").as("createModel");
|
||||
|
||||
navigateToNewModelPage();
|
||||
H.NativeEditor.focus().type("select 1");
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.wait("@dataset");
|
||||
cy.signIn("nocollection");
|
||||
cy.visit("/browse/models");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").within(() => {
|
||||
cy.contains("button", "Save").click();
|
||||
});
|
||||
cy.findByTestId("save-question-modal").within(() => {
|
||||
cy.findByLabelText(/Where do you want to save this/).should(
|
||||
"have.text",
|
||||
"Third collection",
|
||||
);
|
||||
});
|
||||
cy.findByLabelText("Create a new model").click();
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use the notebook editor")
|
||||
.click();
|
||||
H.entityPickerModal().findByText("People").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.findByTestId("save-question-modal")
|
||||
.should("contain", "Save model")
|
||||
.and("contain", H.getPersonalCollectionName(USERS["nocollection"]))
|
||||
.button("Save")
|
||||
.click();
|
||||
cy.wait("@createModel");
|
||||
cy.location("pathname").should("match", /^\/model\/\d+-.*$/);
|
||||
});
|
||||
|
||||
it("suggest the last accessed collection when saving a new structured query model", () => {
|
||||
H.visitCollection(THIRD_COLLECTION_ID);
|
||||
|
||||
navigateToNewModelPage("structured");
|
||||
|
||||
H.entityPickerModal().within(() => {
|
||||
H.entityPickerModalTab("Tables").click();
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").within(() => {
|
||||
cy.contains("button", "Save").click();
|
||||
});
|
||||
it("should be able to create a new native model from the browse page", () => {
|
||||
cy.intercept("POST", "/api/dataset").as("previewModel");
|
||||
cy.intercept("POST", "/api/card").as("createModel");
|
||||
|
||||
cy.visit("/browse/models");
|
||||
cy.findByLabelText("Create a new model").click();
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use a native query")
|
||||
.click();
|
||||
H.NativeEditor.focus().type("select 42");
|
||||
cy.findByTestId("native-query-editor-sidebar")
|
||||
.findByLabelText("Get Answer")
|
||||
.click();
|
||||
cy.wait("@previewModel");
|
||||
cy.findByTestId("visualization-root").should("contain", "42");
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.findByTestId("save-question-modal").within(() => {
|
||||
cy.findByLabelText(/Where do you want to save this/).should(
|
||||
"have.text",
|
||||
"Third collection",
|
||||
);
|
||||
cy.findByLabelText("Name").type("m42");
|
||||
cy.button("Save").click();
|
||||
});
|
||||
cy.wait("@createModel");
|
||||
cy.location("pathname").should("match", /^\/model\/\d+-.*$/);
|
||||
});
|
||||
|
||||
it("should not be possible to initiate a new model creation without native permissions", () => {
|
||||
cy.signIn("nosql");
|
||||
cy.visit("/browse/models");
|
||||
cy.findByTestId("browse-models-header").within(() => {
|
||||
cy.findByRole("heading").should("contain", "Models").and("be.visible");
|
||||
cy.findByLabelText("Create a new model").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function navigateToNewModelPage(queryType = "native") {
|
||||
H.newButton("Model").click();
|
||||
cy.visit("/model/new");
|
||||
if (queryType === "structured") {
|
||||
cy.findByText("Use the notebook editor").click();
|
||||
} else {
|
||||
@@ -94,13 +105,14 @@ function navigateToNewModelPage(queryType = "native") {
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfPinned() {
|
||||
H.visitCollection("root");
|
||||
function checkIfPinned(modelName) {
|
||||
cy.findByTestId("app-bar").findByText("Our analytics").click();
|
||||
cy.location("pathname").should("eq", "/collection/root");
|
||||
|
||||
cy.findByText(modelName)
|
||||
.closest("a")
|
||||
.find(".Icon-ellipsis")
|
||||
.click({ force: true });
|
||||
|
||||
cy.findByText("Unpin").should("be.visible");
|
||||
H.popover().findByText("Unpin").should("be.visible");
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ describe("issue 19180", () => {
|
||||
cy.wait("@cardQuery");
|
||||
cy.button("Cancel").click();
|
||||
H.tableInteractive();
|
||||
cy.findByText("Query results will appear here.").should("not.exist");
|
||||
cy.findByText("Here's where your results will appear").should(
|
||||
"not.exist",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -929,27 +931,22 @@ describe("issue 28971", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
cy.intercept("POST", "/api/card").as("createCard");
|
||||
cy.intercept("POST", "/api/card").as("createModel");
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
});
|
||||
|
||||
it("should be able to filter a newly created model (metabase#28971)", () => {
|
||||
cy.visit("/");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("New").click();
|
||||
H.popover().findByText("Model").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Use the notebook editor").click();
|
||||
H.startNewModel();
|
||||
H.entityPickerModal().within(() => {
|
||||
H.entityPickerModalTab("Tables").click();
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
cy.button("Save").click();
|
||||
cy.findByTestId("save-question-modal").within((modal) => {
|
||||
cy.findByText("Save").click();
|
||||
});
|
||||
cy.wait("@createCard");
|
||||
cy.findByTestId("run-button").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.findByTestId("save-question-modal").button("Save").click();
|
||||
cy.wait("@createModel");
|
||||
|
||||
H.filter();
|
||||
H.popover().within(() => {
|
||||
@@ -958,50 +955,12 @@ describe("issue 28971", () => {
|
||||
cy.button("Apply filter").click();
|
||||
});
|
||||
cy.wait("@dataset");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Quantity is equal to 20").should("exist");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Showing 4 rows").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 28971", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
cy.intercept("POST", "/api/card").as("createCard");
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
});
|
||||
|
||||
it("should be able to filter a newly created model (metabase#28971)", () => {
|
||||
cy.visit("/");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("New").click();
|
||||
H.popover().findByText("Model").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Use the notebook editor").click();
|
||||
H.entityPickerModal().within(() => {
|
||||
H.entityPickerModalTab("Tables").click();
|
||||
cy.findByText("Orders").click();
|
||||
});
|
||||
cy.button("Save").click();
|
||||
cy.findByTestId("save-question-modal").within((modal) => {
|
||||
cy.findByText("Save").click();
|
||||
});
|
||||
cy.wait("@createCard");
|
||||
|
||||
H.filter();
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Quantity").click();
|
||||
cy.findByText("20").click();
|
||||
cy.button("Apply filter").click();
|
||||
});
|
||||
cy.wait("@dataset");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Quantity is equal to 20").should("exist");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Showing 4 rows").should("exist");
|
||||
cy.findByTestId("filter-pill").should(
|
||||
"have.text",
|
||||
"Quantity is equal to 20",
|
||||
);
|
||||
cy.findByTestId("question-row-count").should("have.text", "Showing 4 rows");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -783,8 +783,7 @@ describe("issue 33844", () => {
|
||||
}
|
||||
|
||||
it("should show hidden PKs in model metadata editor and object details after creating a model (metabase#33844)", () => {
|
||||
cy.visit("/");
|
||||
H.newButton("Model").click();
|
||||
cy.visit("/model/new");
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use the notebook editor")
|
||||
.click();
|
||||
@@ -1058,10 +1057,9 @@ describe("issue 34514", () => {
|
||||
cy.intercept("GET", "/api/database/*/schema/*").as("fetchTables");
|
||||
cy.intercept("GET", "/api/database/*").as("fetchDatabase");
|
||||
|
||||
cy.visit("/");
|
||||
// It's important to navigate via UI so that there are
|
||||
// enough entries in the browser history to go back to.
|
||||
H.newButton("Model").click();
|
||||
cy.visit("/model/new");
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText("Use the notebook editor")
|
||||
.click();
|
||||
@@ -1134,7 +1132,9 @@ describe("issue 34514", () => {
|
||||
H.tableInteractive().should("not.exist");
|
||||
cy.findByTestId("query-visualization-root").within(() => {
|
||||
cy.findByText("We're experiencing server issues").should("not.exist");
|
||||
cy.findByText("Query results will appear here.").should("be.visible");
|
||||
cy.findByText("Here's where your results will appear").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1435,6 +1435,21 @@ describe("issue 51925", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 53649", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should not get caught in an infinite loop when opening the native editor (metabase#53649)", () => {
|
||||
H.startNewNativeModel();
|
||||
|
||||
// If the app freezes, this won't work
|
||||
H.NativeEditor.type("select 1");
|
||||
H.NativeEditor.get().should("contain", "select 1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 56698", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -1461,3 +1476,34 @@ describe("issue 56698", () => {
|
||||
H.assertQueryBuilderRowCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 56775", () => {
|
||||
const MODEL_NAME = "Model 56775";
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
H.createQuestion(
|
||||
{
|
||||
type: "model",
|
||||
name: MODEL_NAME,
|
||||
query: {
|
||||
"source-table": PRODUCTS_ID,
|
||||
},
|
||||
},
|
||||
{ visitQuestion: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the correct query after using the back button in a model (metabase#56775)", () => {
|
||||
H.openNotebook();
|
||||
cy.button("Visualize").click();
|
||||
|
||||
cy.go("back");
|
||||
H.openQuestionActions("Edit query definition");
|
||||
|
||||
cy.log("verify that the model definition is visible");
|
||||
H.getNotebookStep("data").findByText(MODEL_NAME).should("not.exist");
|
||||
H.getNotebookStep("data").findByText("Products").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -948,7 +948,7 @@ describe("issue 27257", () => {
|
||||
it("should not drop numeric filter widget value on refresh even if it's zero (metabase#27257)", () => {
|
||||
cy.reload();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Query results will appear here.");
|
||||
cy.findByText("Here's where your results will appear");
|
||||
cy.findByDisplayValue("0");
|
||||
});
|
||||
});
|
||||
|
||||
40
e2e/test/scenarios/native/ai-sql-fixer.cy.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const FIX_MESSAGE = "Fixes applied. Run your query to view results.";
|
||||
|
||||
describe.skip("scenarios > native > ai sql fixer", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.setTokenFeatures("all");
|
||||
cy.intercept("POST", "/api/ee/ai-sql-fixer/fix").as("fixSql");
|
||||
});
|
||||
|
||||
it("should be able to fix SQL with AI", () => {
|
||||
H.visitQuestionAdhoc({
|
||||
display: "table",
|
||||
dataset_query: {
|
||||
database: SAMPLE_DB_ID,
|
||||
type: "native",
|
||||
native: {
|
||||
query: "SELECT1 * FROM ORDERS",
|
||||
},
|
||||
},
|
||||
});
|
||||
cy.wait("@fixSql");
|
||||
cy.findByTestId("query-visualization-root").within(() => {
|
||||
cy.button(/Have Metabot fix it/).click();
|
||||
cy.findByText(FIX_MESSAGE).should("be.visible");
|
||||
});
|
||||
H.runNativeQuery();
|
||||
H.NativeEditor.get()
|
||||
.should("be.visible")
|
||||
.and("contain", "SELECT * FROM ORDERS");
|
||||
cy.findByTestId("query-visualization-root").within(() => {
|
||||
H.tableInteractive().should("be.visible");
|
||||
cy.findByText(FIX_MESSAGE).should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -115,7 +115,8 @@ describe(
|
||||
it("selecting a database in native editor for model actions should not persist the database", () => {
|
||||
[SAMPLE_DB_ID, PG_DB_ID].forEach(enableModelActionsForDatabase);
|
||||
|
||||
startNewAction();
|
||||
cy.visit("/");
|
||||
H.startNewAction();
|
||||
assertNoDatabaseSelected();
|
||||
|
||||
selectDatabase("Sample Database");
|
||||
@@ -129,7 +130,8 @@ describe(
|
||||
selectDatabase(postgresName);
|
||||
cy.wait("@persistDatabase");
|
||||
|
||||
startNewAction();
|
||||
cy.visit("/");
|
||||
H.startNewAction();
|
||||
assertNoDatabaseSelected();
|
||||
});
|
||||
|
||||
@@ -367,12 +369,6 @@ function startNativeModel() {
|
||||
cy.findByRole("heading", { name: "Use a native query" }).click();
|
||||
}
|
||||
|
||||
function startNewAction() {
|
||||
cy.visit("/");
|
||||
cy.findByTestId("app-bar").findByText("New").click();
|
||||
H.popover().findByTextEnsureVisible("Action").click();
|
||||
}
|
||||
|
||||
function assertNoDatabaseSelected() {
|
||||
cy.findByTestId("selected-database").should("not.exist");
|
||||
cy.findByTestId("native-query-top-bar").should(
|
||||
|
||||
@@ -668,7 +668,7 @@ describe("issue 34330", () => {
|
||||
|
||||
it("should only call the autocompleter with all text typed (metabase#34330)", () => {
|
||||
cy.findByTestId("query-visualization-root")
|
||||
.findByText("Query results will appear here.")
|
||||
.findByText("Here's where your results will appear")
|
||||
.should("be.visible");
|
||||
|
||||
H.NativeEditor.type("SEAT", { delay: 10 });
|
||||
|
||||
@@ -34,7 +34,9 @@ describe("issue 11727", { tags: "@external" }, () => {
|
||||
H.runNativeQuery({ wait: false });
|
||||
cy.findByText("Doing science...").should("be.visible");
|
||||
cy.get("body").type("{cmd}{enter}");
|
||||
cy.findByText("Query results will appear here.").should("be.visible");
|
||||
cy.findByText("Here's where your results will appear").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -561,3 +563,21 @@ describe("issue 53649", () => {
|
||||
H.NativeEditor.get().should("contain", "select 1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 57441", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsNormalUser();
|
||||
});
|
||||
|
||||
it("should be possible to create a new snippet from the sidebar (metabase#57441)", () => {
|
||||
H.startNewNativeQuestion();
|
||||
|
||||
H.createSnippet({ name: "snippet 1", content: "select 1" });
|
||||
|
||||
cy.findByTestId("native-query-editor-sidebar").icon("snippet").click();
|
||||
H.rightSidebar().icon("add").click();
|
||||
H.popover().findByText("New snippet").click();
|
||||
H.modal().findByText("Create your new snippet").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,7 +300,7 @@ describe("scenarios > question > native", () => {
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Query results will appear here.").should("be.visible");
|
||||
cy.findByText("Here's where your results will appear").should("be.visible");
|
||||
});
|
||||
|
||||
it("should allow to preview a fully parameterized query", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const { H } = cy;
|
||||
import { USER_GROUPS } from "e2e/support/cypress_data";
|
||||
import { modal } from "e2e/support/helpers";
|
||||
|
||||
const { ALL_USERS_GROUP } = USER_GROUPS;
|
||||
|
||||
@@ -150,6 +149,24 @@ describe("scenarios > question > snippets", () => {
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.get("@results").contains(/christ/i);
|
||||
});
|
||||
|
||||
it("should be possible to search snippets", () => {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
H.createSnippet({ name: `snippet ${i}`, content: `select ${i}` });
|
||||
}
|
||||
|
||||
H.startNewNativeQuestion();
|
||||
cy.icon("snippet").click();
|
||||
|
||||
H.rightSidebar().icon("search").click();
|
||||
H.rightSidebar().findByRole("textbox").type("snippet 14");
|
||||
|
||||
H.rightSidebar().findByText("snippet 14").should("be.visible");
|
||||
H.rightSidebar().findByText("snippet 2").should("not.exist");
|
||||
|
||||
H.rightSidebar().icon("close").click();
|
||||
H.rightSidebar().findByText("snippet 2").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > question > snippets (OSS)", { tags: "@OSS" }, () => {
|
||||
@@ -370,15 +387,13 @@ describe("scenarios > question > snippets (EE)", () => {
|
||||
cy.icon("snippet").click();
|
||||
|
||||
// Edit permissions for a snippet folder
|
||||
cy.findByTestId("sidebar-right").within(() => {
|
||||
cy.findByText("Snippet Folder")
|
||||
.next()
|
||||
.find(".Icon-ellipsis")
|
||||
.click({ force: true });
|
||||
});
|
||||
H.rightSidebar()
|
||||
.findByText("Snippet Folder")
|
||||
.next()
|
||||
.find(".Icon-ellipsis")
|
||||
.click({ force: true });
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Change permissions").click();
|
||||
H.popover().findByText("Change permissions").click();
|
||||
|
||||
// Update permissions for "All users" and let them only "View" this folder
|
||||
H.modal().within(() => {
|
||||
@@ -388,15 +403,17 @@ describe("scenarios > question > snippets (EE)", () => {
|
||||
});
|
||||
|
||||
H.popover().contains("View").click();
|
||||
modal().button("Save").click();
|
||||
H.modal().button("Save").click();
|
||||
|
||||
cy.wait("@updatePermissions");
|
||||
|
||||
// Now let's do the sanity check for the top level (root) snippet permissions and make sure nothing changed there
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Snippets").parent().next().find(".Icon-ellipsis").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Change permissions").click();
|
||||
H.rightSidebar()
|
||||
.findByTestId("snippet-header-buttons")
|
||||
.icon("ellipsis")
|
||||
.click();
|
||||
|
||||
H.popover().findByText("Change permissions").click();
|
||||
|
||||
// UI check
|
||||
H.modal().within(() => {
|
||||
|
||||
@@ -5,10 +5,32 @@ import {
|
||||
ORDERS_BY_YEAR_QUESTION_ID,
|
||||
ORDERS_COUNT_QUESTION_ID,
|
||||
ORDERS_DASHBOARD_ID,
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import { createMockDashboardCard } from "metabase-types/api/mocks";
|
||||
|
||||
const { admin } = USERS;
|
||||
|
||||
const TAB_1 = {
|
||||
id: 1,
|
||||
name: "Tab 1",
|
||||
};
|
||||
|
||||
const TAB_2 = {
|
||||
id: 2,
|
||||
name: "Tab 2",
|
||||
};
|
||||
|
||||
const TAB_3 = {
|
||||
id: 3,
|
||||
name: "Tab 3",
|
||||
};
|
||||
|
||||
const TAB_4 = {
|
||||
id: 4,
|
||||
name: "Tab 4",
|
||||
};
|
||||
|
||||
describe("command palette", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
@@ -413,13 +435,31 @@ H.describeWithSnowplow("shortcuts", { tags: ["@actions"] }, () => {
|
||||
event: "keyboard_shortcut_performed",
|
||||
event_detail: "create-new-collection",
|
||||
});
|
||||
H.openCommandPalette();
|
||||
H.commandPalette().findByRole("option", { name: "New dashboard" }).click();
|
||||
cy.findByRole("dialog", { name: /dashboard/i }).should("exist");
|
||||
cy.realPress("Escape");
|
||||
|
||||
// Using a command palette action registered as a shortcut should only
|
||||
// emit snowplow events when using keyboard shortcuts, not command palette
|
||||
H.expectGoodSnowplowEvent(
|
||||
{
|
||||
event: "keyboard_shortcut_performed",
|
||||
event_detail: "create-new-dashboard",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
cy.realPress("c").realPress("d");
|
||||
cy.findByRole("dialog", { name: /dashboard/i }).should("exist");
|
||||
cy.realPress("Escape");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "keyboard_shortcut_performed",
|
||||
event_detail: "create-new-dashboard",
|
||||
});
|
||||
H.expectGoodSnowplowEvent(
|
||||
{
|
||||
event: "keyboard_shortcut_performed",
|
||||
event_detail: "create-new-dashboard",
|
||||
},
|
||||
1,
|
||||
);
|
||||
|
||||
cy.realPress("g").realPress("d");
|
||||
cy.location("pathname").should("contain", "browse/databases");
|
||||
@@ -461,9 +501,9 @@ H.describeWithSnowplow("shortcuts", { tags: ["@actions"] }, () => {
|
||||
|
||||
H.navigationSidebar().should("be.visible");
|
||||
// Mantine Modals
|
||||
H.newButton("Collection").click();
|
||||
H.startNewCollectionFromSidebar();
|
||||
|
||||
H.modal()
|
||||
cy.findByTestId("new-collection-modal")
|
||||
.findByLabelText(/collection it's saved in/i)
|
||||
.click();
|
||||
|
||||
@@ -478,7 +518,8 @@ H.describeWithSnowplow("shortcuts", { tags: ["@actions"] }, () => {
|
||||
cy.realPress("Escape");
|
||||
// Legacy Modals
|
||||
|
||||
H.newButton("Action").click();
|
||||
H.startNewAction();
|
||||
|
||||
// Remove focus
|
||||
H.modal()
|
||||
.findByText(/Build custom forms/)
|
||||
@@ -488,18 +529,54 @@ H.describeWithSnowplow("shortcuts", { tags: ["@actions"] }, () => {
|
||||
cy.realPress("Escape");
|
||||
cy.realPress("[");
|
||||
H.navigationSidebar().should("not.visible");
|
||||
|
||||
cy.findByLabelText("Settings menu").click();
|
||||
H.popover().findByText("Admin settings").click();
|
||||
|
||||
cy.findByTestId("site-name-setting").should("exist");
|
||||
cy.location("pathname").should("contain", "/admin/settings");
|
||||
cy.realPress("3");
|
||||
cy.location("pathname").should("contain", "/admin/datamodel");
|
||||
cy.realPress("7");
|
||||
cy.location("pathname").should("contain", "/admin/tools");
|
||||
});
|
||||
|
||||
it("should support dashboard shortcuts", () => {
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
H.createDashboardWithTabs({
|
||||
tabs: [TAB_1, TAB_2, TAB_3, TAB_4],
|
||||
dashcards: [
|
||||
createMockDashboardCard({
|
||||
id: -1,
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
dashboard_tab_id: TAB_1.id,
|
||||
}),
|
||||
createMockDashboardCard({
|
||||
id: -2,
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
dashboard_tab_id: TAB_2.id,
|
||||
}),
|
||||
createMockDashboardCard({
|
||||
id: -3,
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
dashboard_tab_id: TAB_3.id,
|
||||
}),
|
||||
createMockDashboardCard({
|
||||
id: -4,
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
dashboard_tab_id: TAB_4.id,
|
||||
}),
|
||||
],
|
||||
}).then((dashboard) => H.visitDashboard(dashboard.id));
|
||||
|
||||
H.openShortcutModal();
|
||||
H.shortcutModal().should("exist");
|
||||
cy.realPress("Escape");
|
||||
|
||||
cy.realPress("o");
|
||||
H.openNavigationSidebar();
|
||||
H.navigationSidebar()
|
||||
.findByRole("tab", { name: /bookmarks/i })
|
||||
.should("contain.text", "Orders in a dashboard");
|
||||
.should("contain.text", "Test Dashboard");
|
||||
cy.realPress("o");
|
||||
H.navigationSidebar()
|
||||
.findByRole("tab", { name: /bookmarks/i })
|
||||
@@ -520,6 +597,31 @@ H.describeWithSnowplow("shortcuts", { tags: ["@actions"] }, () => {
|
||||
cy.findByRole("dialog", { name: "Info" }).should("exist");
|
||||
cy.realPress("]");
|
||||
cy.findByRole("dialog", { name: "Info" }).should("not.exist");
|
||||
|
||||
cy.findByRole("tab", { name: "Tab 1" }).should(
|
||||
"have.attr",
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
cy.realPress("3");
|
||||
cy.findByRole("tab", { name: "Tab 3" }).should(
|
||||
"have.attr",
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
cy.realPress("1");
|
||||
cy.findByRole("tab", { name: "Tab 1" }).should(
|
||||
"have.attr",
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
// Doesn't error on pressing numbers out of bounds
|
||||
cy.realPress("7");
|
||||
cy.findByRole("tab", { name: "Tab 1" }).should(
|
||||
"have.attr",
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should support query builder shortcuts", () => {
|
||||
|
||||
@@ -102,6 +102,35 @@ H.describeWithSnowplow("scenarios > browse", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks when a new model creation is initiated", () => {
|
||||
cy.visit("/browse/models");
|
||||
cy.findByTestId("browse-models-header")
|
||||
.findByLabelText("Create a new model")
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.location("pathname").should("eq", "/model/new");
|
||||
H.expectNoBadSnowplowEvents();
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "plus_button_clicked",
|
||||
triggered_from: "model",
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks when a new metric creation is initiated", () => {
|
||||
cy.visit("/browse/metrics");
|
||||
cy.findByTestId("browse-metrics-header")
|
||||
.findByLabelText("Create a new metric")
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.findByTestId("entity-picker-modal").should("be.visible");
|
||||
|
||||
H.expectNoBadSnowplowEvents();
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "plus_button_clicked",
|
||||
triggered_from: "metric",
|
||||
});
|
||||
});
|
||||
|
||||
it("browsing to a database only triggers a request for schemas for that specific database", () => {
|
||||
cy.intercept("GET", `/api/database/${SAMPLE_DB_ID}/schemas`).as(
|
||||
"schemasForSampleDatabase",
|
||||
|
||||
@@ -603,6 +603,76 @@ H.describeWithSnowplow("scenarios > setup", () => {
|
||||
source: "homepage",
|
||||
});
|
||||
});
|
||||
|
||||
it("should track when 'New' button is clicked", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.log("From the app bar");
|
||||
H.newButton().should("be.visible").click();
|
||||
cy.findByRole("dialog").should("be.visible");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "new_button_clicked",
|
||||
triggered_from: "app-bar",
|
||||
});
|
||||
|
||||
cy.log("Track closing the button as well");
|
||||
H.newButton().should("be.visible").click();
|
||||
cy.findByRole("dialog").should("not.exist");
|
||||
H.expectGoodSnowplowEvent(
|
||||
{
|
||||
event: "new_button_clicked",
|
||||
triggered_from: "app-bar",
|
||||
},
|
||||
2,
|
||||
);
|
||||
|
||||
cy.log("From the empty collection");
|
||||
H.navigationSidebar().findByText("Your personal collection").click();
|
||||
cy.findByTestId("collection-empty-state").within(() => {
|
||||
cy.findByText("This collection is empty").should("be.visible");
|
||||
cy.findByText("New").click();
|
||||
});
|
||||
|
||||
cy.findByRole("dialog").should("be.visible");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "new_button_clicked",
|
||||
triggered_from: "empty-collection",
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Until we refactor the NewItem menu component and drop EntityMenu from it,
|
||||
* the only menu item that can have onClick handler is a "dashboard".
|
||||
*/
|
||||
it("should track when a 'New' button's menu item is clicked", () => {
|
||||
cy.visit("/");
|
||||
|
||||
H.newButton().should("be.visible").click();
|
||||
cy.findByRole("dialog").findByText("Dashboard").click();
|
||||
cy.findByTestId("new-dashboard-modal").should("be.visible");
|
||||
H.expectGoodSnowplowEvent({
|
||||
event: "new_button_item_clicked",
|
||||
triggered_from: "dashboard",
|
||||
});
|
||||
|
||||
cy.findByTestId("new-dashboard-modal").button("Cancel").click();
|
||||
cy.findByTestId("new-dashboard-modal").should("not.exist");
|
||||
|
||||
H.navigationSidebar().findByText("Your personal collection").click();
|
||||
cy.findByTestId("collection-empty-state").within(() => {
|
||||
cy.findByText("This collection is empty").should("be.visible");
|
||||
cy.findByText("New").click();
|
||||
});
|
||||
cy.findByRole("dialog").findByText("Dashboard").click();
|
||||
cy.findByTestId("new-dashboard-modal").should("be.visible");
|
||||
H.expectGoodSnowplowEvent(
|
||||
{
|
||||
event: "new_button_item_clicked",
|
||||
triggered_from: "dashboard",
|
||||
},
|
||||
2,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const pinItem = (name) => {
|
||||
|
||||
@@ -26,26 +26,4 @@ describe("metabase > scenarios > navbar > new menu", () => {
|
||||
cy.url("should.contain", "/question#");
|
||||
H.NativeEditor.get().should("be.visible");
|
||||
});
|
||||
|
||||
it("collection opens modal and redirects to a created collection after saving", () => {
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Collection").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
cy.findByTestId("collection-picker-button").findByText("Our analytics");
|
||||
|
||||
cy.findByPlaceholderText("My new fantastic collection").type(
|
||||
"Test collection",
|
||||
);
|
||||
cy.findByLabelText("Description").type("Test collection description");
|
||||
|
||||
cy.findByText("Create").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("collection-name-heading").should(
|
||||
"have.text",
|
||||
"Test collection",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,10 +56,13 @@ describe("nav > what's new notification", () => {
|
||||
|
||||
function mockVersions({ currentVersion, versions = [] }) {
|
||||
const [latest, ...older] = versions;
|
||||
cy.intercept("GET", "/api/setting/version-info", (req) => {
|
||||
req.reply(createMockVersionInfo({ latest, older }));
|
||||
}).as("versionInfo");
|
||||
|
||||
cy.intercept("GET", "/api/session/properties", (req) => {
|
||||
req.reply((res) => {
|
||||
res.body["version"] = { tag: currentVersion };
|
||||
res.body["version-info"] = createMockVersionInfo({ latest, older });
|
||||
});
|
||||
}).as("sessionProperties");
|
||||
}
|
||||
@@ -68,6 +71,7 @@ function loadHomepage() {
|
||||
cy.visit("/");
|
||||
|
||||
cy.wait("@sessionProperties");
|
||||
cy.wait("@versionInfo");
|
||||
|
||||
// make sure page is loaded
|
||||
cy.findByText("loading").should("not.exist");
|
||||
|
||||
@@ -9,9 +9,9 @@ const { admin } = USERS;
|
||||
const locales = ["en", "xx"];
|
||||
|
||||
describe("scenarios > setup", () => {
|
||||
locales.forEach((locale) => {
|
||||
beforeEach(() => H.restore("blank"));
|
||||
beforeEach(() => H.restore("blank"));
|
||||
|
||||
locales.forEach((locale) => {
|
||||
it(
|
||||
`should allow you to sign up using "${locale}" browser locale`,
|
||||
{ tags: ["@external"] },
|
||||
@@ -223,7 +223,6 @@ describe("scenarios > setup", () => {
|
||||
});
|
||||
|
||||
it("should pre-fill user info for hosted instances (infra-frontend#1109)", () => {
|
||||
H.setTokenFeatures("none");
|
||||
H.mockSessionProperty("is-hosted?", true);
|
||||
|
||||
cy.visit(
|
||||
@@ -238,9 +237,7 @@ describe("scenarios > setup", () => {
|
||||
cy.findByDisplayValue("Doe").should("exist");
|
||||
cy.findByDisplayValue("john@doe.test").should("exist");
|
||||
cy.findByDisplayValue("Doe Unlimited").should("exist");
|
||||
cy.findByLabelText("Create a password")
|
||||
.should("be.focused")
|
||||
.and("be.empty");
|
||||
cy.findByLabelText("Create a password").should("be.empty");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,7 +451,7 @@ H.describeWithSnowplow("scenarios > setup", () => {
|
||||
H.expectNoBadSnowplowEvents();
|
||||
});
|
||||
|
||||
it("should send snowplow events", () => {
|
||||
it("should send snowplow events", { tags: "@flaky" }, () => {
|
||||
let goodEvents = 0;
|
||||
|
||||
goodEvents++; // 1 - new_instance_created
|
||||
@@ -576,14 +573,18 @@ H.describeWithSnowplow("scenarios > setup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore snowplow failures and work as normal", () => {
|
||||
H.blockSnowplow();
|
||||
cy.visit("/setup");
|
||||
skipWelcomePage();
|
||||
it(
|
||||
"should ignore snowplow failures and work as normal",
|
||||
{ tags: "@flaky" },
|
||||
() => {
|
||||
H.blockSnowplow();
|
||||
cy.visit("/setup");
|
||||
skipWelcomePage();
|
||||
|
||||
// 1 event is sent from the BE, which isn't blocked by blockSnowplow()
|
||||
H.expectGoodSnowplowEvents(1);
|
||||
});
|
||||
// 1 event is sent from the BE, which isn't blocked by blockSnowplow()
|
||||
H.expectGoodSnowplowEvents(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const skipWelcomePage = () => {
|
||||
|
||||
@@ -90,13 +90,11 @@ describe("URLs", () => {
|
||||
});
|
||||
|
||||
it("should not slugify users' collections page URL", () => {
|
||||
cy.visit("/collection/root");
|
||||
H.navigationSidebar().within(() => {
|
||||
cy.icon("ellipsis").click();
|
||||
});
|
||||
H.popover().findByText("Other users' personal collections").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("All personal collections");
|
||||
cy.visit("/collection/users");
|
||||
cy.findByTestId("browsercrumbs").should(
|
||||
"contain",
|
||||
/All personal collections/i,
|
||||
);
|
||||
cy.location("pathname").should("eq", "/collection/users");
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
ORDERS_DASHBOARD_ID,
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
import type {
|
||||
DashboardDetails,
|
||||
StructuredQuestionDetails,
|
||||
} from "e2e/support/helpers";
|
||||
import type { DashboardCard } from "metabase-types/api";
|
||||
|
||||
const { ORDERS_ID } = SAMPLE_DATABASE;
|
||||
const { ALL_USERS_GROUP } = USER_GROUPS;
|
||||
|
||||
const cardDetails: H.StructuredQuestionDetails = {
|
||||
const cardDetails: StructuredQuestionDetails = {
|
||||
name: "Question",
|
||||
type: "question",
|
||||
query: {
|
||||
@@ -1032,8 +1036,8 @@ describe("scenarios > organization > entity picker", () => {
|
||||
cy.visit("/");
|
||||
|
||||
// New Collection Flow
|
||||
H.newButton("Collection").click();
|
||||
H.modal()
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal")
|
||||
.findByLabelText(/Collection it's saved in/)
|
||||
.click();
|
||||
|
||||
@@ -1123,8 +1127,8 @@ describe("scenarios > organization > entity picker", () => {
|
||||
cy.visit("/");
|
||||
|
||||
// New Collection Flow
|
||||
H.newButton("Collection").click();
|
||||
H.modal()
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal")
|
||||
.findByLabelText(/Collection it's saved in/)
|
||||
.click();
|
||||
H.entityPickerModalTab("Collections").click();
|
||||
@@ -1253,7 +1257,7 @@ function createTestDashboards() {
|
||||
}
|
||||
|
||||
function createTestDashboardWithEmptyCard(
|
||||
dashboardDetails: H.DashboardDetails = {},
|
||||
dashboardDetails: DashboardDetails = {},
|
||||
) {
|
||||
const dashcardDetails: Partial<DashboardCard>[] = [
|
||||
{
|
||||
@@ -1285,7 +1289,7 @@ function createTestDashboardWithEmptyCard(
|
||||
});
|
||||
}
|
||||
|
||||
function selectQuestionFromDashboard(dashboardDetails?: H.DashboardDetails) {
|
||||
function selectQuestionFromDashboard(dashboardDetails?: DashboardDetails) {
|
||||
createTestDashboardWithEmptyCard(dashboardDetails).then((dashboard) => {
|
||||
H.visitDashboard(dashboard.id);
|
||||
H.editDashboard();
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("official collections", () => {
|
||||
// Gate the UI
|
||||
cy.visit("/collection/root");
|
||||
|
||||
startNewCollectionCreation();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
assertNoCollectionTypeInput();
|
||||
cy.findByLabelText("Close").click();
|
||||
@@ -84,7 +84,7 @@ describe("official collections", () => {
|
||||
|
||||
openCollection("First collection");
|
||||
|
||||
startNewCollectionCreation();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
assertNoCollectionTypeInput();
|
||||
cy.findByLabelText("Close").click();
|
||||
@@ -107,7 +107,7 @@ describe("official collections", () => {
|
||||
|
||||
H.popover().findByText("Make collection official").should("exist");
|
||||
|
||||
startNewCollectionCreation();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
assertHasCollectionTypeInput();
|
||||
cy.findByPlaceholderText("My new fantastic collection").type(
|
||||
@@ -124,7 +124,7 @@ describe("official collections", () => {
|
||||
});
|
||||
H.popover().findByText("Make collection official").should("exist");
|
||||
|
||||
startNewCollectionCreation();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
assertHasCollectionTypeInput();
|
||||
cy.findByLabelText("Close").click();
|
||||
@@ -236,7 +236,7 @@ function openCollection(collectionName) {
|
||||
}
|
||||
|
||||
function createAndOpenOfficialCollection({ name }) {
|
||||
startNewCollectionCreation();
|
||||
H.startNewCollectionFromSidebar();
|
||||
cy.findByTestId("new-collection-modal").then((modal) => {
|
||||
cy.findByPlaceholderText("My new fantastic collection").type(name);
|
||||
cy.findByText("Official").click();
|
||||
@@ -310,5 +310,3 @@ const assertHasCollectionBadgeInNavbar = (expectBadge = true) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const startNewCollectionCreation = () => H.newButton("Collection").click();
|
||||
|
||||
@@ -306,14 +306,8 @@ describe("UI elements that make no sense for users without data permissions (met
|
||||
cy.icon("refresh").should("not.exist");
|
||||
});
|
||||
|
||||
cy.visit("/collection/root");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("New").click();
|
||||
|
||||
H.popover()
|
||||
.should("contain", "Dashboard")
|
||||
.and("contain", "Collection")
|
||||
.and("not.contain", "Question");
|
||||
H.newButton().click();
|
||||
H.popover().should("contain", "Dashboard").and("not.contain", "Question");
|
||||
});
|
||||
|
||||
it("should not show visualization or question settings to users with block data permissions", () => {
|
||||
@@ -341,14 +335,9 @@ describe("UI elements that make no sense for users without data permissions (met
|
||||
cy.findByTestId("qb-header-action-panel").within(() => {
|
||||
cy.icon("refresh").should("not.exist");
|
||||
});
|
||||
cy.visit("/collection/root");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("New").click();
|
||||
|
||||
H.popover()
|
||||
.should("contain", "Dashboard")
|
||||
.and("contain", "Collection")
|
||||
.and("not.contain", "Question");
|
||||
H.newButton().click();
|
||||
H.popover().should("contain", "Dashboard").and("not.contain", "Question");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -183,9 +183,7 @@ describe("issue 25144", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
it("should show Models tab after creation the first model (metabase#24878)", () => {
|
||||
cy.visit("/");
|
||||
|
||||
H.newButton("Model").click();
|
||||
cy.visit("/model/new");
|
||||
cy.findByTestId("new-model-options")
|
||||
.findByText(/use the notebook/i)
|
||||
.click();
|
||||
@@ -500,7 +498,7 @@ describe("issue 30165", () => {
|
||||
cy.get("@dataset.all").should("have.length", 0);
|
||||
cy.get("@cardQuery.all").should("have.length", 0);
|
||||
cy.findByTestId("query-builder-main")
|
||||
.findByText("Query results will appear here.")
|
||||
.findByText("Here's where your results will appear")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||