Compare commits
75 Commits
module-plu
...
readable-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2363e2d970 | ||
|
|
a3d94fef40 | ||
|
|
71fec20eb0 | ||
|
|
ce69347dff | ||
|
|
4d7a8c3b67 | ||
|
|
757e9820f7 | ||
|
|
31aca5ebf1 | ||
|
|
3adbda362d | ||
|
|
0548bc5a18 | ||
|
|
726709c679 | ||
|
|
8ce3936116 | ||
|
|
5bcd89d33f | ||
|
|
916ee6554c | ||
|
|
fff70175e0 | ||
|
|
74216b3098 | ||
|
|
0509e1a5e2 | ||
|
|
81736f712a | ||
|
|
812e9b2254 | ||
|
|
d0ce86c19e | ||
|
|
72e2e9b234 | ||
|
|
93742a629c | ||
|
|
b485c0f7fa | ||
|
|
76979e6167 | ||
|
|
da7629bba6 | ||
|
|
280c1e7be2 | ||
|
|
3d924036db | ||
|
|
883e944cce | ||
|
|
9d1c919f6b | ||
|
|
b08a2f0e65 | ||
|
|
22b1a393dc | ||
|
|
f58c520891 | ||
|
|
f2364b1d27 | ||
|
|
39bd87ede3 | ||
|
|
56232fc3f8 | ||
|
|
61b6635f13 | ||
|
|
86b1f4bca4 | ||
|
|
ac1f4cdc00 | ||
|
|
0e83852557 | ||
|
|
61d0fa5d0f | ||
|
|
504b8ea9ee | ||
|
|
61ce133268 | ||
|
|
f24251fb4e | ||
|
|
7994ad35d8 | ||
|
|
eb845c2ea6 | ||
|
|
3d8a3901a9 | ||
|
|
16bd231ba7 | ||
|
|
89da5b2bc6 | ||
|
|
91f5798258 | ||
|
|
e49c2e60af | ||
|
|
496db401a1 | ||
|
|
188c8f412f | ||
|
|
62831f5e4d | ||
|
|
d5d6eba4c5 | ||
|
|
a3b46fdd3c | ||
|
|
61c0e2915e | ||
|
|
5701125c34 | ||
|
|
ecb8fe82b2 | ||
|
|
40ebe0bb9f | ||
|
|
0700179dad | ||
|
|
e96a5d884d | ||
|
|
587b659ee7 | ||
|
|
4b6744c1f4 | ||
|
|
4c5146d536 | ||
|
|
e28ae43d1d | ||
|
|
d37f1a9dd3 | ||
|
|
1a32584bb8 | ||
|
|
d3ce9de3cc | ||
|
|
249390c4b6 | ||
|
|
efbd247394 | ||
|
|
bfc9d9aa91 | ||
|
|
8658d420d2 | ||
|
|
1a4afe2350 | ||
|
|
d53a909a92 | ||
|
|
9daae78940 | ||
|
|
c881cfaa4e |
@@ -84,6 +84,7 @@
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}
|
||||
:metabase/validate-logging {:level :warning}
|
||||
:metabase/validate-mb-app-db-migrations-test {:level :warning}
|
||||
:metabase/validate-mb-driver-tests {:level :warning}
|
||||
:metabase/validate-mb-once {:level :warning}
|
||||
|
||||
@@ -319,6 +320,12 @@
|
||||
{metabase-enterprise.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
|
||||
metabase.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
|
||||
metabase.entity-retrieval.mirror/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
|
||||
metabase-enterprise.transform-testing.expectations.equals/->EqualsRows {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
metabase-enterprise.transform-testing.expectations.equals/map->EqualsRows {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
metabase-enterprise.transform-testing.expectations.equals/->EqualsSql {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
metabase-enterprise.transform-testing.expectations.equals/map->EqualsSql {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
metabase-enterprise.transform-testing.expectations.empty/->Empty {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
metabase-enterprise.transform-testing.expectations.empty/map->Empty {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
|
||||
clojure.core/print {:message "Use metabase.util.log instead of clojure.core/print"}
|
||||
clojure.core/println {:message "Use metabase.util.log instead of clojure.core/println"}
|
||||
clojure.core/printf {:message "Use metabase.util.log instead of clojure.core/printf"}
|
||||
@@ -617,7 +624,7 @@
|
||||
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
|
||||
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
|
||||
metabase.api.macros/defendpoint hooks.metabase.api.macros/defendpoint
|
||||
metabase.app-db.schema-migrations-test.impl/test-migrations hooks.metabase.app-db.schema-migrations-test.impl/test-migrations
|
||||
metabase.app-db.schema-migrations-test.impl/test-migrations hooks.metabase.app-db.schema-migrations-test.impl/test-migrations!
|
||||
metabase.channel.render.js.pool/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
|
||||
@@ -847,7 +854,13 @@
|
||||
metabase.users.models.user-test/with-groups! macros.metabase.users.models.user-test/with-groups!
|
||||
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
|
||||
metabase.util.namespaces/import-macro macros.metabase.util.namespaces/import-macro
|
||||
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
|
||||
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world
|
||||
potemkin/import-def macros.potemkin.namespaces/import-def
|
||||
potemkin/import-fn macros.potemkin.namespaces/import-fn
|
||||
potemkin/import-macro macros.potemkin.namespaces/import-macro
|
||||
potemkin.namespaces/import-def macros.potemkin.namespaces/import-def
|
||||
potemkin.namespaces/import-fn macros.potemkin.namespaces/import-fn
|
||||
potemkin.namespaces/import-macro macros.potemkin.namespaces/import-macro}}
|
||||
|
||||
:config-in-comment
|
||||
{:linters {:unresolved-symbol {:level :off}}}
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
:uses #{actions
|
||||
analytics
|
||||
api
|
||||
api-scope
|
||||
api.eid-translation
|
||||
collections
|
||||
lib
|
||||
@@ -317,7 +318,9 @@
|
||||
api-scope
|
||||
{:team "Metabot"
|
||||
:api #{metabase.api-scope.core
|
||||
metabase.api-scope.init}}
|
||||
metabase.api-scope.data-app
|
||||
metabase.api-scope.init}
|
||||
:uses #{util}}
|
||||
|
||||
api.eid-translation
|
||||
{:team "UX West"
|
||||
@@ -548,6 +551,7 @@
|
||||
query-processor.pivot
|
||||
request
|
||||
settings
|
||||
startup
|
||||
system
|
||||
task
|
||||
tiles
|
||||
@@ -640,6 +644,7 @@
|
||||
:ns-prefix "metabase.collections-rest"
|
||||
:api #{metabase.collections-rest.api}
|
||||
:uses #{api
|
||||
api-scope
|
||||
api.eid-translation
|
||||
collections
|
||||
documents
|
||||
@@ -795,6 +800,7 @@
|
||||
:uses #{actions
|
||||
analytics
|
||||
api
|
||||
api-scope
|
||||
api.eid-translation
|
||||
app-db
|
||||
channel
|
||||
@@ -904,7 +910,6 @@
|
||||
metabase.driver.settings
|
||||
metabase.driver.sql
|
||||
metabase.driver.sql-jdbc
|
||||
metabase.driver.sql-jdbc.connection.pool-lock
|
||||
metabase.driver.sql-jdbc.execute
|
||||
metabase.driver.sql-jdbc.execute.diagnostic
|
||||
metabase.driver.sql-jdbc.sync
|
||||
@@ -930,6 +935,7 @@
|
||||
query-processor.driver-api
|
||||
settings
|
||||
sql-tools
|
||||
startup
|
||||
system
|
||||
tracing
|
||||
util
|
||||
@@ -957,28 +963,6 @@
|
||||
:model-imports #{:model/Card :model/Dashboard}
|
||||
:model-exports #{:model/EmbeddingTheme}}
|
||||
|
||||
embedding-hub
|
||||
{:team "Embedding"
|
||||
:api #{metabase.embedding-hub.api}
|
||||
:uses #{api
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
metabot
|
||||
permissions
|
||||
premium-features
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/ConnectionImpersonation
|
||||
:model/Dashboard
|
||||
:model/Database
|
||||
:model/DatabaseRouter
|
||||
:model/EmbeddingTheme
|
||||
:model/Sandbox
|
||||
:model/Table
|
||||
:model/Tenant}}
|
||||
|
||||
embedding.rest
|
||||
{:team "Embedding"
|
||||
@@ -1021,7 +1005,7 @@
|
||||
|
||||
explorations
|
||||
{:team "UX West"
|
||||
:api #{metabase.explorations.core metabase.explorations.init metabase.explorations.schema}
|
||||
:api #{metabase.explorations.api metabase.explorations.core metabase.explorations.init metabase.explorations.schema}
|
||||
:uses #{analytics
|
||||
analytics.interface
|
||||
api
|
||||
@@ -1101,7 +1085,7 @@
|
||||
|
||||
glossary
|
||||
{:team "Gadget"
|
||||
:api #{metabase.glossary.api}
|
||||
:api #{metabase.glossary.api metabase.glossary.core}
|
||||
:uses #{api
|
||||
events
|
||||
lib.schema
|
||||
@@ -1126,6 +1110,7 @@
|
||||
indexed-entities
|
||||
{:team "UX West"
|
||||
:api #{metabase.indexed-entities.api
|
||||
metabase.indexed-entities.core
|
||||
metabase.indexed-entities.init}
|
||||
:uses #{analytics
|
||||
api
|
||||
@@ -1340,6 +1325,7 @@
|
||||
comments
|
||||
dashboards
|
||||
documents
|
||||
glossary
|
||||
lib
|
||||
lib.be
|
||||
lib.metadata
|
||||
@@ -1368,6 +1354,7 @@
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -1400,6 +1387,7 @@
|
||||
mcp
|
||||
models
|
||||
request
|
||||
server
|
||||
settings
|
||||
system
|
||||
task
|
||||
@@ -1582,6 +1570,7 @@
|
||||
settings
|
||||
setup
|
||||
sql-tools
|
||||
startup
|
||||
sync
|
||||
util
|
||||
util.config
|
||||
@@ -1616,10 +1605,7 @@
|
||||
system
|
||||
upload
|
||||
util}
|
||||
:model-imports #{:model/AuthIdentity
|
||||
:model/Card
|
||||
:model/Database
|
||||
:model/MetabotMessage}}
|
||||
:model-imports #{:model/AuthIdentity :model/Card :model/MetabotMessage}}
|
||||
|
||||
metrics
|
||||
{:team "UX West"
|
||||
@@ -1758,6 +1744,7 @@
|
||||
{:team "Graphy"
|
||||
:api #{metabase.native-query-snippets.api metabase.native-query-snippets.core metabase.native-query-snippets.schema}
|
||||
:uses #{api
|
||||
api-scope
|
||||
collections
|
||||
events
|
||||
lib
|
||||
@@ -2132,8 +2119,10 @@
|
||||
:ns-prefix "metabase.queries-rest"
|
||||
:api #{metabase.queries-rest.api}
|
||||
:uses #{api
|
||||
api-scope
|
||||
api.eid-translation
|
||||
collections
|
||||
embedding
|
||||
events
|
||||
lib
|
||||
lib.be
|
||||
@@ -2216,6 +2205,7 @@
|
||||
:uses #{analytics
|
||||
analytics.interface
|
||||
api
|
||||
api-scope
|
||||
app-db
|
||||
appearance
|
||||
audit-app
|
||||
@@ -2338,7 +2328,8 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase.revisions.api
|
||||
metabase.revisions.core
|
||||
metabase.revisions.init}
|
||||
metabase.revisions.init
|
||||
metabase.revisions.models.revision}
|
||||
:uses #{api
|
||||
collections
|
||||
dashboards
|
||||
@@ -2368,7 +2359,8 @@
|
||||
:model/Measure
|
||||
:model/ParameterCard
|
||||
:model/Segment
|
||||
:model/Transform}}
|
||||
:model/Transform
|
||||
:model/TransformTest}}
|
||||
|
||||
run-tracking
|
||||
{:team "Gadget"
|
||||
@@ -2410,6 +2402,7 @@
|
||||
:uses #{analytics
|
||||
analytics.interface
|
||||
api
|
||||
api-scope
|
||||
app-db
|
||||
appearance
|
||||
audit-app
|
||||
@@ -2491,6 +2484,7 @@
|
||||
analytics.interface
|
||||
api
|
||||
api-keys
|
||||
api-scope
|
||||
app-db
|
||||
appearance
|
||||
core.initialization-status
|
||||
@@ -2525,6 +2519,7 @@
|
||||
metabase.session.schema
|
||||
metabase.session.settings} ; TODO -- shouldn't be exposed as an API namespace
|
||||
:uses #{api
|
||||
api-scope
|
||||
app-db
|
||||
channel
|
||||
events
|
||||
@@ -2647,6 +2642,7 @@
|
||||
server
|
||||
settings
|
||||
sso.auth-identity
|
||||
startup
|
||||
system
|
||||
util
|
||||
util.config}
|
||||
@@ -2822,6 +2818,7 @@
|
||||
premium-features
|
||||
query-processor
|
||||
settings
|
||||
startup
|
||||
util}
|
||||
:model-imports #{:model/Card :model/Dashboard :model/DashboardCard}}
|
||||
|
||||
@@ -2831,6 +2828,7 @@
|
||||
metabase.timeline.core}
|
||||
:uses #{analytics
|
||||
api
|
||||
api-scope
|
||||
collections
|
||||
events
|
||||
lib.schema
|
||||
@@ -3067,6 +3065,7 @@
|
||||
:api #{metabase.user-key-value.api
|
||||
metabase.user-key-value.init}
|
||||
:uses #{api
|
||||
api-scope
|
||||
lib.schema
|
||||
util
|
||||
util.config}}
|
||||
@@ -3110,6 +3109,7 @@
|
||||
:ns-prefix "metabase.users-rest"
|
||||
:api #{metabase.users-rest.api}
|
||||
:uses #{api
|
||||
api-scope
|
||||
appearance
|
||||
collections
|
||||
events
|
||||
@@ -3210,6 +3210,7 @@
|
||||
:api #{metabase.warehouses-rest.api}
|
||||
:uses #{analytics
|
||||
api
|
||||
api-scope
|
||||
app-db
|
||||
collections
|
||||
database-routing
|
||||
@@ -3229,13 +3230,11 @@
|
||||
sync
|
||||
upload
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema
|
||||
warehouses.secrets
|
||||
enterprise/permissions}
|
||||
warehouses.secrets}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Database
|
||||
@@ -3278,11 +3277,8 @@
|
||||
settings
|
||||
sync.analyze
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
enterprise/permissions}
|
||||
warehouses}
|
||||
:model-exports #{:model/Dimension
|
||||
:model/Field
|
||||
:model/FieldUserSettings
|
||||
@@ -3303,6 +3299,7 @@
|
||||
:api #{metabase.warehouse-schema-rest.api}
|
||||
:uses #{analytics
|
||||
api
|
||||
api-scope
|
||||
app-db
|
||||
collections
|
||||
database-routing
|
||||
@@ -3352,6 +3349,7 @@
|
||||
collections
|
||||
dashboards
|
||||
driver
|
||||
indexed-entities
|
||||
lib
|
||||
lib.be
|
||||
lib.legacy-mbql
|
||||
@@ -3376,7 +3374,6 @@
|
||||
:model/Database
|
||||
:model/Field
|
||||
:model/ModelIndex
|
||||
:model/ModelIndexValue
|
||||
:model/Query
|
||||
:model/Revision
|
||||
:model/Segment
|
||||
@@ -3605,6 +3602,7 @@
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.content-translation.routes}
|
||||
:uses #{api
|
||||
api-scope
|
||||
content-translation
|
||||
embedding
|
||||
premium-features
|
||||
@@ -3634,6 +3632,7 @@
|
||||
metabase-enterprise.custom-viz-plugin.init
|
||||
metabase-enterprise.custom-viz-plugin.settings}
|
||||
:uses #{api
|
||||
api-scope
|
||||
events
|
||||
models
|
||||
premium-features
|
||||
@@ -3647,6 +3646,7 @@
|
||||
{:team "Embedding"
|
||||
:api #{metabase-enterprise.data-apps.api metabase-enterprise.data-apps.init metabase-enterprise.data-apps.sync}
|
||||
:uses #{api
|
||||
api-scope
|
||||
collections
|
||||
lib
|
||||
lib.be
|
||||
@@ -3802,8 +3802,13 @@
|
||||
enterprise/embedding
|
||||
{:team "Embedding"
|
||||
:ns-prefix "metabase-enterprise.embedding-hub"
|
||||
:api #{}
|
||||
:uses #{lib.schema
|
||||
:api #{metabase-enterprise.embedding-hub.api}
|
||||
:uses #{api
|
||||
appearance
|
||||
audit-app
|
||||
embedding
|
||||
lib.schema
|
||||
permissions
|
||||
premium-features
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
@@ -4032,6 +4037,7 @@
|
||||
:ns-prefix "metabase-enterprise.sandbox"
|
||||
:api #{metabase-enterprise.sandbox.api.routes metabase-enterprise.sandbox.api.util metabase-enterprise.sandbox.schema}
|
||||
:uses #{api
|
||||
api-scope
|
||||
app-db
|
||||
audit-app
|
||||
driver
|
||||
@@ -4114,6 +4120,7 @@
|
||||
:model/Timeline
|
||||
:model/Transform
|
||||
:model/TransformTag
|
||||
:model/TransformTest
|
||||
:model/User}}
|
||||
|
||||
enterprise/search
|
||||
@@ -4348,6 +4355,31 @@
|
||||
:model-exports #{:model/Tenant}
|
||||
:model-imports #{:model/Collection :model/User}}
|
||||
|
||||
enterprise/transform-testing
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.transform-testing.api metabase-enterprise.transform-testing.init}
|
||||
:uses #{api
|
||||
audit-app
|
||||
driver
|
||||
events
|
||||
lib
|
||||
lib.schema
|
||||
models
|
||||
premium-features
|
||||
revisions
|
||||
run-tracking
|
||||
sql-tools
|
||||
sql-tools.parsing
|
||||
task
|
||||
transforms.base
|
||||
util
|
||||
warehouses}
|
||||
:model-exports #{:model/TransformTest}
|
||||
:model-imports #{:model/Collection
|
||||
:model/Database
|
||||
:model/Transform
|
||||
:model/User}}
|
||||
|
||||
enterprise/transforms
|
||||
{:team "Graphy"
|
||||
:module-exports #{enterprise/transforms.inspector
|
||||
|
||||
21
.clj-kondo/macros/potemkin/namespaces.clj
Normal file
21
.clj-kondo/macros/potemkin/namespaces.clj
Normal file
@@ -0,0 +1,21 @@
|
||||
(ns macros.potemkin.namespaces)
|
||||
|
||||
(defn- import-var-expansion
|
||||
"Expansion shared by all of the `import-*` replacements below. Synthetic \"docstring\" is needed to silence
|
||||
`:missing-docstring` linter."
|
||||
([sym]
|
||||
(import-var-expansion sym (-> sym name symbol)))
|
||||
([sym new-name]
|
||||
`(def ~new-name "docstring" ~sym)))
|
||||
|
||||
(defmacro import-def
|
||||
([sym] (import-var-expansion sym))
|
||||
([sym new-name] (import-var-expansion sym new-name)))
|
||||
|
||||
(defmacro import-fn
|
||||
([sym] (import-var-expansion sym))
|
||||
([sym new-name] (import-var-expansion sym new-name)))
|
||||
|
||||
(defmacro import-macro
|
||||
([sym] (import-var-expansion sym))
|
||||
([sym new-name] (import-var-expansion sym new-name)))
|
||||
@@ -14,10 +14,10 @@
|
||||
:consistent-alias 2
|
||||
:def-fn 1
|
||||
:deprecated-namespace 95
|
||||
:deprecated-var 95
|
||||
:deprecated-var 85
|
||||
:discouraged-java-method 4
|
||||
:discouraged-namespace 69
|
||||
:discouraged-var 130
|
||||
:discouraged-namespace 68
|
||||
:discouraged-var 132
|
||||
:duplicate-require 1
|
||||
:equals-true 1
|
||||
:main-without-gen-class 1
|
||||
@@ -32,7 +32,7 @@
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 48
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case 44
|
||||
:metabase/validate-deftest 9
|
||||
:missing-docstring 12
|
||||
:missing-docstring 4
|
||||
:missing-protocol-method 3
|
||||
:non-arg-vec-return-type-hint 1
|
||||
:reduce-without-init 3
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
(ns hooks.metabase.app-db.schema-migrations-test.impl
|
||||
(:require [clj-kondo.hooks-api :as hooks]))
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(defn test-migrations
|
||||
[{{[_ migration-range {[binding] :children} & body] :children} :node}]
|
||||
(defn- validate-mb-app-db-migrations-test!
|
||||
"CI runs the namespaces tagged `:mb/app-db-migrations-test` on a shard of their own, because a count-based split
|
||||
can't balance them (see `.github/workflows/app-db.yml`). An untagged one still runs, just on a shard sized for
|
||||
everything else."
|
||||
[{:keys [ns node]}]
|
||||
(when-not (:mb/app-db-migrations-test (meta ns))
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta node)
|
||||
:message (str "Namespaces that run migration tests (test-migrations) should be marked"
|
||||
" ^:mb/app-db-migrations-test [:metabase/validate-mb-app-db-migrations-test]")
|
||||
:type :metabase/validate-mb-app-db-migrations-test))))
|
||||
|
||||
(defn test-migrations!
|
||||
[{{[_ migration-range {[binding] :children} & body] :children} :node, :as x}]
|
||||
(validate-mb-app-db-migrations-test! x)
|
||||
(let [node* (hooks/list-node
|
||||
(list*
|
||||
(hooks/token-node `let)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
(ns hooks.metabase.app-db.schema-migrations-test.impl-test
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]
|
||||
[clj-kondo.impl.utils]
|
||||
[clojure.test :refer :all]
|
||||
[hooks.metabase.app-db.schema-migrations-test.impl]))
|
||||
|
||||
(defn- finding-types!
|
||||
[ns-symb]
|
||||
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-mb-app-db-migrations-test {:level :warning}}}
|
||||
:ignores (atom nil)
|
||||
:findings (atom [])
|
||||
:namespaces (atom {})}]
|
||||
(hooks.metabase.app-db.schema-migrations-test.impl/test-migrations!
|
||||
{:ns ns-symb
|
||||
:node (hooks/parse-string (pr-str '(impl/test-migrations ["v64.2026-09-01T00:00:00"] [migrate!] (migrate!))))})
|
||||
(mapv :type @(:findings clj-kondo.impl.utils/*ctx*))))
|
||||
|
||||
(deftest ^:synchronized require-app-db-migrations-test-tag-test
|
||||
(testing "a namespace that runs migration tests must be tagged, so CI shards it with the other migration tests"
|
||||
(is (= [:metabase/validate-mb-app-db-migrations-test]
|
||||
(finding-types! 'metabase.some-migrations-test))))
|
||||
(testing "a tagged namespace is fine"
|
||||
(is (= []
|
||||
(finding-types! (with-meta 'metabase.some-migrations-test {:mb/app-db-migrations-test true}))))))
|
||||
46
.env.example
46
.env.example
@@ -121,6 +121,9 @@ MB_EDITION="ee"
|
||||
# Frontend dev server port (for hot reload). Must match the port used by `yarn build-hot`.
|
||||
# MB_FRONTEND_DEV_PORT=8080
|
||||
|
||||
# Frontend dev server bind address. Defaults to 127.0.0.1; set 0.0.0.0 to reach it from other devices.
|
||||
# MB_FRONTEND_DEV_HOST=127.0.0.1
|
||||
|
||||
# 3.2 E2E Local Runner Setup
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
@@ -161,3 +164,46 @@ TZ="US/Pacific"
|
||||
# DO NOT USE Docker Desktop. Use Orbstack instead. Metabase will fail to connect
|
||||
# to the dockerized Mongo QA database otherwise.
|
||||
EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 4. BACKEND DRIVER TESTS
|
||||
# ==============================================================================
|
||||
#
|
||||
# Connection details for running backend tests against a driver, e.g.
|
||||
# ./bin/test-agent --drivers=snowflake :only '[metabase.foo-test]'
|
||||
|
||||
# 4.1 BigQuery
|
||||
# ------------------------------------------------------------------------------
|
||||
# Service account with BigQuery access in the project. The test user creates and drops the test-data datasets itself.
|
||||
# https://cloud.google.com/iam/docs/keys-create-delete
|
||||
|
||||
# MB_BIGQUERY_CLOUD_SDK_TEST_PROJECT_ID="<project-id>"
|
||||
# MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON='{"type": "service_account", ...}'
|
||||
# Only for database routing tests
|
||||
# MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING='{"type": "service_account", ...}'
|
||||
|
||||
# 4.2 Redshift
|
||||
# ------------------------------------------------------------------------------
|
||||
# Test data is loaded into a unique schema per test run.
|
||||
|
||||
# MB_REDSHIFT_TEST_HOST="<cluster>.<id>.<region>.redshift.amazonaws.com"
|
||||
# Comma-separated hosts to pick one at random from, instead of MB_REDSHIFT_TEST_HOST
|
||||
# MB_REDSHIFT_TEST_HOSTS=""
|
||||
# MB_REDSHIFT_TEST_PORT=5439
|
||||
# MB_REDSHIFT_TEST_DB="testdb"
|
||||
# MB_REDSHIFT_TEST_USER="metabase_ci"
|
||||
# MB_REDSHIFT_TEST_PASSWORD="<password>"
|
||||
# Only for database routing tests
|
||||
# MB_REDSHIFT_TEST_DB_ROUTING="dev"
|
||||
|
||||
# 4.3 Snowflake
|
||||
# ------------------------------------------------------------------------------
|
||||
# Key-pair authentication. The test user creates and drops the test-data databases itself.
|
||||
# https://docs.snowflake.com/en/user-guide/key-pair-auth
|
||||
|
||||
# MB_SNOWFLAKE_TEST_ACCOUNT="<orgname>-<accountname>"
|
||||
# MB_SNOWFLAKE_TEST_USER="<username>"
|
||||
# MB_SNOWFLAKE_TEST_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
# MB_SNOWFLAKE_TEST_WAREHOUSE="<warehouse>"
|
||||
# MB_SNOWFLAKE_TEST_ADDITIONAL_OPTIONS=""
|
||||
|
||||
20
.github/actions/create-backport/action.yml
vendored
20
.github/actions/create-backport/action.yml
vendored
@@ -85,17 +85,27 @@ runs:
|
||||
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Approve and Merge the PR
|
||||
# Approving takes a different token than the one that opened the PR: GitHub won't let a token
|
||||
# approve its own pull request.
|
||||
- name: Approve the backport PR
|
||||
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
|
||||
run: |
|
||||
gh pr review --approve "$PR_NUMBER"
|
||||
gh pr merge --squash --auto "$PR_NUMBER"
|
||||
run: gh pr review --approve "$PR_NUMBER"
|
||||
shell: bash
|
||||
env:
|
||||
# uses a different token than the token that created the PR so we can approve and merge it
|
||||
GH_TOKEN: ${{ inputs.github-token-2 }}
|
||||
PR_NUMBER: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
|
||||
|
||||
# Merging, though, has to use `github-token` rather than the `GITHUB_TOKEN` that approves: GitHub
|
||||
# starts no workflow run for a push made with `GITHUB_TOKEN`, so an auto-merge enabled with it
|
||||
# lands the backport on the release branch with no CI at all.
|
||||
- name: Merge the backport PR
|
||||
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
|
||||
run: gh pr merge --squash --auto "$PR_NUMBER"
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
PR_NUMBER: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
|
||||
|
||||
- uses: ./.github/actions/notify-pull-request
|
||||
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'true' }}
|
||||
with:
|
||||
|
||||
137
.github/workflows/app-db.yml
vendored
137
.github/workflows/app-db.yml
vendored
@@ -64,8 +64,9 @@ jobs:
|
||||
mariadb: ${{ steps.versions.outputs.mariadb }}
|
||||
mysql: ${{ steps.versions.outputs.mysql }}
|
||||
postgres: ${{ steps.versions.outputs.postgres }}
|
||||
shards: ${{ steps.versions.outputs.shards }}
|
||||
steps:
|
||||
- name: Select versions to test
|
||||
- name: Select versions and shards to test
|
||||
id: versions
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
@@ -133,6 +134,32 @@ jobs:
|
||||
core.setOutput(db, JSON.stringify(selected));
|
||||
}
|
||||
|
||||
const commonExcludes = ":mb/driver-tests :mb/transforms-python-test :mb/old-migrations-test";
|
||||
|
||||
const shards = [
|
||||
{
|
||||
name: 'Enterprise Tests',
|
||||
'build-static-viz': false,
|
||||
'test-args': `:only '"enterprise/backend/test"' :exclude-tags '[${commonExcludes}]'`,
|
||||
},
|
||||
{
|
||||
name: 'EE App DB Tests (Part 1)', // migrations shard
|
||||
'build-static-viz': true,
|
||||
'test-args': `:only '"test"' :only-tags '[:mb/app-db-migrations-test]' :exclude-tags '[${commonExcludes}]'`,
|
||||
},
|
||||
{
|
||||
name: 'EE App DB Tests (Part 2)',
|
||||
'build-static-viz': true,
|
||||
'test-args': `:only '"test"' :partition/total 2 :partition/index 0 :exclude-tags '[${commonExcludes} :mb/app-db-migrations-test]'`,
|
||||
},
|
||||
{
|
||||
name: 'EE App DB Tests (Part 3)',
|
||||
'build-static-viz': true,
|
||||
'test-args': `:only '"test"' :partition/total 2 :partition/index 1 :exclude-tags '[${commonExcludes} :mb/app-db-migrations-test]'`,
|
||||
},
|
||||
];
|
||||
core.setOutput('shards', JSON.stringify(shards));
|
||||
|
||||
be-tests-mariadb:
|
||||
needs: app-db-matrix
|
||||
if: ${{ !inputs.skip && (inputs.app-db == '' || inputs.app-db == 'mariadb') }}
|
||||
@@ -142,26 +169,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ${{ fromJSON(needs.app-db-matrix.outputs.mariadb) }}
|
||||
job:
|
||||
- name: Enterprise Tests
|
||||
build-static-viz: false
|
||||
test-args: >-
|
||||
:only '"enterprise/backend/test"'
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 1)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 2)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
|
||||
|
||||
services:
|
||||
mysql:
|
||||
@@ -175,10 +183,6 @@ jobs:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
|
||||
MARIADB_DATABASE: mb_test
|
||||
env:
|
||||
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
|
||||
# GDGT-3126: re-enable and properly fix old migrations tests
|
||||
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
|
||||
# actual serious env vars below
|
||||
CI: 'true'
|
||||
DRIVERS: mysql
|
||||
MB_DB_TYPE: mysql
|
||||
@@ -205,7 +209,8 @@ jobs:
|
||||
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
|
||||
# "be-tests-mariadb-10-6-ee (0)"
|
||||
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
|
||||
steps:
|
||||
# Shared by every app DB job below via `*app-db-test-steps`.
|
||||
steps: &app-db-test-steps
|
||||
- uses: actions/checkout@v6
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
@@ -214,9 +219,7 @@ jobs:
|
||||
build-static-viz: ${{ matrix.job.build-static-viz }}
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
|
||||
be-tests-mysql:
|
||||
needs: app-db-matrix
|
||||
@@ -227,26 +230,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ${{ fromJSON(needs.app-db-matrix.outputs.mysql) }}
|
||||
job:
|
||||
- name: Enterprise Tests
|
||||
build-static-viz: false
|
||||
test-args: >-
|
||||
:only '"enterprise/backend/test"'
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 1)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 2)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
|
||||
|
||||
services:
|
||||
mysql:
|
||||
@@ -260,10 +244,6 @@ jobs:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
||||
MYSQL_DATABASE: mb_test
|
||||
env:
|
||||
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
|
||||
# GDGT-3126: re-enable and properly fix old migrations tests
|
||||
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
|
||||
# actual serious env vars below
|
||||
CI: 'true'
|
||||
DRIVERS: mysql
|
||||
MB_DB_TYPE: mysql
|
||||
@@ -298,18 +278,7 @@ jobs:
|
||||
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
|
||||
# "be-tests-mariadb-10-6-ee (0)"
|
||||
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
build-static-viz: ${{ matrix.job.build-static-viz }}
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
steps: *app-db-test-steps
|
||||
|
||||
be-tests-postgres:
|
||||
needs: app-db-matrix
|
||||
@@ -320,32 +289,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ${{ fromJSON(needs.app-db-matrix.outputs.postgres) }}
|
||||
job:
|
||||
- name: Enterprise Tests
|
||||
build-static-viz: false
|
||||
test-args: >-
|
||||
:only '"enterprise/backend/test"'
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 1)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
- name: EE App DB Tests (Part 2)
|
||||
build-static-viz: true
|
||||
test-args: >-
|
||||
:only '"test"'
|
||||
:partition/total 2
|
||||
:partition/index 1
|
||||
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
|
||||
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
|
||||
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
|
||||
env:
|
||||
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
|
||||
# GDGT-3126: re-enable and properly fix old migrations tests
|
||||
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
|
||||
# actual serious env vars below
|
||||
CI: 'true'
|
||||
DRIVERS: postgres
|
||||
MB_DB_TYPE: postgres
|
||||
@@ -374,18 +320,7 @@ jobs:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
build-static-viz: ${{ matrix.job.build-static-viz }}
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
artifact-suffix: ${{ matrix.job.name }}
|
||||
test-args: >-
|
||||
${{ matrix.job.test-args }}
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
steps: *app-db-test-steps
|
||||
|
||||
app-db-tests-result:
|
||||
needs:
|
||||
|
||||
18
.github/workflows/translation-update.yml
vendored
18
.github/workflows/translation-update.yml
vendored
@@ -144,12 +144,20 @@ jobs:
|
||||
EOF
|
||||
)"
|
||||
|
||||
- name: Approve and Merge the PR
|
||||
run: |
|
||||
gh pr review --approve "$PR_NUMBER"
|
||||
gh pr merge --squash --auto "$PR_NUMBER"
|
||||
# Approving takes a different token than the one that opened the PR (above, the automation
|
||||
# user's): GitHub won't let a token approve its own pull request.
|
||||
- name: Approve the PR
|
||||
run: gh pr review --approve "$PR_NUMBER"
|
||||
shell: bash
|
||||
env:
|
||||
# uses a different token than the token that created the PR so we can approve and merge it
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number }}
|
||||
|
||||
# Merging goes back through the automation user: GitHub starts no workflow run for a push made
|
||||
# with `GITHUB_TOKEN`, so an auto-merge enabled with it lands on master with no CI.
|
||||
- name: Merge the PR
|
||||
run: gh pr merge --squash --auto "$PR_NUMBER"
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number }}
|
||||
|
||||
22
bb.edn
22
bb.edn
@@ -5,7 +5,9 @@
|
||||
;; Let Mage use the same module-resolution rules as the Kondo hooks.
|
||||
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src" ".clj-kondo/src"]
|
||||
;; use Git version of dep temporarily until a new release > 0.16.4 is cut
|
||||
:deps {dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
|
||||
:deps {;; pinned to the same version as the backend's deps.edn; used by `mage clj-ts` to render HoneySQL as SQL
|
||||
com.github.seancorfield/honeysql {:mvn/version "2.7.1437"}
|
||||
dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
|
||||
:git/url "https://github.com/weavejester/cljfmt"} ;; run cljfmt from bb
|
||||
io.github.paintparty/bling {:mvn/version "0.8.8"} ;; printing bells and whistles
|
||||
medley/medley {:mvn/version "1.3.0"} ;; utilities
|
||||
@@ -235,6 +237,24 @@
|
||||
(nrepl-eval "metabase.comments.core" code (:port opts))
|
||||
(nrepl-eval "metabase.comments.core" code)))}
|
||||
|
||||
clj-ts
|
||||
{:doc "Browse a PR, your local changes, or any Clojure file translated into TypeScript-ish code for easier reading"
|
||||
:examples [["./bin/mage clj-ts" "start the viewer; its home page shows your local changes (vs. the merge-base with master)"]
|
||||
["./bin/mage clj-ts --pr 82600" "view a GitHub PR (a PR URL works too)"]
|
||||
["./bin/mage clj-ts --branch my-feature" "view a branch compared to the branch it was created from"]
|
||||
["./bin/mage clj-ts src/metabase/collections/models/collection.clj" "view one file"]
|
||||
["./bin/mage clj-ts --print src/metabase/collections/models/collection.clj" "print one file's translation"]
|
||||
["./bin/mage clj-ts --stats src" "translate every file under src and report fallbacks"]]
|
||||
:options [["-p" "--pr PR" "GitHub PR number or URL to open"]
|
||||
[nil "--branch BRANCH" "Branch to compare against its parent branch (or --base)"]
|
||||
["-b" "--base REF" "Base ref for local changes or --branch (default: origin/master / the parent branch)"]
|
||||
[nil "--port PORT" "Port for the local server" :default 7788 :parse-fn parse-long]
|
||||
[nil "--print FILE" "Print the translation of FILE instead of starting the server"]
|
||||
[nil "--stats" "Translate files under the given directories and report coverage"]]
|
||||
:arg-schema [:sequential [:string {:name "file-or-dir" :description "A file to open (or directories for --stats)"}]]
|
||||
:requires [[mage.clj-ts.cli :as clj-ts-cli]]
|
||||
:task (task! (clj-ts-cli/run parsed))}
|
||||
|
||||
run-tests
|
||||
{:doc "Quickly run tests locally. Pass dirs, files, namespaces, or single tests as arguments."
|
||||
:examples [["./bin/mage run-tests" "launch interactive test picker"]
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
metabase:
|
||||
image: ${METABASE_IMAGE:-metabase/metabase:latest}
|
||||
ports:
|
||||
- "${METABASE_PORT:-3000}:3000"
|
||||
- "127.0.0.1:${METABASE_PORT:-3000}:3000"
|
||||
environment:
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_DBNAME: metabase
|
||||
|
||||
6
deps.edn
6
deps.edn
@@ -941,8 +941,10 @@
|
||||
{:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}}
|
||||
:main-opts ["-m" "nrepl.cmdline" "-p" "50605"]}
|
||||
|
||||
;; - start a Socket REPL on port 50505 that you can connect your editor to:
|
||||
:socket {:jvm-opts ["-Dclojure.server.repl={:address,\"0.0.0.0\",:port,50505,:accept,clojure.core.server/repl}"]}
|
||||
;; - start a Socket REPL on port 50505 that you can connect your editor to. Bound to loopback: it has no
|
||||
;; authentication, so exposing it on a network interface is remote code execution. dev/docker-compose.yml
|
||||
;; overrides the address for the containerized dev app, where the published port is the boundary.
|
||||
:socket {:jvm-opts ["-Dclojure.server.repl={:address,\"127.0.0.1\",:port,50505,:accept,clojure.core.server/repl}"]}
|
||||
|
||||
;; Liquibase CLI:
|
||||
;;
|
||||
|
||||
@@ -39,8 +39,8 @@ services:
|
||||
image: clojure
|
||||
container_name: metabase-app
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "50505:50505"
|
||||
- "127.0.0.1:3001:3000"
|
||||
- "127.0.0.1:50505:50505"
|
||||
working_dir: /dev/metabase
|
||||
volumes:
|
||||
- ../:/dev/metabase
|
||||
@@ -58,7 +58,10 @@ services:
|
||||
MB_POSTGRESQL_TEST_PASSWORD: password
|
||||
MB_POSTGRESQL_TEST_PORT: 5432
|
||||
MB_POSTGRESQL_TEST_USER: user
|
||||
command: ["clojure", "-M:run:dev:drivers:drivers-dev:ee:ee-dev:socket"]
|
||||
# The :socket alias binds the REPL to loopback inside the container, which docker's port
|
||||
# forwarding can't reach, so bind it to all container interfaces here instead. The host
|
||||
# side stays loopback-only via the 127.0.0.1: prefix in `ports` above.
|
||||
command: ["clojure", "-J-Dclojure.server.repl={:address,\"0.0.0.0\",:port,50505,:accept,clojure.core.server/repl}", "-M:run:dev:drivers:drivers-dev:ee:ee-dev"]
|
||||
profiles:
|
||||
- dev-app
|
||||
networks:
|
||||
@@ -79,10 +82,14 @@ services:
|
||||
# the browser loads the bundle from the host's :8080 — it must be
|
||||
# published, not just exposed on the docker network.
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- ../:/dev/metabase
|
||||
- ~/.bun/install/cache:/root/.bun/install/cache
|
||||
environment:
|
||||
# rspack binds to loopback by default; inside the container it must listen on all
|
||||
# interfaces for docker's port forwarding to reach it.
|
||||
MB_FRONTEND_DEV_HOST: 0.0.0.0
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
@@ -99,7 +106,7 @@ services:
|
||||
container_name: metabase-postgres-app-db
|
||||
restart: always
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: mbuser
|
||||
POSTGRES_PASSWORD: password
|
||||
@@ -116,7 +123,7 @@ services:
|
||||
container_name: metabase-mysql57-app-db
|
||||
restart: always
|
||||
ports:
|
||||
- "3306:3306"
|
||||
- "127.0.0.1:3306:3306"
|
||||
environment:
|
||||
MYSQL_USER: mbuser
|
||||
MYSQL_PASSWORD: password
|
||||
@@ -138,7 +145,7 @@ services:
|
||||
image: metabase/qa-databases:postgres-sample-14
|
||||
container_name: metabasedb_postgres_14_data
|
||||
ports:
|
||||
- "15432:5432"
|
||||
- "127.0.0.1:15432:5432"
|
||||
restart: always
|
||||
profiles:
|
||||
- postgres14data
|
||||
@@ -149,7 +156,7 @@ services:
|
||||
image: metabase/qa-databases:mysql-sample-5.7
|
||||
container_name: metabasedb_mysql_5_7_data
|
||||
ports:
|
||||
- "23306:3306"
|
||||
- "127.0.0.1:23306:3306"
|
||||
restart: always
|
||||
profiles:
|
||||
- mysql57data
|
||||
@@ -160,7 +167,7 @@ services:
|
||||
image: metabase/qa-databases:mysql-sample-8
|
||||
container_name: metabasedb_mysql_8_data
|
||||
ports:
|
||||
- "13306:3306"
|
||||
- "127.0.0.1:13306:3306"
|
||||
restart: always
|
||||
profiles:
|
||||
- mysql8data
|
||||
@@ -172,7 +179,7 @@ services:
|
||||
container_name: metabasedb_mongo_6_data
|
||||
restart: always
|
||||
ports:
|
||||
- "27017:27017"
|
||||
- "127.0.0.1:27017:27017"
|
||||
profiles:
|
||||
- mongodata
|
||||
networks:
|
||||
@@ -183,7 +190,7 @@ services:
|
||||
container_name: vertica_data
|
||||
restart: always
|
||||
ports:
|
||||
- "5433:5433"
|
||||
- "127.0.0.1:5433:5433"
|
||||
profiles:
|
||||
- verticadata
|
||||
networks:
|
||||
@@ -195,8 +202,8 @@ services:
|
||||
image: maildev/maildev:latest
|
||||
container_name: maildev
|
||||
ports:
|
||||
- "1080:1080"
|
||||
- "1025:1025"
|
||||
- "127.0.0.1:1080:1080"
|
||||
- "127.0.0.1:1025:1025"
|
||||
profiles:
|
||||
- pulse
|
||||
networks:
|
||||
@@ -209,7 +216,7 @@ services:
|
||||
- ./idp-private-key.pem:/idp-private-key.pem
|
||||
- ./idp-public-cert.pem:/idp-public-cert.pem
|
||||
ports:
|
||||
- "7000:7000"
|
||||
- "127.0.0.1:7000:7000"
|
||||
entrypoint:
|
||||
- node
|
||||
- app.js
|
||||
|
||||
@@ -257,6 +257,38 @@ Credentials:
|
||||
- **Region**. Pick one from the dropdown in **Admin > AI**. Defaults to `us-east-1`. You can also set it with the environment variable `MB_LLM_BEDROCK_REGION`.
|
||||
- **Session token** (advanced). Only together with **Access key ID** and **Secret access key**. Only needed for temporary credentials. You can also set it with the environment variable `MB_LLM_BEDROCK_SESSION_TOKEN`.
|
||||
|
||||
### IAM permissions for Bedrock
|
||||
|
||||
Metabase talks to Bedrock through the mantle endpoint, `https://bedrock-mantle.{region}.api.aws`, not through `bedrock-runtime`. Mantle is a separate IAM namespace with its own actions, so a policy written against the `bedrock` prefix won't grant access. Metabase lists models and runs conversations, so it needs `bedrock-mantle:ListModels` and `bedrock-mantle:CreateInference`.
|
||||
|
||||
Here's a least-privilege policy that grants both:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock-mantle:ListModels", "bedrock-mantle:CreateInference"],
|
||||
"Resource": "arn:aws:bedrock-mantle:*:*:project/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The AWS managed policy [AmazonBedrockMantleInferenceAccess](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonBedrockMantleInferenceAccess.html) also covers both actions (along with permissions Metabase doesn't use).
|
||||
|
||||
If Metabase reports "AWS Bedrock credentials lack permission for this model or action", check that your policy uses the `bedrock-mantle` prefix.
|
||||
|
||||
### The Bedrock models you can pick depend on the region
|
||||
|
||||
The table above lists the models Metabase can use. The **Models** card only offers the ones Bedrock serves in the connection's region, so what you can pick depends on the region. The mantle catalog has no cross-region inference profiles, so a model that isn't served in your region can't be reached from that region at all.
|
||||
|
||||
If the model list is empty or shorter than you expect after connecting:
|
||||
|
||||
- **Check the region**: the **Region** dropdown lists every AWS region, including regions where Bedrock serves none of these models. The [AWS model cards](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html) show where each model is available. For example, the GPT models are only served in US regions.
|
||||
- **Check your account's data retention setting**: Bedrock marks a model unavailable when your account's data retention mode doesn't meet what that model requires. For example, [Claude Fable 5](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-fable-5.html) requires the `aws_review` data retention mode.
|
||||
|
||||
## vLLM
|
||||
|
||||
- Provider key: `vllm`
|
||||
|
||||
@@ -58,7 +58,7 @@ To connect a provider with your own credentials:
|
||||
1. Go to **Admin > AI**.
|
||||
2. Click **Add a provider**.
|
||||
3. Pick your provider.
|
||||
4. Fill in its credentials. The **Where do I find this?** links open the provider's key management pages in a new tab.
|
||||
4. Enter your credentials. Most providers only need an **API key**. The **Where do I find this?** link opens your provider's key management page in a new tab. For what each provider needs, see [AI providers](./providers.md).
|
||||
5. Click **Connect**.
|
||||
|
||||
If you've already copied a key, one neat thing: just paste the key anywhere on the provider grid, and Metabase selects the matching provider and fills in the key for you. Check to make sure the provider matches.
|
||||
|
||||
8759
docs/api.json
generated
8759
docs/api.json
generated
File diff suppressed because it is too large
Load Diff
57
docs/cloud/ip-allowlist.md
Normal file
57
docs/cloud/ip-allowlist.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "IP allowlist"
|
||||
version: latest
|
||||
has_magic_breadcrumbs: true
|
||||
show_category_breadcrumb: true
|
||||
show_title_breadcrumb: true
|
||||
category: "Cloud"
|
||||
layout: new-docs
|
||||
---
|
||||
|
||||
# IP allowlist
|
||||
|
||||
Use an IP allowlist to limit access to your Metabase instance to approved IP addresses. The allowlist applies to all access to your instance, so people outside the allowlist can't reach the API, embedded dashboards and questions, or public links.
|
||||
|
||||
IP allowlists are only available on Metabase Cloud [Pro or Enterprise](https://www.metabase.com/pricing) plans. To restrict access to a self-hosted Metabase, use your own firewall or load balancer, or consider [air-gapping](https://www.metabase.com/product/air-gapping).
|
||||
|
||||
The allowlist accepts IPv4 addresses and CIDR ranges. Metabase treats a bare IPv4 address as a `/32` range.
|
||||
|
||||
By default, restricting access by IP address is turned off.
|
||||
|
||||
To allow Metabase to reach your databases, see [IP addresses to whitelist](./ip-addresses-to-whitelist.md).
|
||||
|
||||
## Set up an IP allowlist
|
||||
|
||||
When you save an allowlist, your instance restarts. Metabase blocks requests from addresses outside the allowlist, so include the address or range you're connecting from before you save.
|
||||
|
||||
The allowlist doesn't apply to the Metabase Store. If you leave out your own address, you can still log in to the Store and update the allowlist.
|
||||
|
||||
1. Log in to your Metabase [Store account](https://store.metabase.com).
|
||||
2. Navigate to **Instances**.
|
||||
3. In the instance you want to restrict access to, click **Settings**.
|
||||
4. Scroll to **IP allowlist**.
|
||||
5. Enable the **Restrict access by IP address** toggle.
|
||||
6. In the **IP/CIDR ranges** field, enter an IPv4 address or a CIDR range.
|
||||
7. To add more addresses, enter them in the empty field. To add several at once, paste a list with one address or range per line.
|
||||
8. Click **Save**.
|
||||
9. Review the warning, then confirm. Your instance restarts.
|
||||
|
||||
Metabase validates each entry as you type. To save the allowlist, you must first correct or remove invalid entries.
|
||||
|
||||
To remove an address, click the trash icon next to the address or range, then click **Save**.
|
||||
|
||||
## Turn off the IP allowlist
|
||||
|
||||
When you turn off the IP allowlist and save, Metabase clears the saved addresses. To restrict access again, you must re-enter the addresses.
|
||||
|
||||
Turning off the allowlist doesn't remove authentication. People still need to sign in to your Metabase.
|
||||
|
||||
In your instance's **IP allowlist** settings:
|
||||
|
||||
1. Disable the **Restrict access by IP address** toggle.
|
||||
2. Click **Save**.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [IP addresses to whitelist](./ip-addresses-to-whitelist.md)
|
||||
- [Changing your domain name](./custom-domain.md)
|
||||
@@ -29,6 +29,7 @@ Manage your Metabase Cloud instance, subscription, and billing via the [Metabase
|
||||
|
||||
- [Changing which region your Metabase is hosted in](./change-region.md)
|
||||
- [IP addresses to whitelist](ip-addresses-to-whitelist.md)
|
||||
- [IP allowlist](./ip-allowlist.md)
|
||||
- [Changing your domain name](./custom-domain.md)
|
||||
|
||||
### Migrations
|
||||
|
||||
@@ -147,7 +147,9 @@ config:
|
||||
embedding-homepage: hidden
|
||||
embedding-secret-key: null
|
||||
enable-embedding-interactive: false
|
||||
enable-embedding-modular: false
|
||||
enable-embedding-sdk: false
|
||||
enable-embedding-simple: false
|
||||
enable-embedding-static: false
|
||||
enable-password-login: true
|
||||
enable-pivoted-exports: true
|
||||
enable-public-sharing: true
|
||||
@@ -186,7 +188,7 @@ config:
|
||||
landing-page-illustration: default
|
||||
landing-page-illustration-custom: null
|
||||
ldap-attribute-email: mail
|
||||
ldap-attribute-firstname: givenName
|
||||
ldap-attribute-firstname: givenname
|
||||
ldap-attribute-lastname: sn
|
||||
ldap-bind-dn: null
|
||||
ldap-enabled: false
|
||||
|
||||
@@ -731,17 +731,7 @@ Allow admins to securely embed questions and dashboards within other application
|
||||
|
||||
Allow admins to embed Metabase via interactive embedding?
|
||||
|
||||
### `MB_ENABLE_EMBEDDING_MODULAR`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
- [Configuration file name](./config-file.md): `enable-embedding-modular`
|
||||
|
||||
Allow admins to embed Metabase via modular embedding, the SDK for React, and guest embeds?
|
||||
|
||||
### `MB_ENABLE_EMBEDDING_SDK [DEPRECATED]`
|
||||
|
||||
> DEPRECATED: 0.65.0
|
||||
### `MB_ENABLE_EMBEDDING_SDK`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
@@ -749,9 +739,7 @@ Allow admins to embed Metabase via modular embedding, the SDK for React, and gue
|
||||
|
||||
Allow admins to embed Metabase via the SDK?
|
||||
|
||||
### `MB_ENABLE_EMBEDDING_SIMPLE [DEPRECATED]`
|
||||
|
||||
> DEPRECATED: 0.65.0
|
||||
### `MB_ENABLE_EMBEDDING_SIMPLE`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
@@ -759,9 +747,7 @@ Allow admins to embed Metabase via the SDK?
|
||||
|
||||
Allow admins to embed Metabase via modular embedding?
|
||||
|
||||
### `MB_ENABLE_EMBEDDING_STATIC [DEPRECATED]`
|
||||
|
||||
> DEPRECATED: 0.65.0
|
||||
### `MB_ENABLE_EMBEDDING_STATIC`
|
||||
|
||||
- Type: boolean
|
||||
- Default: `false`
|
||||
@@ -1165,7 +1151,7 @@ Attribute to use for the user's email. (usually 'mail', 'email' or 'userPrincipa
|
||||
### `MB_LDAP_ATTRIBUTE_FIRSTNAME`
|
||||
|
||||
- Type: string
|
||||
- Default: `givenName`
|
||||
- Default: `givenname`
|
||||
- [Configuration file name](./config-file.md): `ldap-attribute-firstname`
|
||||
|
||||
Attribute to use for the user's first name. (usually 'givenName').
|
||||
@@ -1433,7 +1419,7 @@ Backed by the bedrock connection in the admin AI settings provider list: reads a
|
||||
|
||||
The AWS region for Amazon Bedrock (e.g. us-east-1).
|
||||
|
||||
Backed by the bedrock connection in the admin AI settings provider list: reads and writes go through the llm-providers connection list, and a value set by this environment variable shadows this one field of that connection. On a self-hosted Metabase, setting only the region enables Bedrock with the AWS default credentials chain, with no access keys configured.
|
||||
Backed by the bedrock connection in the admin AI settings provider list: reads and writes go through the llm-providers connection list, and a value set by this environment variable shadows this one field of that connection. On a self-hosted Metabase, setting only the region enables Bedrock with the AWS default credentials chain, with no access keys configured. [The Bedrock models you can pick depend on the region](../ai/providers.md#the-bedrock-models-you-can-pick-depend-on-the-region).
|
||||
|
||||
### `MB_LLM_BEDROCK_SECRET_ACCESS_KEY`
|
||||
|
||||
|
||||
@@ -343,11 +343,12 @@ To configure a dashboard using its URL, you can add the following optional keywo
|
||||
|
||||
- `fullscreen`
|
||||
- `refresh`
|
||||
- `theme`
|
||||
|
||||
Here's an example URL:
|
||||
|
||||
```
|
||||
https://metabase.mydomain.com/dash/2#refresh=60&fullscreen&night
|
||||
https://metabase.mydomain.com/dash/2#refresh=60&fullscreen&theme=night
|
||||
```
|
||||
|
||||
The part that says `refresh=60` sets the dashboard to automatically refresh every 60 seconds, `fullscreen` sets it to fullscreen mode. Use an ampersand, `&`, in between keywords, and make sure there's a hash, `#`, after the dashboard's ID number.
|
||||
|
||||
@@ -33,7 +33,7 @@ Once Docker is installed, you’re ready to go.
|
||||
2. Copy and paste this command, switching out `<branch-name>` for the name of the branch you’d like to test:
|
||||
|
||||
```bash
|
||||
docker run --platform linux/amd64 -d -p 3000:3000 --name metabase-dev metabase/metabase-dev:<branch-name>
|
||||
docker run --platform linux/amd64 -d -p 127.0.0.1:3000:3000 --name metabase-dev metabase/metabase-dev:<branch-name>
|
||||
```
|
||||
|
||||
3. In your browser, navigate to `http://localhost:3000`, where you should see Metabase. It may take a minute or two to start up depending on your computer.
|
||||
@@ -51,7 +51,7 @@ docker pull metabase/metabase-enterprise-head:latest
|
||||
Then:
|
||||
|
||||
```
|
||||
docker run --platform linux/amd64 -d -p 3000:3000 --name metabase metabase/metabase-enterprise-head:latest
|
||||
docker run --platform linux/amd64 -d -p 127.0.0.1:3000:3000 --name metabase metabase/metabase-enterprise-head:latest
|
||||
```
|
||||
|
||||
The “latest” tag is not automatically upgraded on your local machine, so the above commands ensure that you’re pulling the latest changes.
|
||||
|
||||
@@ -6,6 +6,20 @@ title: Driver interface changelog
|
||||
|
||||
## Metabase 0.64.0
|
||||
|
||||
- `metabase.driver.sql.normalize/default-schema` now takes the database as well as the driver:
|
||||
`[driver database]`. The schema an unqualified table reference resolves to is a property of the connection for a
|
||||
driver that opens a database where others have a default schema — ClickHouse now answers with the database its
|
||||
connection opened, where before it inherited `"public"` from `:sql`. `database` may be nil when the caller has none
|
||||
in hand, and such a driver answers nil for it.
|
||||
|
||||
- New feature `:transforms/testing` -- whether the driver can run transform test suites against temp tables. Drivers
|
||||
with this feature implement the new multimethods `metabase.driver/temp-table-name` `[driver]`,
|
||||
`metabase.driver/compile-create-temp-table` `[driver {:keys [table query]}]`,
|
||||
`metabase.driver/compile-drop-temp-table` `[driver table]`,
|
||||
`metabase.driver/do-with-test-connection` `[driver database f]`,
|
||||
`metabase.driver/execute-on-connection!` `[driver conn query]` and
|
||||
`metabase.driver/query-on-connection` `[driver conn query {:keys [max-rows]}]`.
|
||||
|
||||
- `metabase.driver.sql-jdbc.execute/cancelation-poisons-connection?` `[driver]` -- whether canceling a `Statement`
|
||||
leaves the `Connection` unfit for the next query, so that it must be discarded rather than returned to the
|
||||
connection pool. The query processor cancels a `Statement` whenever reduction stops before the `ResultSet` runs out
|
||||
|
||||
@@ -171,7 +171,7 @@ We have a few helpers for dealing with tests involving snowplow
|
||||
Some of our tests depend on the email being set up, and require a local SMTP server. We use `maildev` Docker image for that purpose. At the time of this writing the image we use is `maildev/maildev:2.1.0`. The default cypress configuration for local development will handle this for you. If you want to set it up manually, you can use this command:
|
||||
|
||||
```sh
|
||||
docker run -d -p 1180:1080 -p 1125:1025 maildev/maildev:2.2.1
|
||||
docker run -d -p 127.0.0.1:1180:1080 -p 127.0.0.1:1125:1025 maildev/maildev:2.2.1
|
||||
```
|
||||
|
||||
The e2e maildev is published on host ports 1180 (web UI) and 1125 (SMTP) on purpose. These differ, on purpose, from the 1080/1025 used for development.
|
||||
|
||||
11
docs/embedding/sdk/api/DataAppLink.html
Normal file
11
docs/embedding/sdk/api/DataAppLink.html
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: DataAppLink
|
||||
layout: docs-api
|
||||
redirect_from:
|
||||
- /docs/latest/embedding/api/DataAppLink
|
||||
---
|
||||
|
||||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>DataAppLink | Embedded analytics SDK API</title><link rel="icon" href="assets/favicon.ico"/>{% include docs/embedded-analytics-sdk-metadata.html %}<meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><link rel="stylesheet" href="assets/custom.css"/><script defer src="assets/main.js"></script><script defer src="assets/custom.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">Embedded analytics SDK API</a><div id="tsd-toolbar-links"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })">Back to documentation</a></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">DataAppLink</a></li></ul><h1>Variable DataAppLink<code class="tsd-tag">Const</code></h1></div><div class="tsd-signature"><span class="tsd-kind-variable">DataAppLink</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><br/> <span class="tsd-kind-parameter">{ to, children, onClick, target, rel, ...rest }</span><span class="tsd-signature-symbol">:</span> <a href="DataAppLinkProps.html" class="tsd-signature-type tsd-kind-interface">DataAppLinkProps</a><span class="tsd-signature-symbol">,</span><br/><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></div><div class="tsd-comment tsd-typography"><p>Internal-only navigation link inside a data app.</p>
|
||||
<p>Renders a plain anchor rather than a router <code><Link></code>: a data app is mounted
|
||||
outside the app's route tree, so it has no router context to read.</p>
|
||||
</div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><br/> <span class="tsd-kind-parameter">{ to, children, onClick, target, rel, ...rest }</span><span class="tsd-signature-symbol">:</span> <a href="DataAppLinkProps.html" class="tsd-signature-type tsd-kind-interface">DataAppLinkProps</a><span class="tsd-signature-symbol">,</span><br/><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">{ to, children, onClick, target, rel, ...rest }</span>: <a href="DataAppLinkProps.html" class="tsd-signature-type tsd-kind-interface">DataAppLinkProps</a></span></li></ul></div><h4 class="tsd-returns-title">Returns <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></h4></li></ul></li></ul></div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav id="tsd-sidebar-links" class="tsd-navigation"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })" class="tsd-nav-link">Back to documentation</a></nav><nav class="tsd-navigation"><a href="index.html">Embedded analytics SDK API</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p><script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script><script type="text/javascript" src="/js/cookie-consent.js"></script><link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" /><link href="/css/gdpr.css" rel="stylesheet" /></p></footer><div class="overlay"></div></body></html>
|
||||
10
docs/embedding/sdk/api/DataAppLinkProps.html
Normal file
10
docs/embedding/sdk/api/DataAppLinkProps.html
Normal file
File diff suppressed because one or more lines are too long
13
docs/embedding/sdk/api/DataAppRouter.html
Normal file
13
docs/embedding/sdk/api/DataAppRouter.html
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: DataAppRouter
|
||||
layout: docs-api
|
||||
redirect_from:
|
||||
- /docs/latest/embedding/api/DataAppRouter
|
||||
---
|
||||
|
||||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>DataAppRouter | Embedded analytics SDK API</title><link rel="icon" href="assets/favicon.ico"/>{% include docs/embedded-analytics-sdk-metadata.html %}<meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><link rel="stylesheet" href="assets/custom.css"/><script defer src="assets/main.js"></script><script defer src="assets/custom.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">Embedded analytics SDK API</a><div id="tsd-toolbar-links"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })">Back to documentation</a></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">DataAppRouter</a></li></ul><h1>Variable DataAppRouter<code class="tsd-tag">Const</code></h1></div><div class="tsd-signature"><span class="tsd-kind-variable">DataAppRouter</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">{ children }</span><span class="tsd-signature-symbol">:</span> <a href="DataAppRouterProps.html" class="tsd-signature-type tsd-kind-interface">DataAppRouterProps</a><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></div><div class="tsd-comment tsd-typography"><p>Wrap your data-app tree once. Inside, use <code><DataAppLink to="…"></code> for
|
||||
navigation and <code>useDataAppLocation()</code> to read the current path.</p>
|
||||
<p>No <code>basename</code> prop: it is auto-detected from the iframe URL
|
||||
(<code>/embed/apps/<name></code>). In the dev preview, where there is no prefix, the
|
||||
basename resolves to <code>""</code> and the sub-path is just the raw pathname.</p>
|
||||
</div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">{ children }</span><span class="tsd-signature-symbol">:</span> <a href="DataAppRouterProps.html" class="tsd-signature-type tsd-kind-interface">DataAppRouterProps</a><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">{ children }</span>: <a href="DataAppRouterProps.html" class="tsd-signature-type tsd-kind-interface">DataAppRouterProps</a></span></li></ul></div><h4 class="tsd-returns-title">Returns <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6" class="tsd-signature-type external" target="_blank">JSX_3.Element</a></h4></li></ul></li></ul></div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav id="tsd-sidebar-links" class="tsd-navigation"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })" class="tsd-nav-link">Back to documentation</a></nav><nav class="tsd-navigation"><a href="index.html">Embedded analytics SDK API</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p><script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script><script type="text/javascript" src="/js/cookie-consent.js"></script><link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" /><link href="/css/gdpr.css" rel="stylesheet" /></p></footer><div class="overlay"></div></body></html>
|
||||
9
docs/embedding/sdk/api/DataAppRouterProps.html
Normal file
9
docs/embedding/sdk/api/DataAppRouterProps.html
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: DataAppRouterProps
|
||||
layout: docs-api
|
||||
redirect_from:
|
||||
- /docs/latest/embedding/api/DataAppRouterProps
|
||||
---
|
||||
|
||||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>DataAppRouterProps | Embedded analytics SDK API</title><link rel="icon" href="assets/favicon.ico"/>{% include docs/embedded-analytics-sdk-metadata.html %}<meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><link rel="stylesheet" href="assets/custom.css"/><script defer src="assets/main.js"></script><script defer src="assets/custom.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">Embedded analytics SDK API</a><div id="tsd-toolbar-links"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })">Back to documentation</a></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">DataAppRouterProps</a></li></ul><h1>Interface DataAppRouterProps</h1></div><div class="tsd-signature"><span class="tsd-signature-keyword">interface</span> <span class="tsd-kind-interface">DataAppRouterProps</span> <span class="tsd-signature-symbol">{</span><br/> <a class="tsd-kind-property" href="#children">children</a><span class="tsd-signature-symbol">?:</span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/index.d.ts#L478" class="tsd-signature-type external" target="_blank">ReactNode</a><span class="tsd-signature-symbol">;</span><br/><span class="tsd-signature-symbol">}</span></div><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h5 class="tsd-index-heading uppercase">Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Properties</h3><div class="tsd-index-list"><a href="#children" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>children?</span></a>
|
||||
</div></section></div></details></section></section><details class="tsd-panel-group tsd-member-group tsd-accordion" open><summary class="tsd-accordion-summary" data-key="section-Properties"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h2>Properties</h2></summary><section><section class="tsd-panel tsd-member"><h3 class="tsd-anchor-link" id="children"><code class="tsd-tag">Optional</code><span>children</span><a href="#children" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><div class="tsd-signature"><span class="tsd-kind-property">children</span><span class="tsd-signature-symbol">?:</span> <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/index.d.ts#L478" class="tsd-signature-type external" target="_blank">ReactNode</a></div></section></section></details></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><details open class="tsd-accordion tsd-page-navigation-section"><summary class="tsd-accordion-summary" data-key="section-Properties"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg>Properties</summary><div><a href="#children"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>children</span></a></div></details></div></details></div><div class="site-menu"><nav id="tsd-sidebar-links" class="tsd-navigation"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })" class="tsd-nav-link">Back to documentation</a></nav><nav class="tsd-navigation"><a href="index.html">Embedded analytics SDK API</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p><script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script><script type="text/javascript" src="/js/cookie-consent.js"></script><link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" /><link href="/css/gdpr.css" rel="stylesheet" /></p></footer><div class="overlay"></div></body></html>
|
||||
10
docs/embedding/sdk/api/UseDataAppLocationResult.html
Normal file
10
docs/embedding/sdk/api/UseDataAppLocationResult.html
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: UseDataAppLocationResult
|
||||
layout: docs-api
|
||||
redirect_from:
|
||||
- /docs/latest/embedding/api/UseDataAppLocationResult
|
||||
---
|
||||
|
||||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>UseDataAppLocationResult | Embedded analytics SDK API</title><link rel="icon" href="assets/favicon.ico"/>{% include docs/embedded-analytics-sdk-metadata.html %}<meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><link rel="stylesheet" href="assets/custom.css"/><script defer src="assets/main.js"></script><script defer src="assets/custom.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">Embedded analytics SDK API</a><div id="tsd-toolbar-links"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })">Back to documentation</a></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">UseDataAppLocationResult</a></li></ul><h1>Type Alias UseDataAppLocationResult</h1></div><div class="tsd-signature"><span class="tsd-signature-keyword">type</span> <span class="tsd-kind-type-alias">UseDataAppLocationResult</span> <span class="tsd-signature-symbol">=</span> <span class="tsd-signature-symbol">{</span><br/> <a class="tsd-kind-property" href="#navigate">navigate</a><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">to</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span><span class="tsd-signature-symbol">;</span><br/> <a class="tsd-kind-property" href="#pathname">pathname</a><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">;</span><br/><span class="tsd-signature-symbol">}</span></div><section class="tsd-panel-group tsd-index-group"><section class="tsd-panel tsd-index-panel"><details class="tsd-index-content tsd-accordion" open><summary class="tsd-accordion-summary tsd-index-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h5 class="tsd-index-heading uppercase">Index</h5></summary><div class="tsd-accordion-details"><section class="tsd-index-section"><h3 class="tsd-index-heading">Properties</h3><div class="tsd-index-list"><a href="#navigate" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>navigate</span></a>
|
||||
<a href="#pathname" class="tsd-index-link"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>pathname</span></a>
|
||||
</div></section></div></details></section></section><details class="tsd-panel-group tsd-member-group tsd-accordion" open><summary class="tsd-accordion-summary" data-key="section-Properties"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h2>Properties</h2></summary><section><section class="tsd-panel tsd-member"><h3 class="tsd-anchor-link" id="navigate"><span>navigate</span><a href="#navigate" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><div class="tsd-signature"><span class="tsd-kind-property">navigate</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">to</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span></div></section><section class="tsd-panel tsd-member"><h3 class="tsd-anchor-link" id="pathname"><span>pathname</span><a href="#pathname" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3><div class="tsd-signature"><span class="tsd-kind-property">pathname</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span></div></section></section></details></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><details open class="tsd-accordion tsd-page-navigation-section"><summary class="tsd-accordion-summary" data-key="section-Properties"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg>Properties</summary><div><a href="#navigate"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>navigate</span></a><a href="#pathname"><svg class="tsd-kind-icon" viewBox="0 0 24 24" aria-label="Property"><use href="assets/icons.svg#icon-1024"></use></svg><span>pathname</span></a></div></details></div></details></div><div class="site-menu"><nav id="tsd-sidebar-links" class="tsd-navigation"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })" class="tsd-nav-link">Back to documentation</a></nav><nav class="tsd-navigation"><a href="index.html">Embedded analytics SDK API</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p><script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script><script type="text/javascript" src="/js/cookie-consent.js"></script><link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" /><link href="/css/gdpr.css" rel="stylesheet" /></p></footer><div class="overlay"></div></body></html>
|
||||
@@ -1 +1 @@
|
||||
window.navigationData = "eJylml1z3CYUhv/LXns6bdqkU985tje1k7Qbr51cZHLBSicrugjWCHnjdPLfO0i7EoLDl3plD+/LcwAhdID9/O9CwTe1OF9cCsagUFTw11IcGpCLs0VRUVZK4Ivzz0HfnqgKk36qVM0WZ4sd5eXi/NVvP878mJUU+ybA6vQp8MXLVz++mEwJRMEVaaqNILJ8L0rCfL3ArafgiBrqC2K3u+OzpPToQwuNHolgXwzTJOap3G7/2aJgpGkW54sS9hIKoqBceEOj3ZmI4Y4MPff04bqkimwYmL5jLEcKPIkbrkCSQtEnjISpAdhaEUULhGMJAYTTdmsccd0dynAPLajXEuJafbKQmBqirYgkNSiQlxXhW1iRZyaIMYK4bhF//uP3X16+8FPXopUFeKG9jDO/4CMaedFwpzvqvlfOfB8kZey+kqLdVp53zGtJnB6nOpcVker+eQ9XUuxLcbDjJFfJjKvntpCvW6WcrqW4Z0VLjTMnQpw9h3r6uwalKN826Q8pWHNmKz7SpiWMfidpHfZXy4x/Bw2o1KlimzNjrcn4/1LIOh4QrZEZ9Z4qBvFQoy24WJc7z6y0lcxWvibFLvUxWN7Y4o0RJJCdaFX6nEdrzIg8LHBr0FlmysKBV5kR+0ocuP7afaLlFjL6Hqj3v1uRG31u1CVlCmR6nxH/7Kip0eZGsdfj/BV8bmS9PqW+tZZ3TrS2romk3zPyCbxKLPb6kcXySK8ln21nkz5HPKF8D4psSKOX8SdaejfSJXylHE7mi1ZVl4J/pduxCT5HIK9EYh9htpIBsR4wKoc+NKE+xnrnPjq3xieqqos9fQvPIe7omhfh9qBi+NuDmsdek5rF4NqTSr9p3uhXLzTgjiVxYgsV2SW5LjPyKMQm4OjE5p+lhk8f+u1rpN2OabL3TWj11IjuoDPafF9BTfk2NMikget6A2VJ+Vbb4eML9znbjtCL2uU4l4IJaZLM0tQJ2NnRF/KkpJKm7Y/1Lz6Jhaq8S3LbwEUxffpD0ZT8ayzBFPVecOAqklQOtpT1sxs6ZCHuy1MInpfSUFIonieBPQDkjCk0RD5HiIpmPhkpzji9sSkfrW2fU7+jja7Z1jx0mm24ohHaRol6OHu71MfHwNsbBbURwG+K8SeVeo7LD5iy+LqSh6qlbFbflGsGNXAVAE98KVHwYbaVGGlYnK6IIita7MyMDBGTeddcUfWst6MIbxSjvMHZ733ewrN5Oo2oMeKSAivXRQU1GUFGYXTDUQj+FzGXl1NJfKtCFR1OofRK0k4WX1eNEd+JLXVARmHyx3Bye2CWJhMYLXb2pwkRZ/BWrN1S3vjSRJ8zIxE4rnzod+uo5dOm7yWup1OP3xnPh22qp1KRiyNHymZFnhbuS4+iyEpQrv7e/AMFstuxDNkp3Cn3C+apoymVvwRVVHfwqNOGe7EDvkQmG2JK5guulqSmDElnRy2V9oaJDWGRB4mYUvkR8izmhxbks29WGGIqr3uRVlIf5HvyyF5M5T002FHHg/MTBA9BqIstcNVlfe+hacjWXgUQRw45CJ3Bu4dvYaZhSOTqwQphLT35rvYjYS0Y309LiHKkUFAoKN0dpSPFWN1E1WvYyBiKkuoaN1AtUxZlKsZ4d+LQDcAIOZVEjy67PKrfPBg7J6M0jXBLnsi6kHSvpmkkpqYR78TBxtyJQ7RuecwvrvRxJ7UOX1wxmXdTIpyb+NHwYOX7VmEIXZ5AOW71jJxk2iBET6B6cZmcIVXos/xVu2G0cG8T/bacGFZTjfIcyjtByuunyUYPlROYfW+sZp0Kk+t3eQSC6MpTKFIKOWSXU9BEyma5DxLRU6h6PP8kvGQgnZQC1xOow48xglPP68qIYD3hsTiDgV3do3ICU39FrTb1RTkXVvYn1dViNB1T398DL28P6g6aveCN8Q3A9fjhqnmAih2wPpx0+wtqCaHzvt53/Q2KVkE3q0eMq8VGoq/xVqsWRZel1e7bvNQ/X2I7GzMRM3n9zyy9xF7OZF4BgwCzlzOZ2PBNxEze+pF5cevH6C2YVeNhX4ZGsZejTP6Mz1xLSHtH9ntGi+4k6njW5buNcIzGtcRUcy6m7JhPhDL9W1O9Y20CIW2fEXEixQJaP3m+2FN/UNQ7BnblwDWcZbYXS1QOX8XpFrRSAlcP/t/FOyaj+WN5bNDMq9/h5NETDvWOUV05KbhQkYhCuWGECjyQo6PbO+NXuaOUuFntlnbfTtUUE3k+VCLlYRgG5Ls2UXzLw5f/APtqHqE="
|
||||
window.navigationData = "eJylm8ty2zYUht9Fa0+nTZt06p3jS2rHaRXJchaZLGDyREIFAjQIWlE6efcORIkEgYMbu0oG/4/vACAIHgDy539nCr6p2fnsUjAGhaKCv5Vi14Ccnc2KDWWlBD47/xz01URtMOmnjarY7Gy2pbycnb/57ceZHzOXom4CrIM+Br56/ebHF5MpgSi4Is3mSRBZfhAlYb5e4NZTcEQN9QWx293xWVJ69LGFRo9EsC+GaRTzVG63/2xWMNI0s/NZCbWEgigoZ97QaHdGYrgjfc89fbguqSJPDEzfMZYjBZ7ELVcgSaHoC0bC1ABsqYiiBcKxhADCabs1jrjuDmW4hxbUawlxrT5ZSEwN0eZEkgoUyMsN4WuYkz0TxBhBXLeIP//x+y+vX/mpS9HKArzQTsaZX/ARjbxouNMddd8rZ74PkjL2sJGiXW8875jXkjg9TnUuN0Sqh30NV1LUpdjZcZKrZMbVc1vIt61STtdS3JOipcaZEiHOnkI9/bsEpShfN+kPKVhzYiseadMSRr+TtA77q2XGX0ADKnWq2ObMWEsy/P9GyCoeEK2RGfWBKgbxUIMtuFiXW8+stJXMVr4lxTb1MVje2OKNESSQrWhV+pxHa0yI3C9wS9BZZsrCgVeZEPtK7Lj+2n2i5Roy+h6o979bkRt9atQbyhTI9D4j/slRU6NNjWKvx/kr+NTIen1KfWst75RobVURSb9n5BN4lVjs5TOL5ZFeSz7bziZ9jnhC+QEUeSKNXsZfaOndSJfwlXI4mS9atbkU/CtdD03wOQJ5JRL7CLOVDIj1gFE59KEJ9THWO/fRuTU+UbW5qOl72Ie4g2tahLudiuHvdmoae0kqFoNrTyr9tnmnX73QgDuWxIktVGSX5LrMyIMQm4CDE5t/lho+fei2r5F2O6bR3jeh1WMjuoPOaPPDBirK16FBJg1cV09QlpSvtR0eX7nP2XaEXtRDjnMpmJAmySxNnYAHO/pCnpRU0rj9sf7FJ7FQG++SfEUUuajre8q3xj58KBzTfzWbe3QtRKvMVXdUHKjeNnBRjOddXxSudmqeKIhT39LizdedtI8hLCU0f0a9xTmGlnuOIapacOAqkqf3tpRP0mE2It+2rjyF4FnnDCWF4pnc2JxGju1CQ+RzhKhoMpmRNQ4rBraKRGvbR//3tNE124qHLggMVzRC2yhR9ceZl/pEHnh7q6AyAvhNMf6oUsdx+QFTFl9X8lC1lM3qmnLNoAKuAuCRLyUKPsy2EiP1671eUOa02JrLLSIm8665omqvd/gIbxCjvN7ZbSffw9488EfUGPGGAiuXxQYqMoCMwugerhD8L2IuL6eS+O6PKtof7OmVpB0tvq4aI96LNXVARmFyfjG6kDFLkwmMFlv7m4uIE3hz1q4pb3yZt8+ZkVsdVz70u3XU8mnj9xLX06nH74znwzbWU6nIXZwjZbMiTwv3pUdRZC4oV38//QMFsoG0DNlZ8SmdDqb+gymVfwOq2CzgWacND2IL/AaZbIgpmS+4uiEVZcgOYdBSae+YeCIs8iARUyo/Qp7E/NiC3PtmhSGm8g4v0lzquxFPHtmJqbxVg50erZxfdXgIQl2sgatD1vcBmoas7VUAceSQg9AJvAf4FmYahkSuHqwQ1tKTr78fCWvB+H5aQpQjhYJCQelu0h0pxjpMVL2GDYy+KKmucanXMmVRxmKMtxC7wwAMkFNJ9DT4kEd1mwdj52SUphHuyAtZFpLWapxGYmoacSF2NmYhdtG65TG/uNInyNQ6z3LFZN5tiXBu46ftvZXXrcIQujyBctzqGTnJuEGInkD14jI5farQZfnz9onRwr2g9dtyYlhNNcpzKPeClNcvo40eKicwu95YzToVJtc/5BEI4lCeQpFSyD67HINGUjbLfZCInkLV4/kn4SUD6aQUuJ5A7X/fEpx6XldGBOsJD8UZDOzXEKicwNRfUatNXVHOHaD9SXW1GG3lHLfanzSfI4Es9Y8tgJd3O7WApha8gREX0eMn4eaZM3YavjrpSEdMIXSS2Pmuv0HRKji8LwPG1WIj0dV4r1WLosvSandtvtG/NWNbGzMSM3ndb2K9xE7OZF4BgwCzkzOZ2PCNxEze8pl5ccvn6JWlVWNVl6FR7OQok+/xmWsJae9IXTPava/HUzTsZUGNxk3OWHNuEe2YL4Qy/cNgvRduAiFtnxFxJMUCWr9Pv6ipPyjqHQK7cuDO1DLbyzAqh+9NdQtaKYGrlf+PGByT0fyhPDZo5j19f6bpCYd6h6iunBRcqEhEodwwQgUeyNFx2JXj9+6DlLgNPiztvj2wKSbyfKhEyqofBuS7NlJ8y8OX/wAhHq/w"
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
33
docs/embedding/sdk/api/snippets/DataAppLink.md
Normal file
33
docs/embedding/sdk/api/snippets/DataAppLink.md
Normal file
@@ -0,0 +1,33 @@
|
||||
```ts
|
||||
const DataAppLink: ({
|
||||
to,
|
||||
children,
|
||||
onClick,
|
||||
target,
|
||||
rel,
|
||||
...rest
|
||||
}: DataAppLinkProps) => JSX_3.Element;
|
||||
```
|
||||
|
||||
Internal-only navigation link inside a data app.
|
||||
|
||||
Renders a plain anchor rather than a router `<Link>`: a data app is mounted
|
||||
outside the app's route tree, so it has no router context to read.
|
||||
|
||||
## Parameters
|
||||
|
||||
<!-- [<snippet parameters>] -->
|
||||
|
||||
| Parameter | Type |
|
||||
| :------------------------------------------------ | :---------------------------------------------- |
|
||||
| `{ to, children, onClick, target, rel, ...rest }` | [`DataAppLinkProps`](./api/DataAppLinkProps.md) |
|
||||
|
||||
<!-- [<endsnippet parameters>] -->
|
||||
|
||||
## Returns
|
||||
|
||||
<!-- [<snippet returns>] -->
|
||||
|
||||
[`JSX_3.Element`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6)
|
||||
|
||||
<!-- [<endsnippet returns>] -->
|
||||
18
docs/embedding/sdk/api/snippets/DataAppLinkProps.md
Normal file
18
docs/embedding/sdk/api/snippets/DataAppLinkProps.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## Extends
|
||||
|
||||
<!-- [<snippet extends>] -->
|
||||
|
||||
- [`Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys)\<[`AnchorHTMLAttributes`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/index.d.ts#L3100)\<[`HTMLAnchorElement`](https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement)\>, `"href"`\>
|
||||
|
||||
<!-- [<endsnippet extends>] -->
|
||||
|
||||
## Properties
|
||||
|
||||
<!-- [<snippet properties>] -->
|
||||
|
||||
| Property | Type |
|
||||
| :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| <a id="children"></a> `children?` | [`ReactNode`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/index.d.ts#L478) |
|
||||
| <a id="to"></a> `to` | `string` |
|
||||
|
||||
<!-- [<endsnippet properties>] -->
|
||||
28
docs/embedding/sdk/api/snippets/DataAppRouter.md
Normal file
28
docs/embedding/sdk/api/snippets/DataAppRouter.md
Normal file
@@ -0,0 +1,28 @@
|
||||
```ts
|
||||
const DataAppRouter: ({ children }: DataAppRouterProps) => JSX_3.Element;
|
||||
```
|
||||
|
||||
Wrap your data-app tree once. Inside, use `<DataAppLink to="…">` for
|
||||
navigation and `useDataAppLocation()` to read the current path.
|
||||
|
||||
No `basename` prop: it is auto-detected from the iframe URL
|
||||
(`/embed/apps/<name>`). In the dev preview, where there is no prefix, the
|
||||
basename resolves to `""` and the sub-path is just the raw pathname.
|
||||
|
||||
## Parameters
|
||||
|
||||
<!-- [<snippet parameters>] -->
|
||||
|
||||
| Parameter | Type |
|
||||
| :------------- | :-------------------------------------------------- |
|
||||
| `{ children }` | [`DataAppRouterProps`](./api/DataAppRouterProps.md) |
|
||||
|
||||
<!-- [<endsnippet parameters>] -->
|
||||
|
||||
## Returns
|
||||
|
||||
<!-- [<snippet returns>] -->
|
||||
|
||||
[`JSX_3.Element`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/jsx-runtime.d.ts#L6)
|
||||
|
||||
<!-- [<endsnippet returns>] -->
|
||||
9
docs/embedding/sdk/api/snippets/DataAppRouterProps.md
Normal file
9
docs/embedding/sdk/api/snippets/DataAppRouterProps.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Properties
|
||||
|
||||
<!-- [<snippet properties>] -->
|
||||
|
||||
| Property | Type |
|
||||
| :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| <a id="children"></a> `children?` | [`ReactNode`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/index.d.ts#L478) |
|
||||
|
||||
<!-- [<endsnippet properties>] -->
|
||||
17
docs/embedding/sdk/api/snippets/UseDataAppLocationResult.md
Normal file
17
docs/embedding/sdk/api/snippets/UseDataAppLocationResult.md
Normal file
@@ -0,0 +1,17 @@
|
||||
```ts
|
||||
type UseDataAppLocationResult = {
|
||||
navigate: (to: string) => void;
|
||||
pathname: string;
|
||||
};
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
<!-- [<snippet properties>] -->
|
||||
|
||||
| Property | Type |
|
||||
| :------------------------------- | :------------------------- |
|
||||
| <a id="navigate"></a> `navigate` | (`to`: `string`) => `void` |
|
||||
| <a id="pathname"></a> `pathname` | `string` |
|
||||
|
||||
<!-- [<endsnippet properties>] -->
|
||||
@@ -101,7 +101,12 @@
|
||||
|
||||
| Name | Description |
|
||||
| :---------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [DataAppLink](./api/DataAppLink.md) | Internal-only navigation link inside a data app. |
|
||||
| [DataAppRouter](./api/DataAppRouter.md) | Wrap your data-app tree once. Inside, use `<DataAppLink to="…">` for navigation and `useDataAppLocation()` to read the current path. |
|
||||
| [useAction](./api/useAction.md) | - |
|
||||
| [useDataAppLocation](./api/useDataAppLocation.md) | The current data-app sub-path and a `navigate` function. |
|
||||
| [DataAppLinkProps](./api/DataAppLinkProps.md) | - |
|
||||
| [DataAppRouterProps](./api/DataAppRouterProps.md) | - |
|
||||
| [InteractiveQuestionComponents](./api/InteractiveQuestionComponents.md) | - |
|
||||
| [MetabaseColors](./api/MetabaseColors.md) | - |
|
||||
| [MetabaseQuestion](./api/MetabaseQuestion.md) | - |
|
||||
@@ -169,6 +174,7 @@
|
||||
| [SdkQuestionTitleProps](./api/SdkQuestionTitleProps.md) | - |
|
||||
| [SdkUserId](./api/SdkUserId.md) | - |
|
||||
| [SqlParameterValues](./api/SqlParameterValues.md) | - |
|
||||
| [UseDataAppLocationResult](./api/UseDataAppLocationResult.md) | - |
|
||||
| [UserBackendJwtResponse](./api/UserBackendJwtResponse.md) | - |
|
||||
|
||||
## useAction
|
||||
|
||||
16
docs/embedding/sdk/api/snippets/useDataAppLocation.md
Normal file
16
docs/embedding/sdk/api/snippets/useDataAppLocation.md
Normal file
@@ -0,0 +1,16 @@
|
||||
```ts
|
||||
const useDataAppLocation: () => UseDataAppLocationResult;
|
||||
```
|
||||
|
||||
The current data-app sub-path and a `navigate` function.
|
||||
|
||||
Paths are relative to the data-app root: `/`, `/customers/42`, etc.
|
||||
`navigate(to)` switches sub-path without a full reload.
|
||||
|
||||
## Returns
|
||||
|
||||
<!-- [<snippet returns>] -->
|
||||
|
||||
[`UseDataAppLocationResult`](./api/UseDataAppLocationResult.md)
|
||||
|
||||
<!-- [<endsnippet returns>] -->
|
||||
11
docs/embedding/sdk/api/useDataAppLocation.html
Normal file
11
docs/embedding/sdk/api/useDataAppLocation.html
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: useDataAppLocation
|
||||
layout: docs-api
|
||||
redirect_from:
|
||||
- /docs/latest/embedding/api/useDataAppLocation
|
||||
---
|
||||
|
||||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>useDataAppLocation | Embedded analytics SDK API</title><link rel="icon" href="assets/favicon.ico"/>{% include docs/embedded-analytics-sdk-metadata.html %}<meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><link rel="stylesheet" href="assets/custom.css"/><script defer src="assets/main.js"></script><script defer src="assets/custom.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">Embedded analytics SDK API</a><div id="tsd-toolbar-links"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })">Back to documentation</a></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">useDataAppLocation</a></li></ul><h1>Variable useDataAppLocation<code class="tsd-tag">Const</code></h1></div><div class="tsd-signature"><span class="tsd-kind-variable">useDataAppLocation</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">()</span> <span class="tsd-signature-symbol">=></span> <a href="UseDataAppLocationResult.html" class="tsd-signature-type tsd-kind-type-alias">UseDataAppLocationResult</a></div><div class="tsd-comment tsd-typography"><p>The current data-app sub-path and a <code>navigate</code> function.</p>
|
||||
<p>Paths are relative to the data-app root: <code>/</code>, <code>/customers/42</code>, etc.
|
||||
<code>navigate(to)</code> switches sub-path without a full reload.</p>
|
||||
</div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">()</span><span class="tsd-signature-symbol">:</span> <a href="UseDataAppLocationResult.html" class="tsd-signature-type tsd-kind-type-alias">UseDataAppLocationResult</a></li><li class="tsd-description"><h4 class="tsd-returns-title">Returns <a href="UseDataAppLocationResult.html" class="tsd-signature-type tsd-kind-type-alias">UseDataAppLocationResult</a></h4></li></ul></li></ul></div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav id="tsd-sidebar-links" class="tsd-navigation"><a href="javascript:navigateBack({ fallbackUrl: '/docs/latest/embedding/sdk/introduction' })" class="tsd-nav-link">Back to documentation</a></nav><nav class="tsd-navigation"><a href="index.html">Embedded analytics SDK API</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p><script type="text/javascript" src="/gdpr-cookie-notice/dist/script.js"></script><script type="text/javascript" src="/js/cookie-consent.js"></script><link href="/gdpr-cookie-notice/dist/style.css" rel="stylesheet" /><link href="/css/gdpr.css" rel="stylesheet" /></p></footer><div class="overlay"></div></body></html>
|
||||
@@ -145,7 +145,6 @@ show-homepage-data
|
||||
show-homepage-pin-message
|
||||
show-homepage-xrays
|
||||
show-metabot
|
||||
show-modular-embed-terms
|
||||
show-sdk-embed-terms
|
||||
show-simple-embed-terms
|
||||
show-static-embed-terms
|
||||
|
||||
@@ -565,8 +565,10 @@ categories:
|
||||
url: "cloud/limitations"
|
||||
- name: "Change region"
|
||||
url: "cloud/change-region"
|
||||
- name: "IP addresses to whitelist"
|
||||
- name: "Metabase IP addresses"
|
||||
url: "cloud/ip-addresses-to-whitelist"
|
||||
- name: "IP allowlist"
|
||||
url: "cloud/ip-allowlist"
|
||||
- name: "Migrate from self-hosted to Cloud"
|
||||
url: "cloud/migrate/guide"
|
||||
- name: "Migrate from Cloud to self-hosted"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Vite 6 Data App
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
allowed_hosts:
|
||||
- https://allowed.data-app.test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync, spawn } = require("child_process");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
@@ -10,8 +10,21 @@ const { waitUntilReady, shell } = require("./cypress-runner-utils");
|
||||
const tempDbPath = path.join(os.tmpdir(), `metabase-test-${process.pid}.db`);
|
||||
|
||||
function getJvmOptsFromDepsEdn(alias = "e2e") {
|
||||
const cmd = `clojure -Sdeps '{:deps {}}' -M -e '(->> (-> "deps.edn" slurp clojure.edn/read-string :aliases :${alias} :jvm-opts) (clojure.string/join " ") println)'`;
|
||||
return execSync(cmd, { encoding: "utf8" }).trim().toString();
|
||||
const depsEdnPath = path.resolve(__dirname, "../../deps.edn");
|
||||
const depsEdn = fs.readFileSync(depsEdnPath, "utf8");
|
||||
const aliasBlockPattern = new RegExp(
|
||||
`:${alias}\\s*\\n\\s*\\{:jvm-opts\\s*\\[([\\s\\S]*?)\\]\\s*\\}`,
|
||||
"m",
|
||||
);
|
||||
const aliasBlock = depsEdn.match(aliasBlockPattern)?.[1];
|
||||
if (!aliasBlock) {
|
||||
throw new Error(`Could not find :jvm-opts for alias :${alias} in deps.edn`);
|
||||
}
|
||||
|
||||
const jvmOpts = [...aliasBlock.matchAll(/"((?:\\.|[^"\\])*)"/g)].map(
|
||||
([, option]) => JSON.parse(`"${option}"`),
|
||||
);
|
||||
return jvmOpts.join(" ");
|
||||
}
|
||||
|
||||
// Ensure that the only two required env vars have values
|
||||
|
||||
@@ -109,7 +109,8 @@ describe("snapshots", () => {
|
||||
updateSetting("enable-public-sharing", true);
|
||||
// interactive is not enabled in the snapshots as it requires a premium feature
|
||||
// updateSetting("enable-embedding-interactive", true);
|
||||
updateSetting("enable-embedding-modular", true).then(() => {
|
||||
updateSetting("enable-embedding-sdk", true);
|
||||
updateSetting("enable-embedding-static", true).then(() => {
|
||||
updateSetting("embedding-secret-key", METABASE_SECRET_KEY);
|
||||
});
|
||||
// dismiss the license token missing banner, not necessary to render it in every test
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
name: Custom Error Component
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
name: Isolation
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
name: Kitchen Sink
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
|
||||
@@ -64,7 +64,10 @@ function Overview() {
|
||||
<h2 style={{ margin: "0 0 8px" }}>All orders</h2>
|
||||
<div style={{ height: 400 }}>
|
||||
{ordersQuery.query ? (
|
||||
<InteractiveQuestion card={{ query: ordersQuery.query }} />
|
||||
<InteractiveQuestion
|
||||
card={{ query: ordersQuery.query }}
|
||||
withDownloads
|
||||
/>
|
||||
) : (
|
||||
<div>…</div>
|
||||
)}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
name: Synced Actions App
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
name: Synced App
|
||||
version: 1
|
||||
path: ./dist/index.js
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
name: Broken Bundle
|
||||
version: 1
|
||||
# Points at a file that isn't in the repo — a valid config, an un-loadable app.
|
||||
path: ./missing.js
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
name: Good App
|
||||
description: A well-formed app that syncs cleanly
|
||||
version: 1
|
||||
path: ./index.js
|
||||
|
||||
@@ -7,6 +7,12 @@ import "@testing-library/cypress/add-commands";
|
||||
import { configure } from "@testing-library/cypress";
|
||||
import "cypress-real-events/support";
|
||||
import addContext from "mochawesome/addContext";
|
||||
|
||||
import {
|
||||
assertNoDataAppScopeDenials,
|
||||
resetDataAppScopeGuard,
|
||||
} from "e2e/support/helpers/e2e-data-app-helpers";
|
||||
|
||||
import "./commands";
|
||||
// Must stay imported after "@cypress/code-coverage/support": its afterEach
|
||||
// zeroes the window coverage counters after collecting each test's fires,
|
||||
@@ -196,3 +202,8 @@ beforeEach(function () {
|
||||
}).as("globalIntercept");
|
||||
}
|
||||
});
|
||||
|
||||
// A data app's scope rejections are recorded by the intercept H.openDataApp installs; see
|
||||
// e2e/support/helpers/e2e-data-app-helpers.ts.
|
||||
beforeEach(resetDataAppScopeGuard);
|
||||
afterEach(assertNoDataAppScopeDenials);
|
||||
|
||||
@@ -16,18 +16,23 @@ import { LOCAL_GIT_PATH } from "./e2e-remote-sync-helpers";
|
||||
export const DATA_APP_NAME = "kitchen-sink";
|
||||
export const DATA_APP_DISPLAY_NAME = "Kitchen Sink";
|
||||
|
||||
export const visitDataAppRoute = (route: string) =>
|
||||
cy.visit(`/apps/${DATA_APP_NAME}/${route}`);
|
||||
export const visitDataAppRoute = (route: string) => {
|
||||
installDataAppScopeGuard();
|
||||
return cy.visit(`/apps/${DATA_APP_NAME}/${route}`);
|
||||
};
|
||||
|
||||
export const fakeDataApp = (overrides: Partial<DataApp> = {}): DataApp => ({
|
||||
id: 1,
|
||||
name: DATA_APP_NAME,
|
||||
display_name: DATA_APP_DISPLAY_NAME,
|
||||
description: null,
|
||||
version: 1,
|
||||
outdated: false,
|
||||
bundle_path: `data_apps/${DATA_APP_NAME}/dist/index.js`,
|
||||
enabled: true,
|
||||
resource_collection_id: null,
|
||||
permission_group_id: null,
|
||||
table_ids: [],
|
||||
allowed_hosts: [],
|
||||
bundle_hash: "e2e-bundle-hash",
|
||||
last_synced_sha: "e2e0000",
|
||||
@@ -101,7 +106,71 @@ export const mockDataApp = <TestEnv = DataAppTestEnv>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The two rejections endpoint scope enforcement can emit. `scope_not_permitted` comes from
|
||||
* `ensure-scopes-checked` — the endpoint declares no `:scope` at all, so a narrowed request
|
||||
* may not reach it. `unsupported_scope` comes from `enforce-scope` — the endpoint is scoped,
|
||||
* but not for the scope the request carries. Either one inside a data app means a route the
|
||||
* SDK really uses was never tagged `data-apps:base`.
|
||||
*/
|
||||
const SCOPE_ERRORS = ["scope_not_permitted", "unsupported_scope"];
|
||||
|
||||
let scopeDenials: string[] = [];
|
||||
let scopeGuardInstalled = false;
|
||||
|
||||
/**
|
||||
* Records every scope rejection the backend returns while a data app is open. Every request a
|
||||
* sandboxed app makes is marked `X-Metabase-Client: data-app` and confined to `data-apps:base`,
|
||||
* so a rejection here is a route missing its `{:scope api-scope/data-app}` tag, not a
|
||||
* permission problem. `assertNoDataAppScopeDenials` (a root `afterEach`) fails the test on any.
|
||||
*/
|
||||
function installDataAppScopeGuard() {
|
||||
if (scopeGuardInstalled) {
|
||||
return;
|
||||
}
|
||||
scopeGuardInstalled = true;
|
||||
|
||||
cy.intercept("/api/**", (req) => {
|
||||
// `after:response` rather than a `req.continue` callback: the latter buffers the body,
|
||||
// which would sit in front of the streamed `/api/dataset` responses.
|
||||
req.on("after:response", (res) => {
|
||||
// `res.body` is whatever the endpoint returned; the scope middleware answers with a
|
||||
// JSON `{error, message}` body.
|
||||
const error = (res.body as { error?: string } | undefined)?.error;
|
||||
if (
|
||||
res.statusCode === 403 &&
|
||||
error !== undefined &&
|
||||
SCOPE_ERRORS.includes(error)
|
||||
) {
|
||||
scopeDenials.push(
|
||||
`${req.method} ${new URL(req.url).pathname} -> ${error}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Root `beforeEach` in `e2e/support/cypress.js`: intercepts reset between tests, so must this record. */
|
||||
export function resetDataAppScopeGuard() {
|
||||
scopeDenials = [];
|
||||
scopeGuardInstalled = false;
|
||||
}
|
||||
|
||||
/** Root `afterEach` in `e2e/support/cypress.js`. Only a test that opened a data app pays for it. */
|
||||
export function assertNoDataAppScopeDenials() {
|
||||
if (!scopeGuardInstalled) {
|
||||
return;
|
||||
}
|
||||
cy.then(() => {
|
||||
expect(
|
||||
scopeDenials,
|
||||
`data-app scope rejections:\n${scopeDenials.join("\n")}`,
|
||||
).to.be.empty;
|
||||
});
|
||||
}
|
||||
|
||||
export function openDataApp(slug: string) {
|
||||
installDataAppScopeGuard();
|
||||
return cy.visit(Urls.dataApp(slug));
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const { WEB_PORT, SMTP_PORT } = WEBMAIL_CONFIG;
|
||||
/**
|
||||
* `bun run test-cypress` starts maildev for you via
|
||||
* e2e/test/scenarios/docker-compose.yml. To start it manually instead:
|
||||
* `docker run -d -p 1180:1080 -p 1125:1025 maildev/maildev:2.2.1`
|
||||
* `docker run -d -p 127.0.0.1:1180:1080 -p 127.0.0.1:1125:1025 maildev/maildev:2.2.1`
|
||||
* or
|
||||
* `npx maildev -s 1125 -w 1180`
|
||||
*/
|
||||
|
||||
@@ -216,7 +216,7 @@ export function prepareSdkIframeEmbedTest({
|
||||
activateToken(withToken);
|
||||
}
|
||||
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-simple", {
|
||||
value: true,
|
||||
});
|
||||
|
||||
@@ -256,7 +256,10 @@ export function prepareGuestEmbedSdkIframeEmbedTest({
|
||||
|
||||
onPrepare?.();
|
||||
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-simple", {
|
||||
value: true,
|
||||
});
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ export const embedModalContent = () =>
|
||||
export const embedModalEnableEmbeddingCard = () =>
|
||||
cy.findByTestId("enable-embedding-card");
|
||||
|
||||
// The card renders a single button, disabled once the terms are accepted, so
|
||||
// an enabled one is the accept button and nothing else.
|
||||
const ACCEPT_TERMS_BUTTON =
|
||||
'[data-testid="enable-embedding-card"] button:not([disabled])';
|
||||
const ACCEPT_TERMS_BUTTON_NAME =
|
||||
/(Agree and (continue|enable)|Enable and continue)/;
|
||||
|
||||
export const embedModalEnableEmbedding = () => {
|
||||
// Wait for the modal before reading the DOM below. That read is a snapshot
|
||||
@@ -17,21 +15,25 @@ export const embedModalEnableEmbedding = () => {
|
||||
embedModalContent().should("exist");
|
||||
|
||||
cy.get("body").then(($body) => {
|
||||
// Nothing left to accept. Either no card mounted — terms were accepted in
|
||||
// the test setup and the section bails early via `showSection` (see
|
||||
// EnableModularEmbeddingSection / EnableGuestEmbedsSection) — or a section
|
||||
// that mounted on stale settings settled on its disabled "Enabled" label.
|
||||
if ($body.find(ACCEPT_TERMS_BUTTON).length === 0) {
|
||||
// No card mounted — terms were accepted in the test setup, the section
|
||||
// bails early via `showSection` (see EnableModularEmbeddingSection /
|
||||
// EnableGuestEmbedsSection) and never renders.
|
||||
if ($body.find('[data-testid="enable-embedding-card"]').length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
cy.get(ACCEPT_TERMS_BUTTON).click();
|
||||
// Match the actionable label, not the disabled "Enabled" one — the latter
|
||||
// also shows transiently on the stale section after an auth-mode switch.
|
||||
cy.findByRole("button", { name: ACCEPT_TERMS_BUTTON_NAME }).click();
|
||||
|
||||
// Once the acceptance registers, the section freezes and relabels its
|
||||
// button to a disabled "Enabled", so the enabled button going away is the
|
||||
// signal. Asserting it here makes a lost click fail on the spot instead of
|
||||
// on a misleading iframe timeout downstream (EMB-2292).
|
||||
cy.get(ACCEPT_TERMS_BUTTON, { timeout: 10_000 }).should("not.exist");
|
||||
// button to a disabled "Enabled", so the actionable label going away is
|
||||
// the signal. Asserting it here makes a lost click fail on the spot
|
||||
// instead of on a misleading iframe timeout downstream (EMB-2292).
|
||||
cy.findByRole("button", {
|
||||
name: ACCEPT_TERMS_BUTTON_NAME,
|
||||
timeout: 10_000,
|
||||
}).should("not.exist");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
Make sure you have the ldap test server running locally:
|
||||
docker run -p 389:389 \
|
||||
docker run -p 127.0.0.1:389:389 \
|
||||
--env LDAP_ADMIN_PASSWORD=adminpass \
|
||||
--env LDAP_USERS=user01@example.org,user02@example.org \
|
||||
--env LDAP_PASSWORDS=123456,123465 \
|
||||
|
||||
@@ -78,7 +78,7 @@ export function signInAsAdminAndEnableEmbeddingSdk() {
|
||||
activateToken("bleeding-edge");
|
||||
enableJwtAuth();
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"enable-embedding-modular": true,
|
||||
"enable-embedding-sdk": true,
|
||||
|
||||
// Needed so CORS headers (e.g. Access-Control-Expose-Headers) are sent in responses,
|
||||
// and we can assert them in tests.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import {
|
||||
type MetabaseCard,
|
||||
StaticQuestion,
|
||||
} from "@metabase/embedding-sdk-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
@@ -11,6 +12,8 @@ import { mountSdkContent } from "e2e/support/helpers/embedding-sdk-component-tes
|
||||
import { signInAsAdminAndEnableEmbeddingSdk } from "e2e/support/helpers/embedding-sdk-testing";
|
||||
import { mockAuthProviderAndJwtSignIn } from "e2e/support/helpers/embedding-sdk-testing/embedding-sdk-helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
|
||||
|
||||
// What a data app actually passes: a `card` object built from a query (e.g. the
|
||||
@@ -72,4 +75,101 @@ describe("scenarios > embedding-sdk > sdk-question card prop", () => {
|
||||
cy.findByTestId("table-root").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("card with a not-yet-resolved query (EMB-2390)", () => {
|
||||
// Data apps mount the question as soon as `useMetabaseQueryObject` returns,
|
||||
// whose `query` is null until it resolves. The SDK must keep showing its
|
||||
// loader for that window and never fall through to the "run your code"
|
||||
// empty state that belongs to a blank question.
|
||||
const EMPTY_STATE_SELECTOR = 'img[alt="Code prompt icon"]';
|
||||
const VISUALIZATION_SELECTOR = '[data-testid="visualization-root"]';
|
||||
const PENDING_QUERY_HOLD_MS = 1500;
|
||||
|
||||
function assertEmptyStateNeverShownWhilePending() {
|
||||
cy.window().then((win) => {
|
||||
return new Cypress.Promise((resolve, reject) => {
|
||||
const startedAt = Date.now();
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
if (win.document.querySelector(EMPTY_STATE_SELECTOR)) {
|
||||
clearInterval(checkInterval);
|
||||
reject(
|
||||
new Error(
|
||||
"the empty results state must not show while `card.query` is null",
|
||||
),
|
||||
);
|
||||
} else if (Date.now() - startedAt >= PENDING_QUERY_HOLD_MS) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function assertEmptyStateNeverShownWhileResolving() {
|
||||
H.assertElementNeverExists({
|
||||
shouldNotExistSelector: EMPTY_STATE_SELECTOR,
|
||||
successSelector: VISUALIZATION_SELECTOR,
|
||||
rejectionMessage:
|
||||
"the empty results state must not flash between the query resolving and the results rendering",
|
||||
pollInterval: 20,
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
function DeferredInteractiveQuestion() {
|
||||
const [query, setQuery] = useState<MetabaseCard["query"]>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InteractiveQuestion card={{ ...card, query }} />
|
||||
<button onClick={() => setQuery(card.query)}>Resolve query</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeferredStaticQuestion() {
|
||||
const [query, setQuery] = useState<MetabaseCard["query"]>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StaticQuestion card={{ ...card, query }} />
|
||||
<button onClick={() => setQuery(card.query)}>Resolve query</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it("InteractiveQuestion should show a loader instead of the empty results state until `card.query` resolves", () => {
|
||||
mountSdkContent(<DeferredInteractiveQuestion />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
assertEmptyStateNeverShownWhilePending();
|
||||
cy.findByTestId("loading-indicator").should("be.visible");
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Resolve query" }).click();
|
||||
assertEmptyStateNeverShownWhileResolving();
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("visualization-root").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("StaticQuestion should show a loader instead of the empty results state until `card.query` resolves", () => {
|
||||
mountSdkContent(<DeferredStaticQuestion />);
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
assertEmptyStateNeverShownWhilePending();
|
||||
cy.findByTestId("loading-indicator").should("be.visible");
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: "Resolve query" }).click();
|
||||
assertEmptyStateNeverShownWhileResolving();
|
||||
|
||||
getSdkRoot().within(() => {
|
||||
cy.findByTestId("visualization-root").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -883,7 +883,7 @@ describe("scenarios > admin > settings > map settings", () => {
|
||||
});
|
||||
|
||||
// Ensure the webhook tester docker container is running
|
||||
// docker run -p 9080:8080/tcp tarampampam/webhook-tester:1.1.0 serve --create-session 00000000-0000-0000-0000-000000000000
|
||||
// docker run -p 127.0.0.1:9080:8080/tcp tarampampam/webhook-tester:1.1.0 serve --create-session 00000000-0000-0000-0000-000000000000
|
||||
describe("notifications", { tags: "@external" }, () => {
|
||||
beforeEach(() => {
|
||||
H.resetWebhookTester();
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("scenarios > admin > settings > SSO > JWT", () => {
|
||||
);
|
||||
cy.button("Set up key").click();
|
||||
H.modal().within(() => {
|
||||
cy.button("Create").click();
|
||||
cy.button("Done").click();
|
||||
});
|
||||
cy.button("Save and enable").click();
|
||||
cy.wait("@updateSettings");
|
||||
@@ -81,14 +81,10 @@ describe("scenarios > admin > settings > SSO > JWT", () => {
|
||||
|
||||
cy.button("Regenerate key").click();
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Delete key and generate a new one?").should("exist");
|
||||
cy.findByText("Set up secret key").should("exist");
|
||||
cy.findByText(
|
||||
"This will cause existing tokens to stop working until the identity provider is updated with a new key.",
|
||||
"This will cause existing tokens to stop working until the identity provider is updated with the new key.",
|
||||
).should("exist");
|
||||
cy.button("Delete key").click();
|
||||
});
|
||||
H.modal().within(() => {
|
||||
cy.findByText("Store your new key").should("exist");
|
||||
cy.button("Done").click();
|
||||
});
|
||||
cy.button("Save changes").click();
|
||||
|
||||
@@ -9,8 +9,10 @@ export const uploadTranslationDictionary = (rows: DictionaryArray) => {
|
||||
cy.intercept("POST", "/api/ee/content-translation/upload-dictionary").as(
|
||||
"uploadDictionary",
|
||||
);
|
||||
cy.intercept("GET", "/api/setting").as("getSettings");
|
||||
cy.signInAsAdmin();
|
||||
cy.visit("/embedding/localization");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.wait("@getSettings");
|
||||
|
||||
cy.findByTestId("content-localization-setting").findByText(
|
||||
/Upload edited translation dictionary/,
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("scenarios > admin > embedding > guest embeds> content translation", ()
|
||||
});
|
||||
|
||||
it("admin settings configuration form is not present", () => {
|
||||
cy.visit("/embedding/localization");
|
||||
cy.visit("/admin/embedding/guest");
|
||||
cy.findByTestId("content-translation-configuration").should("not.exist");
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ describe("scenarios > admin > embedding > guest embeds> content translation", ()
|
||||
describe("The translation download button", () => {
|
||||
it("downloads the stored translations", () => {
|
||||
uploadTranslationDictionaryViaAPI(germanFieldNames);
|
||||
cy.visit("/embedding/localization");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.findByTestId("content-translation-configuration")
|
||||
.button(/Get translation dictionary template/i)
|
||||
.click();
|
||||
@@ -184,7 +184,7 @@ describe("scenarios > admin > embedding > guest embeds> content translation", ()
|
||||
});
|
||||
|
||||
it("rejects, in the frontend, a CSV upload that is too big", () => {
|
||||
cy.visit("/embedding/localization");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.get("#content-translation-dictionary-upload-input").selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from(
|
||||
@@ -210,7 +210,7 @@ describe("scenarios > admin > embedding > guest embeds> content translation", ()
|
||||
});
|
||||
|
||||
it("rejects invalid CSV", () => {
|
||||
cy.visit("/embedding/localization");
|
||||
cy.visit("/admin/embedding");
|
||||
const validCSV = getCSVWithHeaderRow(germanFieldNames);
|
||||
const invalidCSV = validCSV + '\nde,Price,"Preis"X';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const { H } = cy;
|
||||
|
||||
const PATH = "/admin/metabot/mcp/authorizations";
|
||||
const REDIRECT_URI = "https://example.com/callback";
|
||||
const SCOPE = "agent:content:read";
|
||||
|
||||
/**
|
||||
* End-to-end coverage for the OAuth Authorizations admin page.
|
||||
@@ -110,8 +111,8 @@ function denyClient(client: RegisteredClient) {
|
||||
|
||||
/**
|
||||
* Drive the consent flow to a decision for a registered client, recording an `approved` or `denied`
|
||||
* event stamped with the signed-in user. `scope` is omitted (it's optional, and the DCR client's
|
||||
* agent scopes don't include `profile`); the client must be confidential so the public-client PKCE
|
||||
* event stamped with the signed-in user. `/oauth/authorize` requires a scope, and the DCR client's
|
||||
* default scopes include `SCOPE`; the client must be confidential so the public-client PKCE
|
||||
* requirement doesn't apply. Mirrors the real browser flow: GET the consent page, lift the CSRF
|
||||
* token + params signature from its hidden fields, then POST the decision (Cypress carries the
|
||||
* CSRF cookie). Both approve and deny redirect (302).
|
||||
@@ -121,7 +122,7 @@ function decideClient(client: RegisteredClient, approved: boolean) {
|
||||
const authorizeUrl =
|
||||
`/oauth/authorize?client_id=${clientId}` +
|
||||
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
||||
"&response_type=code&state=test-state";
|
||||
`&response_type=code&scope=${encodeURIComponent(SCOPE)}&state=test-state`;
|
||||
|
||||
return cy.request("GET", authorizeUrl).then(({ body }) => {
|
||||
cy.request({
|
||||
@@ -136,6 +137,7 @@ function decideClient(client: RegisteredClient, approved: boolean) {
|
||||
client_id: clientId,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
scope: SCOPE,
|
||||
state: "test-state",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -866,7 +866,7 @@ describe("Remote Sync", () => {
|
||||
H.copySyncedCollectionFixture();
|
||||
H.commitToRepo();
|
||||
// Enable static embedding instance-wide so the Embed option is offered.
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.configureGitAndPullChanges("read-only");
|
||||
|
||||
cy.visit("/");
|
||||
|
||||
@@ -1337,7 +1337,7 @@ describe("scenarios > collection items listing", () => {
|
||||
|
||||
cy.findByRole("button", { name: "Filter" }).click();
|
||||
H.popover().within(() => {
|
||||
cy.findAllByRole("checkbox").should("have.length", 7);
|
||||
cy.findAllByRole("checkbox").should("have.length", 8);
|
||||
cy.findByLabelText("Collection").should("not.be.checked");
|
||||
cy.findByLabelText("Dashboard").should("not.be.checked");
|
||||
cy.findByLabelText("Question").should("not.be.checked");
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
DATA_APP_DISPLAY_NAME as APP_DISPLAY_NAME,
|
||||
DATA_APP_NAME as APP_NAME,
|
||||
} from "e2e/support/helpers";
|
||||
import type { DataApp } from "metabase-types/api";
|
||||
|
||||
import { DATA_APP_TEST_ENV as TEST_ENV } from "./helpers";
|
||||
|
||||
@@ -96,6 +97,69 @@ describe("scenarios > data apps > admin management", () => {
|
||||
.findByText(/AI-generated React apps/)
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
describe("outdated apps", () => {
|
||||
// Only a bump of the supported version makes a real app outdated, so the flag
|
||||
// the API computes is patched onto a real app's responses instead.
|
||||
const OUTDATED_APP = "orders-app";
|
||||
|
||||
function markAppOutdated() {
|
||||
cy.request<DataApp>("POST", `/api/apps/${OUTDATED_APP}/draft`)
|
||||
.its("body.display_name")
|
||||
.as("outdatedAppName");
|
||||
|
||||
cy.intercept("GET", "/api/apps", (req) => {
|
||||
req.continue((res) => {
|
||||
res.body = res.body.map((app: DataApp) =>
|
||||
app.name === OUTDATED_APP
|
||||
? { ...app, version: 1, outdated: true }
|
||||
: app,
|
||||
);
|
||||
});
|
||||
});
|
||||
cy.intercept(
|
||||
{ method: "GET", pathname: `/api/apps/${OUTDATED_APP}` },
|
||||
(req) => {
|
||||
req.continue((res) => {
|
||||
res.body = { ...res.body, version: 1, outdated: true };
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
it("badges an outdated app, refuses to open it, and still lets an admin manage its users", () => {
|
||||
markAppOutdated();
|
||||
|
||||
cy.visit("/admin/settings/apps");
|
||||
|
||||
cy.get<string>("@outdatedAppName").then((displayName) => {
|
||||
cy.findByTestId(`data-app-list-item-${OUTDATED_APP}`)
|
||||
.scrollIntoView()
|
||||
.within(() => {
|
||||
cy.findByText("Outdated").should("be.visible");
|
||||
cy.findByText(displayName).should("be.visible");
|
||||
cy.findByRole("link", { name: displayName }).should("not.exist");
|
||||
cy.findByRole("button", {
|
||||
name: `Actions for ${displayName}`,
|
||||
}).click();
|
||||
});
|
||||
});
|
||||
|
||||
H.popover().findByText("Manage user access").click();
|
||||
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/settings/apps/${OUTDATED_APP}/users`,
|
||||
);
|
||||
H.main()
|
||||
.findByRole("heading", { name: "Manage access to this app" })
|
||||
.should("be.visible");
|
||||
|
||||
H.openDataApp(OUTDATED_APP);
|
||||
H.main().findByText("This data app is outdated").should("be.visible");
|
||||
cy.get("iframe").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(v65): data apps launch in v65 — replace the "no token" suite below with
|
||||
|
||||
110
e2e/test/scenarios/data-apps/backend_scope.cy.spec.ts
Normal file
110
e2e/test/scenarios/data-apps/backend_scope.cy.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
DATA_APP_DISPLAY_NAME as APP_DISPLAY_NAME,
|
||||
DATA_APP_NAME as APP_NAME,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
import { DATA_APP_TEST_ENV as TEST_ENV } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const TIMEOUT = 30000;
|
||||
|
||||
describe("scenarios > data apps > backend scope", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("bleeding-edge");
|
||||
});
|
||||
|
||||
/**
|
||||
* The unit and middleware tests can only assert the endpoints we already thought to list.
|
||||
* This one works the other way round: drive the SDK surface a data app actually exposes and
|
||||
* let the backend tell us what it refuses. The scope guard `H.openDataApp` installs records
|
||||
* every rejection and the root `afterEach` fails the test on any, so this spec only has to
|
||||
* exercise the surface.
|
||||
*/
|
||||
it("serves the whole InteractiveQuestion surface without refusing a request for scope", () => {
|
||||
H.mockDataApp(APP_NAME, {
|
||||
displayName: APP_DISPLAY_NAME,
|
||||
testEnv: TEST_ENV,
|
||||
});
|
||||
H.openDataApp(APP_NAME);
|
||||
|
||||
H.dataAppIframe(APP_DISPLAY_NAME).within(() => {
|
||||
cy.findByTestId("data-app-content", { timeout: TIMEOUT }).should("exist");
|
||||
|
||||
// The toolbar only mounts once the first query has come back, so this doubles as the
|
||||
// wait for `/api/dataset`.
|
||||
cy.findByTestId("interactive-question-result-toolbar", {
|
||||
timeout: TIMEOUT,
|
||||
}).should("exist");
|
||||
|
||||
cy.log("Chart type picker — loads visualization metadata");
|
||||
cy.findByTestId("chart-type-selector-button").click();
|
||||
cy.findByText("More charts").should("be.visible");
|
||||
// Toggle the trigger to close: key events would land on the parent document, not the
|
||||
// iframe, and Mantine treats a stray Escape as a modal dismiss.
|
||||
cy.findByTestId("chart-type-selector-button").click();
|
||||
|
||||
cy.log("Visualization settings");
|
||||
cy.findByTestId("viz-settings-button").click();
|
||||
cy.findByTestId("viz-settings-button").click();
|
||||
|
||||
cy.log("Filter picker — loads table metadata and field values");
|
||||
cy.findByTestId("filter-dropdown-button").click();
|
||||
// This `within` is the whole iframe, so a column name also matches the table
|
||||
// rendered behind the picker. Pick from the popover the button just opened.
|
||||
cy.get('[data-element-id="mantine-popover"]')
|
||||
.should("have.length.above", 0)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.findByText("Total").click();
|
||||
});
|
||||
cy.findByText("Add filter").click();
|
||||
|
||||
cy.log("Summarize picker — loads the aggregation/column pickers");
|
||||
// The button is labelled by how many summaries the question has, and with
|
||||
// none it opens the aggregation picker directly — the badge list, with its
|
||||
// "Add another summary", is what a question that already aggregates opens.
|
||||
cy.findByText("Summarize").click();
|
||||
cy.findByTestId("aggregation-picker").should("be.visible");
|
||||
cy.findByText("Summarize").click();
|
||||
|
||||
cy.log("Group-by picker");
|
||||
cy.findByText("Group").click();
|
||||
cy.findByText("Group").click();
|
||||
|
||||
cy.log(
|
||||
"Download menu — loads the export formats and downloads preference",
|
||||
);
|
||||
cy.findByTestId("question-download-widget-button").click();
|
||||
cy.findByTestId("question-download-widget-button").click();
|
||||
|
||||
cy.log("Notebook editor — loads databases, schemas, tables and fields");
|
||||
cy.findByTestId("notebook-button").click();
|
||||
cy.findByText("Back to visualization", { timeout: TIMEOUT }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("Add a notebook filter step");
|
||||
cy.findByTestId("action-buttons").findByText("Filter").click();
|
||||
cy.get('[data-element-id="mantine-popover"]')
|
||||
.should("have.length.above", 0)
|
||||
.last()
|
||||
.within(() => {
|
||||
// A sparse float has no cached field values, so the picker defaults to
|
||||
// Between and offers Min/Max. A low-cardinality column like Quantity
|
||||
// defaults to `=` and offers a value list instead.
|
||||
cy.findByText("Discount").click();
|
||||
});
|
||||
cy.findByPlaceholderText("Min").type("2");
|
||||
cy.findByText("Add filter").click();
|
||||
|
||||
cy.log("Re-run the edited query");
|
||||
cy.findByText("Visualize").click();
|
||||
cy.findByText("Back to visualization", { timeout: TIMEOUT }).should(
|
||||
"not.exist",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ describe("scenarios > data apps > sandbox isolation", () => {
|
||||
H.activateToken("bleeding-edge");
|
||||
});
|
||||
|
||||
const setUp = () => {
|
||||
const setup = () => {
|
||||
const instanceUrl = Cypress.config("baseUrl") ?? "";
|
||||
const testEnv: IsolationTestEnv = { instanceUrl };
|
||||
|
||||
@@ -63,177 +63,177 @@ describe("scenarios > data apps > sandbox isolation", () => {
|
||||
};
|
||||
|
||||
it("keeps a document.createElement about:blank iframe within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-create-element");
|
||||
});
|
||||
|
||||
it("gates a host-React about:blank iframe", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbeExpectingGuard("isolation-react-about-blank");
|
||||
});
|
||||
|
||||
it("gates an iframe pointing at Metabase itself", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbeExpectingGuard("isolation-react-src");
|
||||
});
|
||||
|
||||
it("gates a srcdoc iframe", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbeExpectingGuard("isolation-react-srcdoc");
|
||||
});
|
||||
|
||||
it("keeps a window.open realm within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-window-open");
|
||||
});
|
||||
|
||||
it("gates the Worker constructor", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-worker");
|
||||
});
|
||||
|
||||
it("gates the SharedWorker constructor", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-shared-worker");
|
||||
});
|
||||
|
||||
it("gates the service worker registration API", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-service-worker");
|
||||
});
|
||||
|
||||
it("gates dynamic import", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-dynamic-import");
|
||||
});
|
||||
|
||||
it("keeps a dangerouslySetInnerHTML iframe within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-inner-html");
|
||||
});
|
||||
|
||||
it("keeps a Function-constructor fetch gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-function-constructor");
|
||||
});
|
||||
|
||||
it("keeps a DOMParser iframe within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-dom-parser");
|
||||
});
|
||||
|
||||
it("keeps a createHTMLDocument iframe adopted into the realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-adopt-html-doc-iframe");
|
||||
});
|
||||
|
||||
it("keeps a createHTMLDocument iframe imported into the realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-import-html-doc-iframe");
|
||||
});
|
||||
|
||||
it("keeps a createDocument iframe adopted into the realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-xml-doc-iframe");
|
||||
});
|
||||
|
||||
it("keeps a template owner-document iframe adopted into the realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-template-doc-iframe");
|
||||
});
|
||||
|
||||
it("keeps a raw API out of the SDK endowments", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-endowment-api");
|
||||
});
|
||||
|
||||
it("keeps an Error.prepareStackTrace realm reference gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-stack-trace-realm");
|
||||
});
|
||||
|
||||
it("keeps window.parent within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-window-parent");
|
||||
});
|
||||
|
||||
it("keeps window.top within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-window-top");
|
||||
});
|
||||
|
||||
it("keeps window.frameElement's owner realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-frame-element");
|
||||
});
|
||||
|
||||
it("keeps window.parent.parent within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-chain");
|
||||
});
|
||||
|
||||
it("gates document.cookie on the parent realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-cookie");
|
||||
});
|
||||
|
||||
it("gates localStorage on the parent realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-local-storage");
|
||||
});
|
||||
|
||||
it("gates sessionStorage on the parent realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-session-storage");
|
||||
});
|
||||
|
||||
it("gates indexedDB on the parent realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-indexeddb");
|
||||
});
|
||||
|
||||
it("gates caches on the parent realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-parent-caches");
|
||||
});
|
||||
|
||||
it("keeps indexed window.frames access gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-window-frames");
|
||||
});
|
||||
|
||||
it("keeps window.opener's realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-window-opener");
|
||||
});
|
||||
|
||||
it("gates FontFace.load", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-font-face");
|
||||
});
|
||||
|
||||
it("gates cookieStore", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-cookie-store");
|
||||
});
|
||||
|
||||
it("gates performance resource timing", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-perf-resource-timing");
|
||||
});
|
||||
|
||||
it("keeps a Range.createContextualFragment iframe within the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-range-fragment-iframe");
|
||||
});
|
||||
|
||||
it("keeps a custom element's upgrade callback in the gated realm", () => {
|
||||
setUp();
|
||||
setup();
|
||||
runProbe("isolation-custom-element");
|
||||
});
|
||||
|
||||
it("keeps a host iframe's realm gated", () => {
|
||||
setUp();
|
||||
setup();
|
||||
|
||||
// A srcless (about:blank) iframe is same-origin, so its `contentWindow` is a
|
||||
// live realm with an un-gated `fetch` — the same capability html2canvas's
|
||||
@@ -289,4 +289,37 @@ describe("scenarios > data apps > sandbox isolation", () => {
|
||||
|
||||
runProbe("isolation-allowed-host-redirect");
|
||||
});
|
||||
|
||||
// The 403s the marker produces are backend behaviour, covered by
|
||||
// `data_app_scope_test.clj`. What only e2e can prove is the premise those 403s rest
|
||||
// on: that the real transport stamps `X-Metabase-Client: data-app` on the requests the
|
||||
// SDK makes from inside the sandbox. The header cannot be spoofed to gain access —
|
||||
// host-realm code the membraned guest can't reach sets it, and it only ever narrows —
|
||||
// but if it ever stopped being sent, the confinement would silently stop applying.
|
||||
it("marks the requests the SDK makes from inside the sandbox as data-app", () => {
|
||||
const markedPaths = new Set<string>();
|
||||
|
||||
cy.intercept("/api/**", (req) => {
|
||||
if (req.headers["x-metabase-client"] === "data-app") {
|
||||
markedPaths.add(new URL(req.url).pathname);
|
||||
}
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
// Wait for the guest bundle to have rendered — the SDK's bootstrap requests are
|
||||
// still in flight while it loads, so asserting earlier races them.
|
||||
H.dataAppIframe(APP_DISPLAY_NAME).within(() => {
|
||||
cy.findByTestId("isolation-result", { timeout: 30000 }).should("exist");
|
||||
});
|
||||
|
||||
// `/api/user/current` is the whole marked surface this fixture produces — it renders
|
||||
// isolation probes, not questions, and the SDK's bootstrap takes site settings from
|
||||
// the auth prefetch rather than refetching `/api/session/properties`.
|
||||
cy.then(() => {
|
||||
expect([...markedPaths], "requests marked as data-app").to.include(
|
||||
"/api/user/current",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,6 +192,36 @@ describe("scenarios > data apps > viewing & routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("outdated apps", () => {
|
||||
it("tells a non-admin why an outdated app won't open", () => {
|
||||
H.mockDataApp(APP_NAME, { displayName: APP_DISPLAY_NAME });
|
||||
|
||||
// The metadata endpoint refuses an outdated app to non-admins with this
|
||||
// exact shape (see `check-not-outdated` in data_apps/api.clj).
|
||||
cy.intercept(
|
||||
{ method: "GET", pathname: `/api/apps/${APP_NAME}` },
|
||||
{
|
||||
statusCode: 409,
|
||||
body: {
|
||||
"error-code": "data-app-outdated",
|
||||
message: "This app was built for version 1 of data apps.",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
cy.signInAsNormalUser();
|
||||
H.openDataApp(APP_NAME);
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("This data app is outdated").should("be.visible");
|
||||
cy.findByText("This app was built for version 1 of data apps.").should(
|
||||
"be.visible",
|
||||
);
|
||||
});
|
||||
cy.get("iframe").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("host error / not-ready screens", () => {
|
||||
it("shows a themed host error screen when the bundle throws while rendering", () => {
|
||||
H.mockDataApp(APP_NAME, {
|
||||
|
||||
@@ -6,6 +6,9 @@ const DB_NAME = "Writable Postgres12";
|
||||
const SOURCE_TABLE = "Animals";
|
||||
const TARGET_TABLE = "transform_table";
|
||||
const TARGET_SCHEMA = "Schema A";
|
||||
const TRANSFORM_DETAIL_TIMEOUT = 10_000;
|
||||
// Python runs asynchronously move data through S3 and an external runner before importing the result.
|
||||
const PYTHON_TRANSFORM_RUN_TIMEOUT = 20_000;
|
||||
|
||||
describe("scenarios > admin > transforms incremental", () => {
|
||||
beforeEach(() => {
|
||||
@@ -194,12 +197,19 @@ def transform(animals):
|
||||
cy.button("Save").click();
|
||||
cy.wait("@createTransform").then(({ response }) => {
|
||||
const transformId = response?.body?.id;
|
||||
if (transformId != null) {
|
||||
cy.wrap(transformId).as("transformId");
|
||||
}
|
||||
expect(response?.statusCode).to.equal(200);
|
||||
expect(transformId).to.be.a("number");
|
||||
});
|
||||
});
|
||||
|
||||
cy.location("pathname", { timeout: TRANSFORM_DETAIL_TIMEOUT }).should(
|
||||
"match",
|
||||
/^\/data-studio\/transforms\/\d+$/,
|
||||
);
|
||||
cy.findByTestId("transforms-header", {
|
||||
timeout: TRANSFORM_DETAIL_TIMEOUT,
|
||||
}).should("be.visible");
|
||||
|
||||
cy.log("run the transform and make sure its table can be queried");
|
||||
H.DataStudio.Transforms.runTab().click();
|
||||
runTransformAndWaitForSuccess();
|
||||
@@ -394,9 +404,11 @@ function visitTransformListPage() {
|
||||
return cy.visit("/data-studio/transforms");
|
||||
}
|
||||
|
||||
function runTransformAndWaitForSuccess() {
|
||||
function runTransformAndWaitForSuccess(
|
||||
options: { timeout?: number } = { timeout: PYTHON_TRANSFORM_RUN_TIMEOUT },
|
||||
) {
|
||||
getRunButton().click();
|
||||
getRunButton().should("have.text", "Ran successfully");
|
||||
getRunButton(options).should("have.text", "Ran successfully");
|
||||
}
|
||||
|
||||
function getRunButton(options: { timeout?: number } = {}) {
|
||||
|
||||
@@ -9,32 +9,32 @@ services:
|
||||
- TZ=US/Pacific
|
||||
image: metabase/qa-databases:postgres-sample-12
|
||||
ports:
|
||||
- 5404:5432
|
||||
- 127.0.0.1:5404:5432
|
||||
|
||||
mongo-sample:
|
||||
image: metabase/qa-databases:mongo-sample-5
|
||||
ports:
|
||||
- 27004:27017
|
||||
- 127.0.0.1:27004:27017
|
||||
|
||||
mysql-sample:
|
||||
environment:
|
||||
- TZ=US/Pacific
|
||||
image: metabase/qa-databases:mysql-sample-8
|
||||
ports:
|
||||
- 3304:3306
|
||||
- 127.0.0.1:3304:3306
|
||||
|
||||
webhook-tester:
|
||||
image: tarampampam/webhook-tester:1.1.0
|
||||
ports:
|
||||
- 9080:8080
|
||||
- 127.0.0.1:9080:8080
|
||||
command: serve --create-session 00000000-0000-0000-0000-000000000000
|
||||
|
||||
maildev:
|
||||
image: maildev/maildev:2.2.1
|
||||
## Host ports deliberately differ from the 1080/1025 used for development
|
||||
ports:
|
||||
- "1180:1080"
|
||||
- "1125:1025"
|
||||
- "127.0.0.1:1180:1080"
|
||||
- "127.0.0.1:1125:1025"
|
||||
|
||||
maildev-ssl:
|
||||
## NOTE: Using this requires a root CA certificate to be added to the Java keystore.
|
||||
@@ -43,8 +43,8 @@ services:
|
||||
## breaks the SSL/SMTP setup and fails the custom SMTP e2e tests.
|
||||
image: maildev/maildev:2.2.1
|
||||
ports:
|
||||
- "1081:1081"
|
||||
- "465:465"
|
||||
- "127.0.0.1:1081:1081"
|
||||
- "127.0.0.1:465:465"
|
||||
environment:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
MAILDEV_WEB_PORT: 1081
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
saml-idp:
|
||||
image: boxyhq/mock-saml:1.4.2
|
||||
ports:
|
||||
- "8090:4000"
|
||||
- "127.0.0.1:8090:4000"
|
||||
environment:
|
||||
APP_URL: http://localhost:8090
|
||||
ENTITY_ID: https://saml.example.com/entityid
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
ldap:
|
||||
image: bitnamilegacy/openldap:2.6.4
|
||||
ports:
|
||||
- "389:389"
|
||||
- "127.0.0.1:389:389"
|
||||
environment:
|
||||
LDAP_ADMIN_PASSWORD: adminpass
|
||||
LDAP_USERS: user01@example.org,user02@example.org
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > embedding > admin route redirects (EMB-1526)", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
// admin/routes.unit.spec.tsx asserts the whole retired-path -> hub table off
|
||||
// the route tree. This covers the part reading the tree cannot: that a
|
||||
// redirect actually fires in a browser, and that it carries its params over.
|
||||
it("redirects retired admin embedding URLs to the hub", () => {
|
||||
cy.visit("/admin/embedding");
|
||||
cy.location("pathname").should("eq", "/embedding/security");
|
||||
|
||||
cy.visit("/admin/embedding/themes/some-theme-id");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
"/embedding/appearance/theme/some-theme-id",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe(
|
||||
"scenarios > embedding > admin settings > oss",
|
||||
{ tags: "@OSS" },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.updateSetting("show-sdk-embed-terms", false);
|
||||
});
|
||||
|
||||
it("shows all embedding types without the setup guide", () => {
|
||||
cy.log("Navigate to Embedding admin section");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.log("Check that we're on the embedding settings page");
|
||||
cy.url().should("include", "/admin/embedding");
|
||||
cy.get("main").findByText("Embedding settings").should("be.visible");
|
||||
|
||||
cy.log("Verify sidebar does not contain setup guide");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Setup guide/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify sidebar does not contain guest embeds link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Guest embeds/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify sidebar contains security settings link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Security/ })
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should show embedding upsell on oss", () => {
|
||||
cy.visit("/admin/embedding/interactive");
|
||||
|
||||
cy.findByTestId("admin-layout-content").within(() => {
|
||||
cy.findByRole("heading", { name: "Embedding settings" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("upsell gem icon should be visible");
|
||||
cy.icon("gem").should("be.visible");
|
||||
|
||||
cy.findByRole("link", { name: "Upgrade" })
|
||||
.should("have.attr", "href")
|
||||
.and(
|
||||
"eq",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embedding-page&source_plan=oss&utm_users=10&utm_campaign=embedding-methods",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show CORS setting on security page", () => {
|
||||
cy.visit("/admin/embedding/security");
|
||||
|
||||
cy.findByTestId("admin-layout-content").within(() => {
|
||||
cy.findByText("Cross-Origin Resource Sharing (CORS)").should("exist");
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > embedding > admin settings > starter", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.activateToken("starter");
|
||||
|
||||
H.updateSetting("show-sdk-embed-terms", false);
|
||||
});
|
||||
|
||||
it("shows all embedding types without the setup guide", () => {
|
||||
cy.log("Navigate to Embedding admin section");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.log("Check that we're on the embedding settings page");
|
||||
cy.url().should("include", "/admin/embedding");
|
||||
cy.get("main").findByText("Embedding settings").should("be.visible");
|
||||
|
||||
cy.log("Verify sidebar does not contain setup guide");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Setup guide/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify sidebar does not contain guest embeds link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Guest embeds/ })
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("Verify sidebar contains security settings link");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Security/ })
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should show embedding upsell on oss", () => {
|
||||
cy.visit("/admin/embedding/interactive");
|
||||
|
||||
cy.findByTestId("admin-layout-content").within(() => {
|
||||
cy.findByRole("heading", { name: "Embedding settings" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("upsell gem icon should be visible");
|
||||
cy.icon("gem").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show CORS setting on security page", () => {
|
||||
cy.visit("/admin/embedding/security");
|
||||
|
||||
cy.findByTestId("admin-layout-content").within(() => {
|
||||
cy.findByText("Cross-Origin Resource Sharing (CORS)").should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,773 +0,0 @@
|
||||
import { SAMPLE_DB_TABLES } from "e2e/support/cypress_data";
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
import { createThemeViaApi } from "./embedding-theme-editor/helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const { STATIC_ORDERS_ID } = SAMPLE_DB_TABLES;
|
||||
|
||||
/**
|
||||
* The embedding hub at `/embedding` -- the tabbed section, not the older
|
||||
* onboarding checklist that `onboarding/embedding-homepage.cy.spec.ts` covers.
|
||||
* One describe per tab; each tab adds its own as it lands.
|
||||
*
|
||||
* Deliberately thin: card copy, the upsell banner and the AI card's done states
|
||||
* are unit-tested next to their components. What is here is what needs a real
|
||||
* browser or a real backend -- routing, the two step orderings, and completion
|
||||
* coming back from the checklist endpoint.
|
||||
*
|
||||
* Get started's blocks are both `@EE`, because the steps themselves are: tenants,
|
||||
* SSO and data segregation only exist on an enterprise build. The unlicensed case
|
||||
* is that same build with no token activated, which is what the locked states are
|
||||
* about.
|
||||
*/
|
||||
|
||||
const CARD = "embedding-hub-checklist-card";
|
||||
|
||||
const PRO_STEP_ORDER = [
|
||||
"Connect a database",
|
||||
"Create a dashboard",
|
||||
"Get embed snippet",
|
||||
"Configure data permissions and tenants",
|
||||
"Set up SSO",
|
||||
"Embed in production with SSO",
|
||||
"Create a custom theme",
|
||||
"Configure AI",
|
||||
];
|
||||
|
||||
// Without modular embedding every Fine-tune step is locked, so AI -- the one
|
||||
// advanced step still reachable -- is promoted into the first section.
|
||||
const UNLICENSED_STEP_ORDER = [
|
||||
"Connect a database",
|
||||
"Create a dashboard",
|
||||
"Get embed snippet",
|
||||
"Configure AI",
|
||||
"Configure data permissions and tenants",
|
||||
"Set up SSO",
|
||||
"Embed in production with SSO",
|
||||
"Create a custom theme",
|
||||
];
|
||||
|
||||
// Every plan sees every tab; the gem marks the ones its token can't unlock.
|
||||
// OSS and Starter lack all four gated features, so they expect the same.
|
||||
const TAB_GEMS_WITHOUT_PAID_FEATURES = [
|
||||
{ label: "Get started", hasGem: false },
|
||||
{ label: "Security", hasGem: false },
|
||||
{ label: "Authentication", hasGem: true },
|
||||
{ label: "Permissions", hasGem: false },
|
||||
{ label: "Tenancy", hasGem: true },
|
||||
{ label: "Appearance", hasGem: true },
|
||||
{ label: "Localization", hasGem: true },
|
||||
];
|
||||
|
||||
function assertStepOrder(titles: string[]) {
|
||||
cy.findAllByTestId(CARD)
|
||||
.should("have.length", titles.length)
|
||||
.each(($card, index) => {
|
||||
cy.wrap($card).should("contain.text", titles[index]);
|
||||
});
|
||||
}
|
||||
|
||||
function assertHubTabs(tabs: { label: string; hasGem: boolean }[]) {
|
||||
cy.findByRole("navigation", { name: "Embedding hub" }).within(() => {
|
||||
// Every tab is a link and nothing else in the nav is, so the count makes
|
||||
// the list exhaustive -- a tab the hub should not have fails here.
|
||||
cy.findAllByRole("link").should("have.length", tabs.length);
|
||||
|
||||
for (const { label, hasGem } of tabs) {
|
||||
cy.findByRole("link", { name: label })
|
||||
.findByTestId("upsell-gem")
|
||||
.should(hasGem ? "exist" : "not.exist");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe("scenarios > embedding > embedding hub > get started", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("redirects from the hub root and lists the licensed step order", () => {
|
||||
cy.visit("/embedding");
|
||||
|
||||
cy.log("the hub root is the Get started tab");
|
||||
cy.location("pathname").should("eq", "/embedding/get-started");
|
||||
|
||||
cy.findByRole("link", { name: "Get started" }).should(
|
||||
"have.attr",
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByRole("heading", {
|
||||
name: "Get started with Metabase Embedding",
|
||||
}).should("be.visible");
|
||||
|
||||
assertStepOrder(PRO_STEP_ORDER);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to Get started from the permissions wizard", () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("link", { name: "Configure data permissions and tenants" })
|
||||
.click();
|
||||
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
"/embedding/get-started/permissions",
|
||||
);
|
||||
|
||||
cy.log(
|
||||
"the wizard's back link is relative, so it lands on the host that mounted it rather than admin",
|
||||
);
|
||||
cy.findByRole("link", { name: /Back to the setup guide/ }).click();
|
||||
|
||||
cy.location("pathname").should("eq", "/embedding/get-started");
|
||||
});
|
||||
|
||||
it("finishes the permissions wizard and lands back on Get started", () => {
|
||||
cy.log("seed every step the wizard checks, so Summary is the one left");
|
||||
H.updateSetting("use-tenants", true);
|
||||
cy.request("POST", "/api/collection", {
|
||||
name: "Shared collection",
|
||||
namespace: "shared-tenant-collection",
|
||||
}).then(({ body: sharedCollection }) => {
|
||||
H.createDashboard({
|
||||
name: "Tenant dashboard",
|
||||
collection_id: sharedCollection.id,
|
||||
});
|
||||
});
|
||||
|
||||
cy.request("POST", "/api/ee/tenant", {
|
||||
name: "Test Tenant",
|
||||
slug: "test-tenant",
|
||||
});
|
||||
|
||||
cy.request("POST", "/api/permissions/group", { name: "Test Group" }).then(
|
||||
({ body: group }) => {
|
||||
cy.sandboxTable({ table_id: STATIC_ORDERS_ID, group_id: group.id });
|
||||
},
|
||||
);
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
|
||||
cy.log("the Summary step closes the wizard out");
|
||||
cy.findByRole("button", { name: "Done" }).click();
|
||||
|
||||
cy.location("pathname").should("eq", "/embedding/get-started");
|
||||
cy.findByTestId("embedding-hub-main").should("be.visible");
|
||||
});
|
||||
|
||||
it("finishes the SSO wizard and lands back on Get started", () => {
|
||||
cy.log("JWT configured is what unlocks the two steps after it");
|
||||
enableJwtAuth();
|
||||
|
||||
cy.visit("/embedding/get-started/sso");
|
||||
|
||||
cy.findByRole("button", { name: "Next" }).click();
|
||||
|
||||
cy.log("confirming the manual login test is what closes the wizard out");
|
||||
// A link, not a button: it is a `Button component={Link}`, so the step's
|
||||
// own navigation is what returns to the hub.
|
||||
cy.findByRole("link", { name: "Log in works, I'm done" }).click();
|
||||
|
||||
cy.location("pathname").should("eq", "/embedding/get-started");
|
||||
cy.findByTestId("embedding-hub-main").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlicensed", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("promotes AI into the first section and locks the Fine-tune steps", () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
assertStepOrder(UNLICENSED_STEP_ORDER);
|
||||
|
||||
cy.log(
|
||||
"the upsell banner is what stands in for the Fine-tune subtitle",
|
||||
);
|
||||
cy.findByText(
|
||||
"Upgrade to Metabase Pro to configure advanced options.",
|
||||
).should("be.visible");
|
||||
|
||||
cy.log("a feature-locked step is inert, with no link to follow");
|
||||
cy.findByRole("link", { name: "Set up SSO" }).should("not.exist");
|
||||
cy.findByText("Set up SSO")
|
||||
.closest(`[data-testid=${CARD}]`)
|
||||
.should("have.attr", "aria-disabled", "true");
|
||||
|
||||
cy.log("AI is never locked -- the admin AI page needs no token");
|
||||
cy.findByRole("button", { name: "Configure AI" }).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("marks the embed step complete once a guest embed is published", () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByText("Get embed snippet")
|
||||
.closest(`[data-testid=${CARD}]`)
|
||||
.findByLabelText("Step 3 complete")
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
cy.log("publish a dashboard as a guest embed");
|
||||
H.createDashboard({ name: "Published dashboard" }).then(
|
||||
({ body: dashboard }) => {
|
||||
cy.request("PUT", `/api/dashboard/${dashboard.id}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cy.log(
|
||||
"the checklist is served without a licence, so completion is real rather than all-false",
|
||||
);
|
||||
cy.visit("/embedding/get-started");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByText("Get embed snippet")
|
||||
.closest(`[data-testid=${CARD}]`)
|
||||
.findByLabelText("Step 3 complete")
|
||||
.should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > embedding > embedding hub > security", () => {
|
||||
describe("pro", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
// The snapshot turns embedding on, but this test is about turning it on
|
||||
// from the hub, so start from off the way a fresh instance would be.
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
});
|
||||
|
||||
it("turns embedding on with one switch", () => {
|
||||
cy.intercept("GET", "/api/setting").as("getSettings");
|
||||
cy.intercept("GET", "/api/session/properties").as("getSessionProperties");
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
cy.log("The first enable goes through the terms modal");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Modular embedding and SDK for React")
|
||||
.should("be.visible");
|
||||
|
||||
// The switch renders before either request lands, reading undefined as
|
||||
// off and as terms-already-seen, so a click before then writes the
|
||||
// settings directly instead of opening the modal.
|
||||
cy.wait(["@getSettings", "@getSessionProperties"]);
|
||||
cy.findAllByRole("switch").first().should("not.be.checked").click();
|
||||
cy.findByRole("button", { name: "Agree" }).click();
|
||||
|
||||
cy.log("The merged setting is written");
|
||||
|
||||
// A fresh visit rather than cy.reload(): reloading the app inside the
|
||||
// Cypress runner leaves the hub, landing on /unauthorized and then home.
|
||||
cy.visit("/embedding/security");
|
||||
cy.wait(["@getSettings", "@getSessionProperties"]);
|
||||
|
||||
cy.findAllByRole("switch").first().should("be.checked");
|
||||
|
||||
cy.request("GET", "/api/session/properties").then(({ body }) => {
|
||||
expect(body["enable-embedding-modular"]).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it("lists published guest embeds even after guest embeds are switched off", () => {
|
||||
cy.log("Publish a dashboard as a guest embed");
|
||||
// Publishing is itself gated on guest embeds being on, so this has to
|
||||
// come before the dashboard is marked embeddable.
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.createDashboard({ name: "Published dashboard" }).then(
|
||||
({ body: dashboard }) => {
|
||||
cy.request("PUT", `/api/dashboard/${dashboard.id}`, {
|
||||
enable_embedding: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
assertPublishedDashboardIsListed();
|
||||
|
||||
cy.log(
|
||||
"Turning guest embeds off is exactly when an admin needs to audit what is already out there",
|
||||
);
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
assertPublishedDashboardIsListed();
|
||||
});
|
||||
});
|
||||
|
||||
describe("oss", { tags: "@OSS" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
});
|
||||
|
||||
it("shows the Pro upsell banner and the CORS setting", () => {
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByText(
|
||||
"Upgrade to Metabase Pro to access the SDK for React and more advanced options.",
|
||||
).should("be.visible");
|
||||
// OSS leaves PLUGIN_ADMIN_SETTINGS.useUpsellFlow without a trigger, so
|
||||
// UpsellCta renders the store link; paid builds get a button that
|
||||
// opens the in-app flow instead.
|
||||
cy.findByRole("link", { name: "Try Metabase Pro" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.findByText("Cross-Origin Resource Sharing (CORS)").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows every hub tab, gemming the ones this plan lacks", () => {
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
assertHubTabs(TAB_GEMS_WITHOUT_PAID_FEATURES);
|
||||
});
|
||||
});
|
||||
|
||||
describe("starter", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("starter");
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
});
|
||||
|
||||
it("shows the Pro upsell banner and the CORS setting", () => {
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
// Starter is below Pro too -- SDK access is still gated.
|
||||
cy.findByRole("button", { name: "Try Metabase Pro" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.findByText("Cross-Origin Resource Sharing (CORS)").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows every hub tab, gemming the ones this plan lacks", () => {
|
||||
cy.visit("/embedding/security");
|
||||
|
||||
assertHubTabs(TAB_GEMS_WITHOUT_PAID_FEATURES);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > embedding > embedding hub > authentication", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("saves JWT settings from the hub's copy of the admin form", () => {
|
||||
cy.intercept("PUT", "/api/setting").as("updateSettings");
|
||||
|
||||
cy.visit("/embedding/authentication");
|
||||
|
||||
cy.log("the hub supplies the heading, so the form does not add its own");
|
||||
cy.findByRole("heading", { name: "Authentication" }).should("be.visible");
|
||||
cy.findByRole("heading", { name: "JWT" }).should("not.exist");
|
||||
|
||||
cy.findByLabelText("JWT Identity Provider URI *")
|
||||
.type("https://jwt.example.com/auth")
|
||||
.blur();
|
||||
|
||||
cy.log("the shared secret is generated through the key modal");
|
||||
cy.button("Set up key").click();
|
||||
H.modal().within(() => {
|
||||
cy.button("Create").click();
|
||||
});
|
||||
|
||||
cy.button("Save and enable").click();
|
||||
cy.wait("@updateSettings");
|
||||
|
||||
cy.log("the settings the form writes are the real ones");
|
||||
cy.request("GET", "/api/session/properties").then(({ body }) => {
|
||||
expect(body["jwt-enabled"]).to.equal(true);
|
||||
expect(body["jwt-identity-provider-uri"]).to.equal(
|
||||
"https://jwt.example.com/auth",
|
||||
);
|
||||
});
|
||||
|
||||
cy.log("and they survive a reload of the tab");
|
||||
// cy.reload() does not work here: the app comes back without the hub
|
||||
// around it, so the page renders with no "Authentication" heading and the
|
||||
// form is never reached. Visiting the path again mounts the hub properly.
|
||||
cy.visit("/embedding/authentication");
|
||||
H.main()
|
||||
.findByLabelText("JWT Identity Provider URI *")
|
||||
.should("have.value", "https://jwt.example.com/auth");
|
||||
});
|
||||
|
||||
it("sends the admin to Admin settings when only SAML is configured", () => {
|
||||
configureSaml();
|
||||
|
||||
cy.visit("/embedding/authentication");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("SAML is configured").should("be.visible");
|
||||
cy.findByLabelText("JWT Identity Provider URI *").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlicensed", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("upsells instead of exposing the form", () => {
|
||||
cy.visit("/embedding/authentication");
|
||||
|
||||
// Card copy is unit-tested next to the component; this is what needs a
|
||||
// real browser -- the upsell rendering in place of the real form.
|
||||
H.main().within(() => {
|
||||
cy.findByText("Secure your embeds with single sign-on").should(
|
||||
"be.visible",
|
||||
);
|
||||
|
||||
cy.log("nothing configurable renders below the paywall");
|
||||
cy.findByLabelText("JWT Identity Provider URI *").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > embedding > embedding hub > permissions", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("navigates every tab and level of the editor without leaving the hub", () => {
|
||||
cy.visit("/embedding/permissions");
|
||||
|
||||
cy.log("The editor renders at the hub's path, not admin's");
|
||||
cy.location("pathname").should("match", /^\/embedding\/permissions/);
|
||||
|
||||
walkThroughPermissionsEditor();
|
||||
assertStillInsideHub();
|
||||
});
|
||||
|
||||
it("navigates every tab and level of the editor with tenants enabled", () => {
|
||||
// The tenant-only tabs (Shared collections, Tenant collections) only
|
||||
// show up once tenants are on, so this is the full tab set -- the
|
||||
// other test is the narrower one licensing alone shows.
|
||||
H.updateSetting("use-tenants", true);
|
||||
|
||||
cy.visit("/embedding/permissions");
|
||||
|
||||
cy.log("The editor renders at the hub's path, not admin's");
|
||||
cy.location("pathname").should("match", /^\/embedding\/permissions/);
|
||||
|
||||
walkThroughPermissionsEditor();
|
||||
|
||||
cy.log("Shared collections tab");
|
||||
cy.findByRole("tab", { name: "Shared collections" }).click();
|
||||
cy.findAllByRole("menuitem").first().click();
|
||||
cy.location("pathname").should("match", /^\/embedding\/permissions/);
|
||||
|
||||
cy.log("Tenant collections tab");
|
||||
cy.findByRole("tab", { name: "Tenant collections" }).click();
|
||||
cy.findAllByRole("menuitem").first().click();
|
||||
|
||||
assertStillInsideHub();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function walkThroughPermissionsEditor() {
|
||||
cy.log("Data tab, Groups view: lands here by default");
|
||||
cy.findAllByRole("menuitem").first().click();
|
||||
cy.findByTestId("permission-table").should("be.visible");
|
||||
|
||||
cy.log("Drilling into a group's database entity stays in the hub");
|
||||
cy.findByTestId("permission-table").findByText("Sample Database").click();
|
||||
cy.location("pathname").should("match", /^\/embedding\/permissions/);
|
||||
|
||||
cy.log("Data tab, Databases view");
|
||||
cy.findByRole("tab", { name: "Databases" }).click();
|
||||
cy.findAllByRole("menuitem").first().click();
|
||||
cy.findByTestId("permission-table").should("be.visible");
|
||||
|
||||
cy.log("Collections tab");
|
||||
cy.findByRole("tab", { name: "Collections" }).click();
|
||||
cy.findAllByRole("menuitem").first().click();
|
||||
cy.findByTestId("permission-table").should("be.visible");
|
||||
|
||||
cy.log("Application tab");
|
||||
cy.findByRole("tab", { name: "Application" }).click();
|
||||
cy.findByTestId("permission-table").should("be.visible");
|
||||
}
|
||||
|
||||
function assertStillInsideHub() {
|
||||
cy.log("Still inside the embedding hub throughout");
|
||||
cy.location("pathname").should("match", /^\/embedding\/permissions/);
|
||||
cy.findByTestId("embedding-hub-nav").should("be.visible");
|
||||
}
|
||||
|
||||
describe("scenarios > embedding > embedding hub > tenancy", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("offers the enable card while the multi-tenant strategy is off", () => {
|
||||
H.updateSetting("use-tenants", false);
|
||||
|
||||
cy.visit("/embedding");
|
||||
|
||||
cy.findByTestId("embedding-hub-nav")
|
||||
.findByRole("link", { name: "Tenancy" })
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/embedding/tenancy");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByText("Enable multi-tenant user strategy").should("be.visible");
|
||||
cy.findByRole("tab", { name: "Tenants" }).should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("carries the tenant surfaces once the strategy is on", () => {
|
||||
H.updateSetting("use-tenants", false);
|
||||
|
||||
cy.visit("/embedding/tenancy");
|
||||
|
||||
cy.log("Enable multi-tenancy from the enable card, not the API");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("button", { name: "Enable multi-tenancy" })
|
||||
.click();
|
||||
|
||||
cy.log("Enabling lands on the tenants listing, now empty");
|
||||
// The empty state's copy is interpolated with anchor links, so no
|
||||
// single node's text content matches the sentence exactly -- a regex
|
||||
// matches the substring instead.
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText(/Create your first tenant to start adding/)
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByRole("tab", { name: "Tenants" }).should("be.visible");
|
||||
cy.findByRole("tab", { name: "Tenant users" }).click();
|
||||
});
|
||||
|
||||
cy.log("The listing's own links stay inside the hub");
|
||||
cy.url().should("include", "/embedding/tenancy/people");
|
||||
cy.url().should("not.include", "/admin/people");
|
||||
});
|
||||
});
|
||||
|
||||
describe("oss", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("upsells rather than hiding the tab", () => {
|
||||
cy.visit("/embedding/tenancy");
|
||||
|
||||
// Card copy is unit-tested next to the component; this just confirms
|
||||
// the upsell renders in place of the real tenants surfaces.
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Use a multi-tenant user strategy")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > embedding > embedding hub > appearance", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("carries the theme listing and the branding settings on one tab", () => {
|
||||
cy.visit("/embedding");
|
||||
|
||||
cy.findByTestId("embedding-hub-nav")
|
||||
.findByRole("link", { name: "Appearance" })
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/embedding/appearance");
|
||||
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
cy.findByRole("heading", { name: "Appearance" }).should("be.visible");
|
||||
cy.findByRole("heading", { name: "Themes" }).should("be.visible");
|
||||
cy.findByRole("heading", { name: "Branding elements" }).should(
|
||||
"be.visible",
|
||||
);
|
||||
cy.findByText("Loading message").should("be.visible");
|
||||
cy.findByText("When calculations return no results").should(
|
||||
"be.visible",
|
||||
);
|
||||
cy.findByText("When no objects can be found").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("opens the theme editor inside the hub", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("button", { name: /New theme/ })
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/embedding/appearance/theme/new");
|
||||
cy.url().should("not.include", "/admin/embedding/themes");
|
||||
});
|
||||
|
||||
it("opens an existing theme inside the hub", () => {
|
||||
createThemeViaApi("Existing theme");
|
||||
cy.visit("/embedding/appearance");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Existing theme")
|
||||
.click();
|
||||
|
||||
// The listing is the admin one, mounted with the hub's basePath: card
|
||||
// clicks have to land here rather than on the admin route.
|
||||
cy.url().should("match", /\/embedding\/appearance\/theme\/\d+/);
|
||||
cy.url().should("not.include", "/admin/embedding/themes");
|
||||
});
|
||||
|
||||
it("creates a theme from the hub and shows it in the listing", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("button", { name: /New theme/ })
|
||||
.click();
|
||||
|
||||
cy.findByLabelText("Theme name").clear().type("Hub theme");
|
||||
cy.findByRole("button", { name: /Save theme/ }).click();
|
||||
|
||||
H.undoToastList().contains("Theme saved").should("be.visible");
|
||||
|
||||
cy.url().should("eq", Cypress.config().baseUrl + "/embedding/appearance");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Hub theme")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlicensed", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("upsells rather than hiding the tab", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Create custom themes")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("sends a theme editor deep link back to the upsell", () => {
|
||||
// A themeId, not the bare `theme` path -- that one redirects from the
|
||||
// route table whatever the plan, so it would pass without the guard.
|
||||
cy.visit("/embedding/appearance/theme/new");
|
||||
|
||||
cy.url().should("eq", Cypress.config().baseUrl + "/embedding/appearance");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Create custom themes")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > embedding > embedding hub > localization", () => {
|
||||
describe("pro", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("mounts the dictionary configuration", () => {
|
||||
cy.visit("/embedding/localization");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByTestId("content-translation-configuration")
|
||||
.findByText("Translate embedded dashboards and questions")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlicensed", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("upsells rather than hiding the tab", () => {
|
||||
cy.visit("/embedding/localization");
|
||||
|
||||
// Card copy is unit-tested next to the component; this just confirms
|
||||
// the upsell renders in place of the real dictionary configuration.
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Translate your embedded content")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function configureSaml() {
|
||||
cy.readFile("test_resources/sso/auth0-public-idp.cert", "utf8").then(
|
||||
(certificate) => {
|
||||
cy.request("PUT", "/api/setting", {
|
||||
"saml-enabled": true,
|
||||
"saml-identity-provider-uri": "https://example.test",
|
||||
"saml-identity-provider-certificate": certificate,
|
||||
"saml-identity-provider-issuer": "https://example.test/issuer",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function assertPublishedDashboardIsListed() {
|
||||
cy.findByTestId("embedding-hub-main").within(() => {
|
||||
// The hub clips its content and scrolls it internally, so this card sits
|
||||
// below the fold on a CI-sized viewport and Cypress reads it as hidden
|
||||
// until it is scrolled in.
|
||||
cy.findByText("Published guest embeds")
|
||||
.scrollIntoView()
|
||||
.should("be.visible");
|
||||
cy.findByText("Published dashboard").should("be.visible");
|
||||
});
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
ORDERS_QUESTION_ID,
|
||||
} from "e2e/support/cypress_sample_instance_data";
|
||||
|
||||
const standalonePath = "/embedding/security";
|
||||
const standalonePath = "/admin/embedding/guest";
|
||||
|
||||
// These tests will run on both OSS and EE instances. Both without a token!
|
||||
describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("show-sdk-embed-terms", false);
|
||||
});
|
||||
|
||||
it("should not offer to share or embed models (metabase#20815)", () => {
|
||||
@@ -39,14 +39,14 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
it("should show the sdk upsell link in oss", () => {
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
mainPage().within(() => {
|
||||
cy.findByRole("link", { name: "Try Metabase Pro" })
|
||||
cy.findByRole("link", { name: "Upgrade" })
|
||||
.should("have.attr", "href")
|
||||
.and(
|
||||
"eq",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_campaign=embedding-hub&utm_content=embedding-hub-security&source_plan=oss&utm_users=10",
|
||||
"https://www.metabase.com/upgrade?utm_source=product&utm_medium=upsell&utm_content=embedding-page&source_plan=oss&utm_users=10&utm_campaign=embedding-methods",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -71,7 +71,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
};
|
||||
["question", "dashboard"].forEach((object) => {
|
||||
it(`should be able to publish/embed and then unpublish a ${object} without filters`, () => {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
const embeddableObject = object === "question" ? "card" : "dashboard";
|
||||
@@ -181,10 +181,9 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
cy.visit(standalonePath);
|
||||
cy.wait("@currentlyEmbeddedObject");
|
||||
|
||||
// The hub's Security tab renders this section only when published
|
||||
// guest embeds exist, so unpublishing empties it rather than
|
||||
// leaving a section with no rows.
|
||||
mainPage().findByTestId("embedded-resources").should("not.exist");
|
||||
mainPage()
|
||||
.findAllByText(/No (questions|dashboards) have been embedded yet./)
|
||||
.should("have.length", 2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,7 +200,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
enable_embedding: true,
|
||||
});
|
||||
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
|
||||
@@ -232,7 +231,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
it("should regenerate embedding token and invalidate previous embed url", () => {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
|
||||
@@ -298,7 +297,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
|
||||
});
|
||||
|
||||
function resetEmbedding() {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
H.updateSetting("embedding-secret-key", null);
|
||||
}
|
||||
|
||||
@@ -324,5 +323,5 @@ function visitAndEnableSharing(object, unpublishBeforeOpen = true) {
|
||||
}
|
||||
|
||||
function mainPage() {
|
||||
return cy.findByTestId("embedding-hub-main");
|
||||
return cy.findByTestId("admin-layout-content");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ function createThemeViaApi(name = "Test theme") {
|
||||
|
||||
function visitThemeEditor(themeId: number) {
|
||||
cy.intercept("GET", `/api/embed-theme/${themeId}`).as("getTheme");
|
||||
cy.visit(`/embedding/appearance/theme/${themeId}`);
|
||||
cy.visit(`/admin/embedding/themes/${themeId}`);
|
||||
cy.wait("@getTheme");
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ describe(
|
||||
cy.log("editor panel should show the theme name");
|
||||
cy.findByLabelText("Theme name").should("have.value", "My custom theme");
|
||||
|
||||
cy.log("theme editor should render full width");
|
||||
cy.findByTestId("embedding-hub-content-cap").should("not.exist");
|
||||
cy.log("sidebar should be hidden on theme editor page");
|
||||
cy.findByTestId("admin-layout-sidebar").should("not.exist");
|
||||
|
||||
cy.log("save button should be disabled when no changes");
|
||||
cy.findByRole("button", { name: /Save theme/ }).should("be.disabled");
|
||||
@@ -105,12 +105,12 @@ describe(
|
||||
cy.findByRole("button", { name: /Cancel/ }).click();
|
||||
|
||||
cy.log("should navigate back to the themes listing");
|
||||
cy.url().should("include", "/embedding/appearance");
|
||||
cy.url().should("not.match", /\/theme\/\d+/);
|
||||
cy.url().should("include", "/admin/embedding/themes");
|
||||
cy.url().should("not.match", /\/themes\/\d+/);
|
||||
});
|
||||
|
||||
it("shows not found for invalid theme id", () => {
|
||||
cy.visit("/embedding/appearance/theme/99999");
|
||||
cy.visit("/admin/embedding/themes/99999");
|
||||
|
||||
H.main().findByText("We're a little lost...").should("be.visible");
|
||||
});
|
||||
@@ -139,7 +139,7 @@ describe(
|
||||
});
|
||||
|
||||
cy.log("should remain on the editor page after cancelling");
|
||||
cy.url().should("match", /\/theme\/\d+/);
|
||||
cy.url().should("match", /\/themes\/\d+/);
|
||||
|
||||
cy.log("confirm deletion");
|
||||
cy.findByRole("button", { name: /Delete theme/ }).click();
|
||||
@@ -150,8 +150,8 @@ describe(
|
||||
H.undoToast().findByText("Theme deleted successfully").should("exist");
|
||||
|
||||
cy.log("should navigate back to the themes listing");
|
||||
cy.url().should("include", "/embedding/appearance");
|
||||
cy.url().should("not.match", /\/theme\/\d+/);
|
||||
cy.url().should("include", "/admin/embedding/themes");
|
||||
cy.url().should("not.match", /\/themes\/\d+/);
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("Theme to delete").should("not.exist");
|
||||
@@ -181,13 +181,13 @@ describe(
|
||||
H.undoToast().findByText("Theme deleted successfully").should("exist");
|
||||
|
||||
cy.log("should land on the themes listing — not 404 or leave-prompt");
|
||||
cy.url().should("include", "/embedding/appearance");
|
||||
cy.url().should("not.match", /\/theme\/\d+/);
|
||||
cy.url().should("include", "/admin/embedding/themes");
|
||||
cy.url().should("not.match", /\/themes\/\d+/);
|
||||
H.main().findByText("We're a little lost...").should("not.exist");
|
||||
});
|
||||
|
||||
it("does not show the delete button when creating a new theme", () => {
|
||||
cy.visit("/embedding/appearance/theme/new");
|
||||
cy.visit("/admin/embedding/themes/new");
|
||||
|
||||
cy.findByLabelText("Theme name").should("be.visible");
|
||||
cy.findByRole("button", { name: /Delete theme/ }).should("not.exist");
|
||||
@@ -365,7 +365,7 @@ describe(
|
||||
|
||||
describe("preview panel", () => {
|
||||
it("shows enable embedding prompt when embedding is not enabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-simple", false);
|
||||
|
||||
createThemeViaApi("Preview test").then((theme) => {
|
||||
visitThemeEditor(theme.id);
|
||||
@@ -384,8 +384,8 @@ describe(
|
||||
});
|
||||
|
||||
it("shows theme preview when embedding is enabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("show-simple-embed-terms", false);
|
||||
|
||||
createThemeViaApi("Preview test").then((theme) => {
|
||||
visitThemeEditor(theme.id);
|
||||
@@ -398,8 +398,8 @@ describe(
|
||||
|
||||
describe("preview picker", () => {
|
||||
beforeEach(() => {
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("show-simple-embed-terms", false);
|
||||
});
|
||||
|
||||
it("defaults to a dashboard and can switch to a question", () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe(
|
||||
});
|
||||
|
||||
it("lazily seeds Light and Dark on first visit, and preserves admin deletions across reloads", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.log(
|
||||
@@ -46,23 +46,23 @@ describe(
|
||||
cy.findByRole("button", { name: /New theme/ }).click();
|
||||
});
|
||||
|
||||
cy.url().should("match", /\/embedding\/appearance\/theme\/new$/);
|
||||
cy.url().should("match", /\/admin\/embedding\/themes\/new$/);
|
||||
});
|
||||
|
||||
it("does not create a theme when cancelling from the draft editor", () => {
|
||||
cy.intercept("POST", "/api/embed-theme").as("createTheme");
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main()
|
||||
.findByRole("button", { name: /New theme/ })
|
||||
.click();
|
||||
|
||||
cy.url().should("match", /\/embedding\/appearance\/theme\/new$/);
|
||||
cy.url().should("match", /\/admin\/embedding\/themes\/new$/);
|
||||
|
||||
cy.findByRole("button", { name: /Cancel/ }).click();
|
||||
|
||||
cy.log("navigates back to the listing");
|
||||
cy.url().should("match", /\/embedding\/appearance$/);
|
||||
cy.url().should("match", /\/admin\/embedding\/themes$/);
|
||||
|
||||
cy.log("new theme card is still visible");
|
||||
H.main()
|
||||
@@ -75,14 +75,14 @@ describe(
|
||||
|
||||
it("navigates to theme editor when clicking an existing theme card", () => {
|
||||
createThemeViaApi("My theme");
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("My theme").click();
|
||||
});
|
||||
|
||||
cy.log("navigates to the theme editor page");
|
||||
cy.url().should("match", /\/embedding\/appearance\/theme\/\d+/);
|
||||
cy.url().should("match", /\/admin\/embedding\/themes\/\d+/);
|
||||
});
|
||||
|
||||
it("uses white-labeled colors as a base for creating themes", () => {
|
||||
@@ -98,7 +98,7 @@ describe(
|
||||
H.updateSetting("application-colors", whitelabelColors);
|
||||
|
||||
cy.intercept("POST", "/api/embed-theme").as("createTheme");
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main()
|
||||
.findByRole("button", { name: /New theme/ })
|
||||
@@ -126,7 +126,7 @@ describe(
|
||||
|
||||
it("can duplicate a theme", () => {
|
||||
createThemeViaApi("Untitled theme");
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("Untitled theme").should("be.visible");
|
||||
@@ -146,7 +146,7 @@ describe(
|
||||
|
||||
it("can delete a theme with confirmation", () => {
|
||||
createThemeViaApi("Untitled theme");
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().findByText("Untitled theme").should("be.visible");
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
});
|
||||
|
||||
it("shows the upsell with Metabase Pro copy and an external upgrade link", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
cy.log("nav label has an upsell gem");
|
||||
cy.findByRole("navigation", { name: "Embedding hub" })
|
||||
.findByRole("link", { name: "Appearance" })
|
||||
.findByTestId("upsell-gem")
|
||||
.should("exist");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Themes/ })
|
||||
.within(() => {
|
||||
cy.icon("gem").should("be.visible");
|
||||
});
|
||||
|
||||
H.main().within(() => {
|
||||
cy.log("upsell copy matches the Figma");
|
||||
@@ -54,13 +55,14 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
body: { available: false, plan_alias: "pro-cloud" },
|
||||
});
|
||||
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
cy.log("nav label has an upsell gem");
|
||||
cy.findByRole("navigation", { name: "Embedding hub" })
|
||||
.findByRole("link", { name: "Appearance" })
|
||||
.findByTestId("upsell-gem")
|
||||
.should("exist");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Themes/ })
|
||||
.within(() => {
|
||||
cy.icon("gem").should("be.visible");
|
||||
});
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("Metabase Pro").should("be.visible");
|
||||
@@ -101,7 +103,7 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
body: { available: false, plan_alias: "pro-cloud" },
|
||||
});
|
||||
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("Metabase Pro").should("be.visible");
|
||||
@@ -113,7 +115,7 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
cy.findByText(/Please ask a Metabase Store Admin/).should("not.exist");
|
||||
|
||||
cy.log("no trial → no trial line");
|
||||
cy.findByText(/14 day trial/).should("not.exist");
|
||||
cy.findByText(/14-day free trial/).should("not.exist");
|
||||
|
||||
cy.log(
|
||||
"CTA is rendered (hosted starter as store admin → opens the upgrade modal)",
|
||||
@@ -143,7 +145,7 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
body: { available: true, plan_alias: "pro-cloud" },
|
||||
});
|
||||
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
H.main().within(() => {
|
||||
cy.findByText("Metabase Pro").should("be.visible");
|
||||
@@ -153,7 +155,7 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
|
||||
cy.log("trial line is rendered");
|
||||
cy.findByText(
|
||||
/Get a 14 day trial of this and other Pro features/,
|
||||
/Get a 14-day free trial of this and other pro features/,
|
||||
).should("be.visible");
|
||||
|
||||
cy.log("button text switches to 'Try for free'");
|
||||
@@ -171,13 +173,14 @@ describe("scenarios > embedding > themes > upsell", () => {
|
||||
});
|
||||
|
||||
it("does not show the upsell and renders the themes listing", () => {
|
||||
cy.visit("/embedding/appearance");
|
||||
cy.visit("/admin/embedding/themes");
|
||||
|
||||
cy.log("nav label has no upsell gem");
|
||||
cy.findByRole("navigation", { name: "Embedding hub" })
|
||||
.findByRole("link", { name: "Appearance" })
|
||||
.findByTestId("upsell-gem")
|
||||
.should("not.exist");
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByRole("link", { name: /Themes/ })
|
||||
.within(() => {
|
||||
cy.icon("gem").should("not.exist");
|
||||
});
|
||||
|
||||
H.main().within(() => {
|
||||
cy.log("upsell copy is absent");
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > modular embedding settings", { tags: "@EE" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("should link to user strategy when tenants are disabled", () => {
|
||||
cy.visit("/admin/embedding/modular");
|
||||
|
||||
H.main()
|
||||
.findByText("Tenants")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/admin/people/user-strategy");
|
||||
});
|
||||
|
||||
it("should link to tenants page when tenants are enabled", () => {
|
||||
H.updateSetting("use-tenants", true);
|
||||
cy.visit("/admin/embedding/modular");
|
||||
|
||||
H.main()
|
||||
.findByText("Tenants")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/admin/people/tenants");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import { enableJwtAuth } from "e2e/support/helpers/e2e-jwt-helpers";
|
||||
|
||||
import {
|
||||
clickNewEmbedButton,
|
||||
getEmbedSidebar,
|
||||
navigateToEntitySelectionStep,
|
||||
navigateToGetCodeStep,
|
||||
@@ -22,8 +21,9 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("show-simple-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("show-static-embed-terms", false);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
@@ -44,9 +44,7 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
});
|
||||
|
||||
H.modal().should("not.exist");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("heading", { name: "Security" })
|
||||
.should("be.visible");
|
||||
cy.findAllByTestId("sdk-setting-card").should("be.visible");
|
||||
});
|
||||
|
||||
it("should close wizard when clicking `Done` button on the last step", () => {
|
||||
@@ -65,9 +63,7 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
});
|
||||
|
||||
H.modal().should("not.exist");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("heading", { name: "Security" })
|
||||
.should("be.visible");
|
||||
cy.findAllByTestId("sdk-setting-card").should("be.visible");
|
||||
});
|
||||
|
||||
it("should close wizard when navigating back in browser history", () => {
|
||||
@@ -76,12 +72,14 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
.contains("General")
|
||||
.should("be.visible");
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("heading", { name: "Security" })
|
||||
.should("be.visible");
|
||||
cy.visit("/admin/embedding");
|
||||
cy.findAllByTestId("sdk-setting-card").should("be.visible");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId("sdk-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.wait("@dashboard");
|
||||
|
||||
@@ -98,8 +96,8 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
});
|
||||
|
||||
describe("auth type switch", () => {
|
||||
it("allows to select the `guest` item even when modular embedding is disabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
it("allows to select the `guest` item even when static embedding setting is disabled", () => {
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
|
||||
@@ -108,8 +106,8 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
cy.findByLabelText("Guest").should("be.enabled");
|
||||
});
|
||||
|
||||
it("allows to select the `Metabase Account` item even when modular embedding is disabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
it("allows to select the `Metabase Account` item even when simple embedding setting is disabled", () => {
|
||||
H.updateSetting("enable-embedding-simple", false);
|
||||
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
|
||||
@@ -129,9 +127,22 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
.click();
|
||||
};
|
||||
|
||||
const openFromEmbeddingHub = () => {
|
||||
cy.visit("/embedding/security");
|
||||
clickNewEmbedButton();
|
||||
const openFromAdminEmbedding = () => {
|
||||
cy.visit("/admin/embedding");
|
||||
cy.findAllByTestId("sdk-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
};
|
||||
|
||||
const openFromAdminGuestEmbeds = () => {
|
||||
cy.visit("/admin/embedding/guest");
|
||||
cy.findAllByTestId("guest-embeds-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
};
|
||||
|
||||
const openFromSharingMenu = () => {
|
||||
@@ -158,22 +169,30 @@ describe("scenarios > embedding > sdk iframe embed setup > common", () => {
|
||||
openFromCommandPalette();
|
||||
assertCheckedAuth("sso");
|
||||
|
||||
openFromEmbeddingHub();
|
||||
openFromAdminEmbedding();
|
||||
assertCheckedAuth("sso");
|
||||
|
||||
openFromSharingMenu();
|
||||
assertCheckedAuth("sso");
|
||||
|
||||
// The Guest embeds admin section is intentionally guest-only and
|
||||
// forces guest mode regardless of SSO configuration.
|
||||
openFromAdminGuestEmbeds();
|
||||
assertCheckedAuth("guest");
|
||||
});
|
||||
|
||||
it("defaults to Guest from all entry points when SSO is not configured", () => {
|
||||
openFromCommandPalette();
|
||||
assertCheckedAuth("guest");
|
||||
|
||||
openFromEmbeddingHub();
|
||||
openFromAdminEmbedding();
|
||||
assertCheckedAuth("guest");
|
||||
|
||||
openFromSharingMenu();
|
||||
assertCheckedAuth("guest");
|
||||
|
||||
openFromAdminGuestEmbeds();
|
||||
assertCheckedAuth("guest");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("scenarios > embedding > sdk iframe embed setup > common (oss and start
|
||||
});
|
||||
|
||||
it("allows to select the `guest` item even when static embedding setting is disabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("scenarios > embedding > sdk iframe embed setup > common (oss and start
|
||||
});
|
||||
|
||||
it("does not allow to select the `Metabase Account`, when token feature is missing (oss)", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-simple", false);
|
||||
|
||||
H.visitQuestion(ORDERS_COUNT_QUESTION_ID);
|
||||
|
||||
|
||||
@@ -4,30 +4,32 @@ import {
|
||||
openSharingMenu,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
import { clickNewEmbedButton, getEmbedSidebar } from "./helpers";
|
||||
import { getEmbedSidebar } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
const DATA_BY_EMBEDDING_TYPE = {
|
||||
guest: {
|
||||
path: "/embedding/security",
|
||||
path: "/admin/embedding/guest",
|
||||
token: null,
|
||||
authMethodLabel: "Guest",
|
||||
cardTestId: "guest-embeds-setting-card",
|
||||
cardText:
|
||||
"To continue, enable embedding and agree to the usage conditions.",
|
||||
embeddingSettingName: "enable-embedding-modular",
|
||||
"To continue, enable guest embeds and agree to the usage conditions.",
|
||||
embeddingSettingName: "enable-embedding-static",
|
||||
showTermsSettingName: "show-static-embed-terms",
|
||||
tooltipText:
|
||||
/You should, however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature/,
|
||||
},
|
||||
modular: {
|
||||
path: "/embedding/security",
|
||||
path: "/admin/embedding",
|
||||
token: "bleeding-edge",
|
||||
authMethodLabel: "Metabase account (SSO)",
|
||||
cardTestId: "sdk-setting-card",
|
||||
cardText:
|
||||
"To continue, enable modular embedding and agree to the usage conditions.",
|
||||
embeddingSettingName: "enable-embedding-modular",
|
||||
showTermsSettingName: "show-modular-embed-terms",
|
||||
embeddingSettingName: "enable-embedding-simple",
|
||||
showTermsSettingName: "show-simple-embed-terms",
|
||||
tooltipText: /Sharing Metabase accounts is a security risk/,
|
||||
},
|
||||
} as const;
|
||||
@@ -48,6 +50,7 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (EE)"
|
||||
authMethodLabel,
|
||||
embeddingSettingName,
|
||||
showTermsSettingName,
|
||||
cardTestId,
|
||||
cardText,
|
||||
tooltipText,
|
||||
} = value;
|
||||
@@ -64,7 +67,11 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (EE)"
|
||||
|
||||
cy.visit(path);
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.findByLabelText(authMethodLabel).click();
|
||||
|
||||
@@ -120,7 +127,11 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (EE)"
|
||||
|
||||
cy.visit(path);
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.findByLabelText(authMethodLabel).click();
|
||||
|
||||
@@ -145,7 +156,11 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (EE)"
|
||||
|
||||
cy.visit(path);
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId(cardTestId)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.findByLabelText(authMethodLabel).click();
|
||||
|
||||
@@ -155,7 +170,7 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (EE)"
|
||||
});
|
||||
|
||||
it("shows guest embed status bar when guest embedding is toggled from disabled to enabled state", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { embedModalEnableEmbeddingCard } from "e2e/support/helpers";
|
||||
|
||||
import { clickNewEmbedButton, getEmbedSidebar } from "./helpers";
|
||||
import { getEmbedSidebar } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
@@ -23,16 +23,20 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (oss
|
||||
|
||||
describe("guest", () => {
|
||||
it("shows the Enable to Continue button and enables embedding on click", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("enable-embedding-static", false);
|
||||
H.updateSetting("show-static-embed-terms", true);
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId("guest-embeds-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
embedModalEnableEmbeddingCard().should(
|
||||
"contain.text",
|
||||
"To continue, enable embedding and agree to the usage conditions.",
|
||||
"To continue, enable guest embeds and agree to the usage conditions.",
|
||||
);
|
||||
|
||||
embedModalEnableEmbeddingCard().within(() => {
|
||||
@@ -95,12 +99,16 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (oss
|
||||
});
|
||||
|
||||
it("shows the enable card with fair usage terms when embedding is already enabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("show-static-embed-terms", true);
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId("guest-embeds-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
embedModalEnableEmbeddingCard()
|
||||
.should("contain.text", "Agree to the")
|
||||
@@ -116,7 +124,7 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (oss
|
||||
.should("have.attr", "target", "_blank");
|
||||
|
||||
cy.findByText(
|
||||
"To continue, enable embedding and agree to the usage conditions.",
|
||||
/To continue, enable guest embeds and agree to the/,
|
||||
).should("not.exist");
|
||||
});
|
||||
|
||||
@@ -131,16 +139,20 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (oss
|
||||
});
|
||||
|
||||
it("hides the enable card when embedding is already enabled", () => {
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("show-static-embed-terms", false);
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId("guest-embeds-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
getEmbedSidebar()
|
||||
.contains(
|
||||
"To continue, enable embedding and agree to the usage conditions.",
|
||||
"To continue, enable guest embeds and agree to the usage conditions.",
|
||||
)
|
||||
.should("not.exist");
|
||||
});
|
||||
@@ -148,16 +160,20 @@ describe("scenarios > embedding > sdk iframe embed setup > enable embed js (oss
|
||||
|
||||
describe("Metabase account (sso)", () => {
|
||||
it("does not show the Enable to Continue button and disables item", () => {
|
||||
H.updateSetting("enable-embedding-modular", false);
|
||||
H.updateSetting("show-modular-embed-terms", true);
|
||||
H.updateSetting("enable-embedding-simple", false);
|
||||
H.updateSetting("show-simple-embed-terms", true);
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId("guest-embeds-setting-card")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
embedModalEnableEmbeddingCard().within(() => {
|
||||
cy.findByText(
|
||||
"To continue, enable embedding and agree to the usage conditions.",
|
||||
"To continue, enable guest embeds and agree to the usage conditions.",
|
||||
).should("not.exist");
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ describe(suiteTitle, () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
|
||||
@@ -26,8 +26,9 @@ describe(suiteTitle, () => {
|
||||
// guest mode while SSO is unconfigured, so an unaccepted guest CTA would
|
||||
// otherwise have to be clicked through first. That flow is covered by
|
||||
// embed-flow-enable-embed-js-*.
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("show-simple-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("show-static-embed-terms", false);
|
||||
H.mockEmbedJsToDevServer();
|
||||
});
|
||||
|
||||
@@ -21,8 +21,9 @@ describe("scenarios > embedding > sdk iframe embed setup > get code step", () =>
|
||||
H.enableTracking();
|
||||
// Accept the embedding terms up front so the wizard never shows the
|
||||
// Agree CTA — its flow is covered by embed-flow-enable-embed-js-*.
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("show-modular-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("show-simple-embed-terms", false);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.updateSetting("show-static-embed-terms", false);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
|
||||
@@ -24,18 +24,18 @@ export const getResourceSelectorButton = (
|
||||
options?: Partial<Cypress.Timeoutable>,
|
||||
) => cy.findByTestId("embed-browse-entity-button", options);
|
||||
|
||||
// "New embed" is a button in the embedding hub's nav.
|
||||
export const clickNewEmbedButton = () =>
|
||||
cy.findByRole("button", { name: "New embed" }).click();
|
||||
|
||||
export const visitNewEmbedPage = (
|
||||
{ waitForResource } = { waitForResource: true },
|
||||
) => {
|
||||
cy.intercept("GET", "/api/dashboard/*").as("dashboard");
|
||||
|
||||
cy.visit("/embedding/security");
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
clickNewEmbedButton();
|
||||
cy.findAllByTestId(/(sdk-setting-card|guest-embeds-setting-card)/)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
|
||||
cy.get("body").then(() => {
|
||||
if (waitForResource) {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe(suiteTitle, () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
@@ -291,7 +291,7 @@ describe("recently created dashboards", () => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
cy.intercept("GET", "/api/activity/recents?*").as("recentActivity");
|
||||
|
||||
@@ -24,7 +24,8 @@ describe(suiteTitle, () => {
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.enableTracking();
|
||||
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.updateSetting("enable-embedding-static", true);
|
||||
H.setupAnthropicLlmProvider();
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/*").as("dashboard");
|
||||
@@ -174,13 +175,12 @@ describe(suiteTitle, () => {
|
||||
cy.visit(`/question/${ORDERS_QUESTION_ID}`);
|
||||
|
||||
H.openEmbedJsModal();
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
cy.get("#iframe-embed-container")
|
||||
.findByTestId("preview-loading-indicator", { timeout: 20_000 })
|
||||
.should("be.visible");
|
||||
|
||||
H.embedModalEnableEmbedding();
|
||||
|
||||
cy.get("[data-iframe-loaded]", { timeout: 20_000 }).should(
|
||||
"have.length",
|
||||
1,
|
||||
@@ -198,13 +198,6 @@ describe(suiteTitle, () => {
|
||||
|
||||
visitNewEmbedPage();
|
||||
|
||||
// The hub's "New embed" button sits in the nav and is clickable as soon as
|
||||
// the layout mounts, where the admin settings card it replaced only
|
||||
// appeared once that page had loaded its settings. That delay was giving
|
||||
// the throttled recents response a head start; without it the wizard is
|
||||
// still resolving when the assertions run.
|
||||
cy.wait("@getRecents");
|
||||
|
||||
H.getSimpleEmbedIframeContent().within(() => {
|
||||
cy.findByText("Person overview").should("not.exist");
|
||||
cy.findByText("Orders in a dashboard").should("be.visible");
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("OSS", { tags: "@OSS" }, () => {
|
||||
H.resetSnowplow();
|
||||
cy.signInAsAdmin();
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
@@ -69,7 +69,7 @@ describe("EE without license", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("starter");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
@@ -95,7 +95,7 @@ describe(suiteTitle, () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.enableTracking();
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
H.setupAnthropicLlmProvider();
|
||||
|
||||
cy.intercept("GET", "/api/dashboard/**").as("dashboard");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clickNewEmbedButton, navigateToEmbedOptionsStep } from "./helpers";
|
||||
import { navigateToEmbedOptionsStep } from "./helpers";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
@@ -33,12 +33,20 @@ const assertPreviewFinishesLoading = () => {
|
||||
cy.findByTestId("preview-loading-indicator").should("not.exist");
|
||||
};
|
||||
|
||||
const reopenNewEmbedModal = () => {
|
||||
cy.findAllByTestId(/(sdk-setting-card|guest-embeds-setting-card)/)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByText("New embed").click();
|
||||
});
|
||||
};
|
||||
|
||||
describe("scenarios > embedding > sdk iframe embed setup > user settings persistence", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
H.updateSetting("enable-embedding-modular", true);
|
||||
H.updateSetting("enable-embedding-simple", true);
|
||||
|
||||
cy.intercept("PUT", "/api/setting/sdk-iframe-embed-setup-settings").as(
|
||||
"persistSettings",
|
||||
@@ -111,7 +119,7 @@ describe("scenarios > embedding > sdk iframe embed setup > user settings persist
|
||||
cy.findByTestId("sdk-iframe-embed-setup-modal-content").should("not.exist");
|
||||
|
||||
cy.log("reopen the New embed modal");
|
||||
clickNewEmbedButton();
|
||||
reopenNewEmbedModal();
|
||||
|
||||
cy.log(
|
||||
"the preview must finish loading — the restored saved theme must not leave it stuck on the loader",
|
||||
|
||||
@@ -256,10 +256,10 @@ describe("scenarios > embedding > sdk iframe embedding > internal-navigation", (
|
||||
|
||||
it("should not support internal navigation on guest embeds", () => {
|
||||
// Enable guest embedding settings
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-simple", {
|
||||
value: true,
|
||||
});
|
||||
cy.request("PUT", "/api/setting/enable-embedding-modular", {
|
||||
cy.request("PUT", "/api/setting/enable-embedding-static", {
|
||||
value: true,
|
||||
});
|
||||
updateSetting("embedding-secret-key", JWT_SHARED_SECRET);
|
||||
|
||||
@@ -14,24 +14,24 @@ describe("scenarios - setup guide", () => {
|
||||
H.activateToken("pro-cloud");
|
||||
});
|
||||
|
||||
it("Contains the setup guide in the hub nav", () => {
|
||||
cy.visit("/embedding/security");
|
||||
it("Contains setup guide in sidebar", () => {
|
||||
cy.visit("/admin/embedding");
|
||||
|
||||
cy.findByRole("navigation", { name: "Embedding hub" })
|
||||
.findByRole("link", { name: "Get started" })
|
||||
cy.findByTestId("admin-layout-sidebar")
|
||||
.findByText("Setup guide")
|
||||
.should("exist")
|
||||
.click();
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByRole("heading", { name: "Get started with Metabase Embedding" })
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByRole("heading", { name: "Embedding setup guide" })
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it('"Create a dashboard" card should save the x-ray and show a success toast without leaving the guide', () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.log("Find and click on 'Create a dashboard' card");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Create a dashboard")
|
||||
.click();
|
||||
|
||||
@@ -50,14 +50,14 @@ describe("scenarios - setup guide", () => {
|
||||
H.undoToast().findByText("See it").should("be.visible");
|
||||
|
||||
cy.log("Should remain on the setup guide");
|
||||
cy.url().should("include", "/embedding/get-started");
|
||||
cy.url().should("include", "/admin/embedding/setup-guide");
|
||||
});
|
||||
|
||||
it('"Connect a database" card should pass from param in navigation URL', () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.log("Find and click on 'Connect a database' card");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Connect a database")
|
||||
.click();
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("Uploading CSVs to sample database should mark the 'Add Data' step as done", () => {
|
||||
cy.intercept("GET", "/api/embedding-hub/checklist").as("getChecklist");
|
||||
cy.intercept("GET", "/api/ee/embedding-hub/checklist").as("getChecklist");
|
||||
|
||||
cy.log("Enable CSV uploads");
|
||||
cy.request("PUT", "/api/setting/uploads-settings", {
|
||||
@@ -86,16 +86,16 @@ describe("scenarios - setup guide", () => {
|
||||
},
|
||||
});
|
||||
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.log("'Connect a database' should not be marked as done");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Connect a database")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.findByLabelText("Step 1 complete")
|
||||
.closest("button")
|
||||
.findByText("Done")
|
||||
.should("not.exist");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Connect a database")
|
||||
.click();
|
||||
|
||||
@@ -122,18 +122,18 @@ describe("scenarios - setup guide", () => {
|
||||
cy.wait("@getChecklist");
|
||||
|
||||
cy.log("'Connect a database' should be marked as done");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Connect a database")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.closest("button")
|
||||
.scrollIntoView()
|
||||
.findByLabelText("Step 1 complete")
|
||||
.findByText("Done")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it('"Get embed snippet" card should take you to the embed flow', () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get embed snippet")
|
||||
.click();
|
||||
|
||||
@@ -148,17 +148,17 @@ describe("scenarios - setup guide", () => {
|
||||
cy.log("Create a dashboard to embed");
|
||||
H.createDashboard({ name: "Test Dashboard" });
|
||||
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.log("step should not be marked as done at first");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get embed snippet")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.findByLabelText("Step 3 complete")
|
||||
.closest("button")
|
||||
.findByText("Done")
|
||||
.should("not.exist");
|
||||
|
||||
cy.log("open embed wizard");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get embed snippet")
|
||||
.click();
|
||||
|
||||
@@ -189,10 +189,10 @@ describe("scenarios - setup guide", () => {
|
||||
H.modal().first().findByLabelText("Close").click();
|
||||
|
||||
cy.log("step should be marked as done");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get embed snippet")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.findByLabelText("Step 3 complete", { timeout: 10_000 })
|
||||
.closest("button")
|
||||
.findByText("Done", { timeout: 10_000 })
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
@@ -271,14 +271,45 @@ describe("scenarios - setup guide", () => {
|
||||
.should("not.exist");
|
||||
});
|
||||
|
||||
it('"Configure data permissions and tenants" card should navigate to permissions onboarding page', () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
it("should link to user strategy when tenants are disabled", () => {
|
||||
H.restore("setup");
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Configure data permissions and tenants")
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
H.main()
|
||||
.findByText("Tenants")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/admin/people/user-strategy");
|
||||
});
|
||||
|
||||
it("should link to tenants page when tenants are enabled", () => {
|
||||
H.restore("setup");
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
H.updateSetting("use-tenants", true);
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
H.main()
|
||||
.findByText("Tenants")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/admin/people/tenants");
|
||||
});
|
||||
|
||||
it('"Configure data permissions and enable tenants" card should navigate to permissions onboarding page', () => {
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Configure data permissions and enable tenants")
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/embedding/get-started/permissions");
|
||||
cy.url().should("include", "/admin/embedding/setup-guide/permissions");
|
||||
|
||||
H.main()
|
||||
.findByText("Configure data permissions and enable tenants")
|
||||
@@ -291,7 +322,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("all 5 steps are present and none are completed at first");
|
||||
H.main().within(() => {
|
||||
@@ -352,9 +383,9 @@ describe("scenarios - setup guide", () => {
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.log("create an x-ray dashboard via the embedding setup guide");
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Create a dashboard")
|
||||
.click();
|
||||
|
||||
@@ -368,7 +399,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.log("wait for x-ray dashboard to generate and save it");
|
||||
H.undoToast().should("contain", "Your dashboard was saved");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("tenants should not be enabled");
|
||||
cy.request("GET", "/api/session/properties").then((response) => {
|
||||
@@ -473,7 +504,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.log("enable tenants via setting without creating a shared collection");
|
||||
H.updateSetting("use-tenants", true);
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("no steps should be completed");
|
||||
H.main().within(() => {
|
||||
@@ -504,7 +535,7 @@ describe("scenarios - setup guide", () => {
|
||||
namespace: "shared-tenant-collection",
|
||||
});
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("wait until step is marked as complete");
|
||||
H.main().icon("check").should("have.length", 1);
|
||||
@@ -525,7 +556,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("click the enable tenants button");
|
||||
H.main()
|
||||
@@ -584,7 +615,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("can create two tenants and show summary", () => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("step 1 should be marked as done before navigating");
|
||||
H.main()
|
||||
@@ -668,11 +699,11 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
cy.log("Configure data permissions step should be done");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Configure data permissions and tenants")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Configure data permissions and enable tenants")
|
||||
.closest("button")
|
||||
.scrollIntoView()
|
||||
.findByLabelText("Step 4 complete")
|
||||
.findByText("Done")
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("verify tenant_attributes are saved correctly via API");
|
||||
@@ -714,7 +745,7 @@ describe("scenarios - setup guide", () => {
|
||||
slug: "existing-tenant",
|
||||
});
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
H.main()
|
||||
.findByRole("listitem", { name: "Create tenants" })
|
||||
@@ -748,7 +779,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("reloads with strategy pre-selected and 'Select data' step unlocked when RLS is configured", () => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log(
|
||||
"'Select data' step should not be locked when RLS is configured",
|
||||
@@ -774,7 +805,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("enable tenants and create shared collection");
|
||||
H.main()
|
||||
@@ -837,7 +868,7 @@ describe("scenarios - setup guide", () => {
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("enable tenants and create shared collection");
|
||||
H.main()
|
||||
@@ -924,7 +955,7 @@ describe("scenarios - setup guide", () => {
|
||||
"updatePermissionsGraph",
|
||||
);
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("steps 3, 4, and 5 should be locked initially");
|
||||
H.main().within(() => {
|
||||
@@ -1147,7 +1178,7 @@ describe("scenarios - setup guide", () => {
|
||||
},
|
||||
);
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("wait for checklist data to load before interacting with steps");
|
||||
H.main()
|
||||
@@ -1259,7 +1290,7 @@ describe("scenarios - setup guide", () => {
|
||||
"updatePermissionsGraph",
|
||||
);
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("enable tenants and create shared collection");
|
||||
H.main()
|
||||
@@ -1361,22 +1392,26 @@ describe("scenarios - setup guide", () => {
|
||||
);
|
||||
|
||||
it("locks the production embed step when JWT is not enabled", () => {
|
||||
cy.visit("/embedding/get-started");
|
||||
cy.visit("/admin/embedding/setup-guide");
|
||||
|
||||
cy.log("jwt should be disabled by default");
|
||||
cy.request("GET", "/api/session/properties").then(({ body }) => {
|
||||
expect(body["jwt-enabled"]).to.equal(false);
|
||||
});
|
||||
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Embed in production with SSO")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.should("have.attr", "aria-disabled", "true")
|
||||
.realHover();
|
||||
.closest("button")
|
||||
.icon("lock")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByRole("tooltip").should("have.text", "Set up SSO to unlock");
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Embed in production with SSO")
|
||||
.closest("button")
|
||||
.findByText("Complete the other steps to unlock")
|
||||
.should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1403,7 +1438,7 @@ describe("scenarios - setup guide", () => {
|
||||
H.addPostgresDatabase("QA Postgres12");
|
||||
|
||||
cy.get<number>("@postgresID").then((postgresId) => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
H.main()
|
||||
.findByRole("radio", { name: /Connection impersonation/ })
|
||||
@@ -1468,7 +1503,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("should show 'no compatible databases' message when only Sample Database exists", () => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("select connection impersonation strategy");
|
||||
H.main()
|
||||
@@ -1496,7 +1531,7 @@ describe("scenarios - setup guide", () => {
|
||||
it("should show disabled database with tooltip when database does not support connection impersonation", function () {
|
||||
H.addPostgresDatabase("QA Postgres12");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
H.main()
|
||||
.findByRole("radio", { name: /Connection impersonation/ })
|
||||
@@ -1550,7 +1585,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("creates a tenant with database_role attribute when using connection impersonation", () => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("select connection impersonation strategy");
|
||||
H.main()
|
||||
@@ -1618,9 +1653,9 @@ describe("scenarios - setup guide", () => {
|
||||
cy.intercept("PUT", "/api/permissions/graph").as(
|
||||
"updatePermissionsGraph",
|
||||
);
|
||||
cy.intercept("GET", "/api/embedding-hub/checklist").as("getChecklist");
|
||||
cy.intercept("GET", "/api/ee/embedding-hub/checklist").as("getChecklist");
|
||||
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log(
|
||||
"wait for checklist to load and page to settle on the summary step",
|
||||
@@ -1710,7 +1745,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("creates a tenant with database_slug attribute when using database routing", () => {
|
||||
cy.visit("/embedding/get-started/permissions");
|
||||
cy.visit("/admin/embedding/setup-guide/permissions");
|
||||
|
||||
cy.log("select database routing strategy");
|
||||
H.main()
|
||||
@@ -1768,7 +1803,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("disables the Enable JWT button when IdP URI is empty", () => {
|
||||
cy.visit("/embedding/get-started/sso");
|
||||
cy.visit("/admin/embedding/setup-guide/sso");
|
||||
|
||||
cy.findByLabelText(/JWT Identity Provider URI/i)
|
||||
.should("be.visible")
|
||||
@@ -1783,7 +1818,7 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
it("can configure JWT auth and complete SSO setup", () => {
|
||||
cy.visit("/embedding/get-started/sso");
|
||||
cy.visit("/admin/embedding/setup-guide/sso");
|
||||
|
||||
cy.log("no steps should be completed initially");
|
||||
H.main().icon("check").should("not.exist");
|
||||
@@ -1841,24 +1876,25 @@ describe("scenarios - setup guide", () => {
|
||||
});
|
||||
|
||||
cy.log("should go back to setup guide");
|
||||
cy.url().should("include", "/embedding/get-started");
|
||||
cy.url().should("include", "/admin/embedding/setup-guide");
|
||||
cy.url().should("not.include", "/sso");
|
||||
|
||||
cy.log("'Configure SSO' card should be marked as done");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
.findByText("Set up SSO")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Configure SSO")
|
||||
.closest("button")
|
||||
.scrollIntoView()
|
||||
.findByLabelText("Step 5 complete", { timeout: 10_000 })
|
||||
.findByText("Done", { timeout: 10_000 })
|
||||
.should("be.visible");
|
||||
|
||||
cy.log("'Embed in production with SSO' should now be unlocked");
|
||||
cy.findByTestId("embedding-hub-main")
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Embed in production with SSO")
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.closest('[data-testid="embedding-hub-checklist-card"]')
|
||||
.should("not.have.attr", "aria-disabled");
|
||||
.closest("button")
|
||||
.icon("lock")
|
||||
.should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1874,7 +1910,7 @@ describe("scenarios - setup guide", () => {
|
||||
"jwt-shared-secret": "0".repeat(64),
|
||||
});
|
||||
|
||||
cy.visit("/embedding/get-started/sso");
|
||||
cy.visit("/admin/embedding/setup-guide/sso");
|
||||
|
||||
cy.log("step 1 should be marked as done");
|
||||
H.main()
|
||||
|
||||
1418
e2e/test/scenarios/explorations/explorations.cy.spec.ts
Normal file
1418
e2e/test/scenarios/explorations/explorations.cy.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user